Minor fixes and geom module documentation

This commit is contained in:
Balakumar Sundaralingam
2024-08-05 21:52:45 -07:00
parent 3690d28c54
commit a027cbcf38
37 changed files with 2754 additions and 656 deletions

View File

@@ -505,6 +505,49 @@ class CudaRobotModel(CudaRobotModelConfig):
return self.get_state(js.position, link_name, calculate_jacobian)
def compute_kinematics_from_joint_state(
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 compute_kinematics_from_joint_position(
self,
joint_position: torch.Tensor,
link_name: Optional[str] = None,
calculate_jacobian: bool = False,
) -> CudaRobotModelState:
"""Compute forward kinematics of the robot.
Args:
joint_position: Joint position of robot. Assumed to only contain active joints in the
order specified in :attr:`CudaRobotModel.joint_names`.
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.
"""
return self.get_state(joint_position, link_name, calculate_jacobian)
def get_robot_link_meshes(self) -> List[Mesh]:
"""Get meshes of all links of the robot.
@@ -578,12 +621,12 @@ class CudaRobotModel(CudaRobotModelConfig):
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.
Note that only the links specified in :class:`~CudaRobotModelConfig.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`.
:class:`~CudaRobotModelConfig.link_names`.
Returns:
Pose: Poses of links at given joint configuration.
@@ -839,6 +882,19 @@ class CudaRobotModel(CudaRobotModelConfig):
return True
def get_active_js(self, full_js: JointState):
"""Get joint state of active joints of the robot.
Args:
full_js: Joint state of all joints.
Returns:
JointState: Joint state of active joints.
"""
active_jnames = self.joint_names
out_js = full_js.get_ordered_joint_state(active_jnames)
return out_js
@property
def ee_link(self) -> str:
"""End-effector link of the robot. Changing requires reinitializing this class."""

View File

@@ -8,4 +8,9 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""CuRoboLib Module."""
"""
cuRoboLib module contains CUDA implementations (kernels) of robotics algorithms, wrapped in
C++, and compiled with PyTorch for use in Python.
All implementations are in ``.cu`` files in ``cpp`` sub-directory.
"""

View File

@@ -24,7 +24,7 @@ except ImportError:
from torch.utils.cpp_extension import load
# CuRobo
from curobo.curobolib.util_file import add_cpp_path
from curobo.util_file import add_cpp_path
kinematics_fused_cu = load(
name="kinematics_fused_cu",

View File

@@ -24,7 +24,7 @@ except ImportError:
from torch.utils.cpp_extension import load
# CuRobo
from curobo.curobolib.util_file import add_cpp_path
from curobo.util_file import add_cpp_path
line_search_cu = load(
name="line_search_cu",

View File

@@ -24,7 +24,7 @@ except ImportError:
from torch.utils.cpp_extension import load
# CuRobo
from curobo.curobolib.util_file import add_cpp_path
from curobo.util_file import add_cpp_path
lbfgs_step_cu = load(
name="lbfgs_step_cu",

View File

@@ -23,7 +23,7 @@ except ImportError:
from torch.utils.cpp_extension import load
# CuRobo
from curobo.curobolib.util_file import add_cpp_path
from curobo.util_file import add_cpp_path
log_warn("tensor_step_cu not found, jit compiling...")
tensor_step_cu = load(

View File

@@ -8,27 +8,9 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""Deprecated, use functions from :mod:`curobo.util_file` instead."""
# Standard Library
import os
def get_cpp_path():
path = os.path.dirname(__file__)
return os.path.join(path, "cpp")
def join_path(path1, path2):
if isinstance(path2, str):
return os.path.join(path1, path2)
else:
return path2
def add_cpp_path(sources):
cpp_path = get_cpp_path()
new_list = []
for s in sources:
s = join_path(cpp_path, s)
new_list.append(s)
return new_list
# CuRobo
from curobo.util_file import add_cpp_path, get_cpp_path, join_path

View File

@@ -10,8 +10,8 @@
#
"""
This module contains code for geometric processing such as pointcloud processing, analytic signed
distance computation for the environment, and also signed distance computation between robot and the
environment. These functions can be used for robot self collision checking and also for robot
environment collision checking.
This module contains functions for geometric processing such as pointcloud processing, analytic
signed distance computation for the environment, and also signed distance computation between robot
and the environment. These functions can be used for robot self collision checking and also for
robot environment collision checking.
"""

View File

@@ -8,6 +8,7 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""Computer Vision functions, including projection between depth and pointclouds."""
# Third Party
import torch
@@ -17,15 +18,18 @@ from curobo.util.torch_utils import get_torch_jit_decorator
@get_torch_jit_decorator()
def project_depth_to_pointcloud(depth_image: torch.Tensor, intrinsics_matrix: torch.Tensor):
"""Projects numpy depth image to point cloud.
def project_depth_to_pointcloud(
depth_image: torch.Tensor,
intrinsics_matrix: torch.Tensor,
) -> torch.Tensor:
"""Projects depth image to point cloud.
Args:
np_depth_image: numpy array float, shape (h, w).
intrinsics array: numpy array float, 3x3 intrinsics matrix.
depth_image: torch tensor of shape (b, h, w).
intrinsics array: torch tensor for intrinsics matrix of shape (b, 3, 3).
Returns:
array of float (h, w, 3)
torch tensor of shape (b, h, w, 3)
"""
fx = intrinsics_matrix[0, 0]
fy = intrinsics_matrix[1, 1]
@@ -48,16 +52,21 @@ def project_depth_to_pointcloud(depth_image: torch.Tensor, intrinsics_matrix: to
@get_torch_jit_decorator()
def get_projection_rays(
height: int, width: int, intrinsics_matrix: torch.Tensor, depth_to_meter: float = 0.001
):
"""Projects numpy depth image to point cloud.
height: int,
width: int,
intrinsics_matrix: torch.Tensor,
depth_to_meter: float = 0.001,
) -> torch.Tensor:
"""Get projection rays for a image size and batch of intrinsics matrices.
Args:
np_depth_image: numpy array float, shape (h, w).
intrinsics array: numpy array float, 3x3 intrinsics matrix.
height: Height of the images.
width: Width of the images.
intrinsics_matrix: Batch of intrinsics matrices of shape (b, 3, 3).
depth_to_meter: Scaling factor to convert depth to meters.
Returns:
array of float (h, w, 3)
torch.Tensor: Projection rays of shape (b, height * width, 3).
"""
fx = intrinsics_matrix[:, 0:1, 0:1]
fy = intrinsics_matrix[:, 1:2, 1:2]
@@ -92,15 +101,15 @@ def get_projection_rays(
def project_pointcloud_to_depth(
pointcloud: torch.Tensor,
output_image: torch.Tensor,
):
"""Projects pointcloud to depth image
) -> torch.Tensor:
"""Projects pointcloud to depth image based on indices.
Args:
np_depth_image: numpy array float, shape (h, w).
intrinsics array: numpy array float, 3x3 intrinsics matrix.
pointcloud: PointCloud of shape (b, h, w, 3).
output_image: Image of shape (b, h, w).
Returns:
array of float (h, w)
torch.Tensor: Depth image of shape (b, h, w).
"""
width, height = output_image.shape
@@ -112,10 +121,26 @@ def project_pointcloud_to_depth(
@get_torch_jit_decorator()
def project_depth_using_rays(
depth_image: torch.Tensor, rays: torch.Tensor, filter_origin: bool = False
):
depth_image: torch.Tensor,
rays: torch.Tensor,
filter_origin: bool = False,
depth_threshold: float = 0.01,
) -> torch.Tensor:
"""Project depth image to pointcloud using projection rays.
Projection rays can be calculated using :func:`~curobo.geom.cv.get_projection_rays` function.
Args:
depth_image: Dpepth image of shape (b, h, w).
rays: Projection rays of shape (b, h * w, 3).
filter_origin: Remove points with depth less than depth_threshold.
depth_threshold: Threshold to filter points.
Returns:
Pointcloud of shape (b, h * w, 3).
"""
if filter_origin:
depth_image = torch.where(depth_image < 0.01, 0, depth_image)
depth_image = torch.where(depth_image < depth_threshold, 0, depth_image)
depth_image = depth_image.view(depth_image.shape[0], -1, 1).contiguous()
points = depth_image * rays

View File

@@ -8,14 +8,14 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""Module contains deprecated code for computing Signed Distance Field and it's gradient."""
# Third Party
import torch
# from curobo.util.torch_utils import get_torch_jit_decorator
# @get_torch_jit_decorator()
def lookup_distance(pt, dist_matrix_flat, num_voxels):
"""Lookup distance from distance matrix."""
# flatten:
ind_pt = (
(pt[..., 0]) * (num_voxels[1] * num_voxels[2]) + pt[..., 1] * num_voxels[2] + pt[..., 2]
@@ -26,6 +26,7 @@ def lookup_distance(pt, dist_matrix_flat, num_voxels):
# @get_torch_jit_decorator()
def compute_sdf_gradient(pt, dist_matrix_flat, num_voxels, dist):
"""Compute gradient of SDF."""
grad_l = []
for i in range(3): # x,y,z
pt_n = pt.clone()
@@ -50,6 +51,8 @@ def compute_sdf_gradient(pt, dist_matrix_flat, num_voxels, dist):
class SDFGrid(torch.autograd.Function):
"""Sdf grid torch function."""
@staticmethod
def forward(ctx, pt, dist_matrix_flat, num_voxels):
# input = x_id,y_id,z_id

View File

@@ -8,17 +8,28 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""Module contains uilities for world collision checkers."""
# CuRobo
from curobo.geom.sdf.world import CollisionCheckerType, WorldCollisionConfig
from curobo.geom.sdf.world import (
CollisionCheckerType,
WorldCollision,
WorldCollisionConfig,
WorldPrimitiveCollision,
)
from curobo.util.logger import log_error
def create_collision_checker(config: WorldCollisionConfig):
if config.checker_type == CollisionCheckerType.PRIMITIVE:
# CuRobo
from curobo.geom.sdf.world import WorldPrimitiveCollision
def create_collision_checker(config: WorldCollisionConfig) -> WorldCollision:
"""Create collision checker based on configuration.
Args:
config: Input world collision configuration.
Returns:
Type of collision checker based on configuration.
"""
if config.checker_type == CollisionCheckerType.PRIMITIVE:
return WorldPrimitiveCollision(config)
elif config.checker_type == CollisionCheckerType.BLOX:
# CuRobo
@@ -37,4 +48,3 @@ def create_collision_checker(config: WorldCollisionConfig):
return WorldVoxelCollision(config)
else:
log_error("Unknown Collision Checker type: " + config.checker_type, exc_info=True)
raise NotImplementedError

View File

@@ -8,6 +8,7 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""Warp-lang based world collision functions are implemented as torch autograd functions."""
# Third Party
import torch
@@ -28,6 +29,8 @@ else:
class SdfMeshWarpPy(torch.autograd.Function):
"""Pytorch autograd function for computing signed distance between spheres and meshes."""
@staticmethod
def forward(
ctx,
@@ -109,6 +112,8 @@ class SdfMeshWarpPy(torch.autograd.Function):
class SweptSdfMeshWarpPy(torch.autograd.Function):
"""Compute signed distance between trajectory of spheres and meshes."""
@staticmethod
def forward(
ctx,

View File

@@ -8,6 +8,7 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""CUDA kernels implemented in warp-lang for computing signed distance to meshes."""
# Third Party
import warp as wp
@@ -18,6 +19,7 @@ def mesh_query_point_fn(
point: wp.vec3,
max_distance: float,
):
"""Query point on mesh."""
collide_result = wp.mesh_query_point(idx, point, max_distance)
return collide_result
@@ -46,6 +48,7 @@ def get_swept_closest_pt_batch_env(
env_query_idx: wp.array(dtype=wp.int32),
use_batch_env: wp.uint8,
):
"""Compute signed distance between a trajectory of a sphere and world meshes."""
# we launch nspheres kernels
# compute gradient here and return
# distance is negative outside and positive inside
@@ -364,6 +367,7 @@ def get_closest_pt_batch_env(
use_batch_env: wp.uint8,
compute_esdf: wp.uint8,
):
"""Compute signed distance between a sphere and world meshes."""
# we launch nspheres kernels
# compute gradient here and return
# distance is negative outside and positive inside

View File

@@ -8,6 +8,8 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""Deprecated warp kernels that use API compatibile with warp-lang < 1.0.0"""
# Third Party
import warp as wp
@@ -36,6 +38,7 @@ def get_swept_closest_pt_batch_env(
env_query_idx: wp.array(dtype=wp.int32),
use_batch_env: wp.uint8,
):
"""Kernel to compute swept closest point to mesh."""
# we launch nspheres kernels
# compute gradient here and return
# distance is negative outside and positive inside
@@ -352,6 +355,7 @@ def get_closest_pt_batch_env(
use_batch_env: wp.uint8,
compute_esdf: wp.uint8,
):
"""Kernel to compute closest point to mesh."""
# we launch nspheres kernels
# compute gradient here and return
# distance is negative outside and positive inside

View File

@@ -8,6 +8,9 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""World representations for computing signed distance are implemented in this module."""
from __future__ import annotations
# Standard Library
from dataclasses import dataclass
@@ -27,16 +30,39 @@ from curobo.util.logger import log_error, log_info, log_warn
@dataclass
class CollisionBuffer:
"""Helper class stores all buffers required to compute collision cost and gradients."""
#: Buffer to store signed distance cost value for each query sphere.
distance_buffer: torch.Tensor
#: Buffer to store gradient of signed distance cost value for each query sphere.
grad_distance_buffer: torch.Tensor
#: Buffer to store sparsity index for each query sphere. If sphere's value is not in collsiion,
#: sparsity index is set to 0, else 1. Used to prevent rewriting 0 values in distance_buffer
#: and grad_distance_buffer.
sparsity_index_buffer: torch.Tensor
#: Shape of the distance buffer. This is used to check if the buffer needs to be recreated.
shape: Optional[torch.Size] = None
def __post_init__(self):
"""Initialize the buffer shape if not provided."""
self.shape = self.distance_buffer.shape
@classmethod
def initialize_from_shape(cls, shape: torch.Size, tensor_args: TensorDeviceType):
def initialize_from_shape(
cls, shape: torch.Size, tensor_args: TensorDeviceType
) -> CollisionBuffer:
"""Initialize the CollisionBuffer from the given shape of query spheres.
Args:
shape: Input shape of the query spheres. The shape is (batch, horizon, n_spheres, 4).
tensor_args: Device and precision of the tensors.
Returns:
CollisionBuffer: Initialized CollisionBuffer object.
"""
batch, horizon, n_spheres, _ = shape
distance_buffer = torch.zeros(
(batch, horizon, n_spheres),
@@ -56,6 +82,12 @@ class CollisionBuffer:
return CollisionBuffer(distance_buffer, grad_distance_buffer, sparsity_idx)
def _update_from_shape(self, shape: torch.Size, tensor_args: TensorDeviceType):
"""Update shape of buffers.
Args:
shape: New shape of the query spheres.
tensor_args: device and precision of the tensors.
"""
batch, horizon, n_spheres, _ = shape
self.distance_buffer = torch.zeros(
(batch, horizon, n_spheres),
@@ -75,23 +107,24 @@ class CollisionBuffer:
self.shape = shape[:3]
def update_buffer_shape(self, shape: torch.Size, tensor_args: TensorDeviceType):
"""Update the buffer shape if the shape of the query spheres changes.
Args:
shape: New shape of the query spheres.
tensor_args: device and precision of the tensors.
"""
if self.shape != shape[:3]:
# print("Recreating PRIM: ",self.shape, shape)
# self = CollisionBuffer.initialize_from_shape(
# shape,
# tensor_args,
# )
self._update_from_shape(shape, tensor_args)
# print("New shape:",self.shape)
def clone(self):
def clone(self) -> CollisionBuffer:
"""Clone the CollisionBuffer object."""
dist_buffer = self.distance_buffer.clone()
grad_buffer = self.grad_distance_buffer.clone()
sparse_buffer = self.sparsity_index_buffer.clone()
return CollisionBuffer(dist_buffer, grad_buffer, sparse_buffer)
def __mul__(self, scalar: float):
def __mul__(self, scalar: float) -> CollisionBuffer:
"""Multiply the CollisionBuffer by a scalar value."""
self.distance_buffer *= scalar
self.grad_distance_buffer *= scalar
self.sparsity_index_buffer *= int(scalar)
@@ -100,18 +133,25 @@ class CollisionBuffer:
@dataclass
class CollisionQueryBuffer:
"""Class stores all buffers required to query collisions
This class currently has three main buffers. We initialize the required
buffers based on ?
"""
"""Class stores all buffers required to query collisions across world representations."""
#: Buffer to store signed distance cost value for Cuboid world obstacles.
primitive_collision_buffer: Optional[CollisionBuffer] = None
#: Buffer to store signed distance cost value for Mesh world obstacles.
mesh_collision_buffer: Optional[CollisionBuffer] = None
#: Buffer to store signed distance cost value for Blox world obstacles.
blox_collision_buffer: Optional[CollisionBuffer] = None
#: Buffer to store signed distance cost value for Voxel world obstacles.
voxel_collision_buffer: Optional[CollisionBuffer] = None
#: Shape of the query spheres. This is used to check if the buffer needs to be recreated.
shape: Optional[torch.Size] = None
def __post_init__(self):
"""Initialize the shape of the query spheres if not provided."""
if self.shape is None:
if self.primitive_collision_buffer is not None:
self.shape = self.primitive_collision_buffer.shape
@@ -122,7 +162,8 @@ class CollisionQueryBuffer:
elif self.voxel_collision_buffer is not None:
self.shape = self.voxel_collision_buffer.shape
def __mul__(self, scalar: float):
def __mul__(self, scalar: float) -> CollisionQueryBuffer:
"""Multiply tensors by a scalar value."""
if self.primitive_collision_buffer is not None:
self.primitive_collision_buffer = self.primitive_collision_buffer * scalar
if self.mesh_collision_buffer is not None:
@@ -133,7 +174,8 @@ class CollisionQueryBuffer:
self.voxel_collision_buffer = self.voxel_collision_buffer * scalar
return self
def clone(self):
def clone(self) -> CollisionQueryBuffer:
"""Clone the CollisionQueryBuffer object."""
prim_buffer = mesh_buffer = blox_buffer = voxel_buffer = None
if self.primitive_collision_buffer is not None:
prim_buffer = self.primitive_collision_buffer.clone()
@@ -157,7 +199,17 @@ class CollisionQueryBuffer:
shape: torch.Size,
tensor_args: TensorDeviceType,
collision_types: Dict[str, bool],
):
) -> CollisionQueryBuffer:
"""Initialize the CollisionQueryBuffer from the given shape of query spheres.
Args:
shape: Input shape of the query spheres. The shape is (batch, horizon, n_spheres, 4).
tensor_args: Device and precision of the tensors.
collision_types: Dictionary of collision types to initialize buffers for.
Returns:
CollisionQueryBuffer: Initialized CollisionQueryBuffer object.
"""
primitive_buffer = mesh_buffer = blox_buffer = voxel_buffer = None
if "primitive" in collision_types and collision_types["primitive"]:
primitive_buffer = CollisionBuffer.initialize_from_shape(shape, tensor_args)
@@ -176,7 +228,17 @@ class CollisionQueryBuffer:
shape: torch.Size,
tensor_args: TensorDeviceType,
collision_types: Dict[str, bool],
):
) -> CollisionQueryBuffer:
"""Create the CollisionQueryBuffer from the given shape of query spheres.
Args:
shape: Input shape of the query spheres. The shape is (batch, horizon, n_spheres, 4).
tensor_args: Device and precision of the tensors.
collision_types: Dictionary of collision types to initialize buffers for.
Returns:
CollisionQueryBuffer: Initialized CollisionQueryBuffer object.
"""
if "primitive" in collision_types and collision_types["primitive"]:
self.primitive_collision_buffer = CollisionBuffer.initialize_from_shape(
shape, tensor_args
@@ -195,6 +257,13 @@ class CollisionQueryBuffer:
tensor_args: TensorDeviceType,
collision_types: Optional[Dict[str, bool]],
):
"""Update buffer shape if it doesn't match existing shape.
Args:
shape: New shape of the query spheres.
tensor_args: Device and precision of the tensors.
collision_types: Dictionary of collision types to update buffers for.
"""
# update buffers:
assert len(shape) == 4 # shape is: batch, horizon, n_spheres, 4
if self.shape is None: # buffers not initialized:
@@ -215,7 +284,12 @@ class CollisionQueryBuffer:
def get_gradient_buffer(
self,
):
) -> Optional[torch.Tensor]:
"""Compute the gradient buffer by summing the gradient buffers of all collision types.
Returns:
torch.Tensor: Gradient buffer for all collision types
"""
prim_buffer = mesh_buffer = blox_buffer = None
current_buffer = None
if self.primitive_collision_buffer is not None:
@@ -245,10 +319,7 @@ class CollisionQueryBuffer:
class CollisionCheckerType(Enum):
"""Type of collision checker to use.
Args:
Enum (_type_): _description_
"""
"""Type of collision checker to use."""
PRIMITIVE = "PRIMITIVE"
BLOX = "BLOX"
@@ -258,15 +329,37 @@ class CollisionCheckerType(Enum):
@dataclass
class WorldCollisionConfig:
"""Configuration parameters for the WorldCollision object."""
#: Device and precision of the tensors.
tensor_args: TensorDeviceType
#: World obstacles to load for collision checking.
world_model: Optional[Union[List[WorldConfig], WorldConfig]] = None
#: Number of obstacles to cache for collision checking across representations.
#: Use this to create a fixed size buffer for collision checking, e.g, {'obb': 1000} will
#: create a buffer of 1000 cuboids for each environment.
cache: Optional[Dict[Obstacle, int]] = None
#: Number of environments to use for collision checking.
n_envs: int = 1
#: Type of collision checker to use.
checker_type: CollisionCheckerType = CollisionCheckerType.PRIMITIVE
#: Maximum distance to compute collision checking cost outside the object. This value is
#: added in addition to a query sphere radius and collision activation distance. A smaller
#: value will speedup collision checking but can result in slower convergence with swept
#: sphere collision checking.
max_distance: Union[torch.Tensor, float] = 0.1
#: Maximum distance outside an obstacle to use when computing euclidean signed distance field
#: (ESDF) from different world representations.
max_esdf_distance: Union[torch.Tensor, float] = 100.0
def __post_init__(self):
"""Post initialization method to set default values."""
if self.world_model is not None and isinstance(self.world_model, list):
self.n_envs = len(self.world_model)
if isinstance(self.max_distance, float):
@@ -279,7 +372,17 @@ class WorldCollisionConfig:
world_coll_checker_dict: Dict,
world_model_dict: Union[WorldConfig, Dict, List[WorldConfig]] = None,
tensor_args: TensorDeviceType = TensorDeviceType(),
):
) -> WorldCollisionConfig:
"""Load the WorldCollisionConfig from a dictionary.
Args:
world_coll_checker_dict: Dictionary containing the configuration parameters.
world_model_dict: Dictionary containing obstacles.
tensor_args: Device and precision of the tensors.
Returns:
WorldCollisionConfig: Initialized WorldCollisionConfig object.
"""
world_cfg = world_model_dict
if world_model_dict is not None:
if isinstance(world_model_dict, list) and isinstance(world_model_dict[0], dict):
@@ -295,7 +398,14 @@ class WorldCollisionConfig:
class WorldCollision(WorldCollisionConfig):
"""Base class for computing signed distance between query spheres and world obstacles."""
def __init__(self, config: Optional[WorldCollisionConfig] = None):
"""Initialize the WorldCollision object.
Args:
config: Configuration parameters for the WorldCollision object.
"""
if config is not None:
WorldCollisionConfig.__init__(self, **vars(config))
self.collision_types = {} # Use this dictionary to store collision types
@@ -303,8 +413,28 @@ class WorldCollision(WorldCollisionConfig):
self._cache_voxelization_collision_buffer = None
def load_collision_model(self, world_model: WorldConfig):
"""Load the world obstacles for collision checking."""
raise NotImplementedError
def update_obstacle_pose_in_world_model(self, name: str, pose: Pose, env_idx: int = 0):
"""Update the pose of an obstacle in the world model.
Args:
name: Name of the obstacle to update.
pose: Pose to update the obstacle.
env_idx: Environment index to update the obstacle.
"""
if self.world_model is None:
return
if isinstance(self.world_model, list):
world = self.world_model[env_idx]
else:
world = self.world_model
obstacle = world.get_obstacle(name)
if obstacle is not None:
obstacle.pose = pose.to_list()
def get_sphere_distance(
self,
query_sphere,
@@ -316,11 +446,7 @@ class WorldCollision(WorldCollisionConfig):
sum_collisions: bool = True,
compute_esdf: bool = False,
):
"""
Computes the signed distance via analytic function
Args:
tensor_sphere: b, n, 4
"""
"""Compute the signed distance between query spheres and world obstacles."""
raise NotImplementedError
def get_sphere_collision(
@@ -332,12 +458,7 @@ class WorldCollision(WorldCollisionConfig):
env_query_idx: Optional[torch.Tensor] = None,
return_loss: bool = False,
):
"""
Computes the signed distance via analytic function
Args:
tensor_sphere: b, n, 4
we assume we don't need gradient for this function. If you need gradient, use get_sphere_distance
"""
"""Compute binary collision between query spheres and world obstacles."""
raise NotImplementedError
@@ -354,6 +475,7 @@ class WorldCollision(WorldCollisionConfig):
return_loss: bool = False,
sum_collisions: bool = True,
):
"""Compute the signed distance between trajectory of spheres and world obstacles."""
raise NotImplementedError
def get_swept_sphere_collision(
@@ -368,17 +490,7 @@ class WorldCollision(WorldCollisionConfig):
env_query_idx: Optional[torch.Tensor] = None,
return_loss: bool = False,
):
raise NotImplementedError
def get_sphere_trace(
self,
query_sphere,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
sweep_steps: int,
env_query_idx: Optional[torch.Tensor] = None,
return_loss: bool = False,
):
"""Compute binary collision between trajectory of spheres and world obstacles."""
raise NotImplementedError
def get_voxels_in_bounding_box(
@@ -386,14 +498,29 @@ class WorldCollision(WorldCollisionConfig):
cuboid: Cuboid = Cuboid(name="test", pose=[0, 0, 0, 1, 0, 0, 0], dims=[1, 1, 1]),
voxel_size: float = 0.02,
) -> Union[List[Cuboid], torch.Tensor]:
"""Get occupied voxels in a grid bounded by the given cuboid.
Args:
cuboid: Bounding box to get the occupied voxels.
voxel_size: Size of the voxel grid.
Returns:
Tensor with the occupied voxels in the bounding box.
"""
new_grid = self.get_occupancy_in_bounding_box(cuboid, voxel_size)
occupied = new_grid.get_occupied_voxels(0.0)
return occupied
def clear_voxelization_cache(self):
"""Clear cache that contains voxelization locations."""
self._cache_voxelization = None
def update_cache_voxelization(self, new_grid: VoxelGrid):
"""Update locaiton of voxels based on new grid parameters. Only for debugging.
Args:
new_grid: New grid to use for getting voxelized occupancy of current world obstacles.
"""
if (
self._cache_voxelization is None
or self._cache_voxelization.voxel_size != new_grid.voxel_size
@@ -418,6 +545,16 @@ class WorldCollision(WorldCollisionConfig):
cuboid: Cuboid = Cuboid(name="test", pose=[0, 0, 0, 1, 0, 0, 0], dims=[1, 1, 1]),
voxel_size: float = 0.02,
) -> VoxelGrid:
"""Get the occupancy of voxels in a grid bounded by the given cuboid.
Args:
cuboid: Cuboid to get the occupancy of voxels. Provide pose and dimenstions to
create occupancy information.
voxel_size: Size in meters to use as the voxel size.
Returns:
Grid with the occupancy of voxels in the bounding box.
"""
new_grid = VoxelGrid(
name=cuboid.name, dims=cuboid.dims, pose=cuboid.pose, voxel_size=voxel_size
)
@@ -448,6 +585,20 @@ class WorldCollision(WorldCollisionConfig):
voxel_size: float = 0.02,
dtype=torch.float32,
) -> VoxelGrid:
"""Get the Euclidean signed distance in a grid bounded by the given cuboid.
Distance is positive inside obstacles and negative outside obstacles.
Args:
cuboid: Bounding cuboid to query signed distance.
voxel_size: Size of the voxels in meters.
dtype: Data type of the feature tensor. Use :var:`torch.bfloat16` or
:var:`torch.float8_e4m3fn` for reduced memory usage.
Returns:
Voxels with the Euclidean signed distance in the bounding box.
"""
new_grid = VoxelGrid(
name=cuboid.name,
dims=cuboid.dims,
@@ -483,9 +634,22 @@ class WorldCollision(WorldCollisionConfig):
cuboid: Cuboid = Cuboid(name="test", pose=[0, 0, 0, 1, 0, 0, 0], dims=[1, 1, 1]),
voxel_size: float = 0.02,
) -> Mesh:
"""Get a mesh representation of the world obstacles based on occupancy in a bounding box.
This uses marching cubes to create a mesh representation of the world obstacles. Use this
to debug world representations.
Args:
cuboid: Bounding box to get the mesh representation.
voxel_size: Size of the voxels in meters.
Returns:
Mesh representation of the world obstacles in the bounding box.
"""
voxels = self.get_voxels_in_bounding_box(cuboid, voxel_size)
# voxels = voxels.cpu().numpy()
# cuboids = [Cuboid(name="c_"+str(x), pose=[voxels[x,0],voxels[x,1],voxels[x,2], 1,0,0,0], dims=[voxel_size, voxel_size, voxel_size]) for x in range(voxels.shape[0])]
# cuboids = [Cuboid(name="c_"+str(x), pose=[voxels[x,0],voxels[x,1],voxels[x,2], 1,0,0,0],
# dims=[voxel_size, voxel_size, voxel_size]) for x in range(voxels.shape[0])]
# mesh = WorldConfig(cuboid=cuboids).get_mesh_world(True).mesh[0]
mesh = Mesh.from_pointcloud(
voxels[:, :3].detach().cpu().numpy(),
@@ -493,11 +657,27 @@ class WorldCollision(WorldCollisionConfig):
)
return mesh
def get_obstacle_names(self, env_idx: int = 0):
def get_obstacle_names(self, env_idx: int = 0) -> List[str]:
"""Get the names of the obstacles in the world.
Args:
env_idx: Environment index to get the obstacle names.
Returns:
Obstacle names in the world.
"""
return []
def check_obstacle_exists(self, name: str, env_idx: int = 0) -> bool:
"""Check if an obstacle exists in the world by name.
Args:
name: Name of the obstacle to check.
env_idx: Environment index to check the obstacle.
Returns:
True if the obstacle exists in the world, else False.
"""
obstacle_names = self.get_obstacle_names(env_idx)
if name in obstacle_names:
@@ -507,14 +687,14 @@ class WorldCollision(WorldCollisionConfig):
class WorldPrimitiveCollision(WorldCollision):
"""World Oriented Bounding Box representation object
We represent the world with oriented bounding boxes. For speed, we assume there is a
maximum number of obbs that can be instantiated. This number is read from the WorldCollisionConfig.
If no cache is setup, we use the number from the first call of load_collision_model.
"""
"""World collision checking with oriented bounding boxes (cuboids) for obstacles."""
def __init__(self, config: WorldCollisionConfig):
"""Initialize the WorldPrimitiveCollision object.
Args:
config: Configuration parameters for the WorldPrimitiveCollision object.
"""
super().__init__(config)
self._world_cubes = None
self._cube_tensor_list = None
@@ -529,17 +709,36 @@ class WorldPrimitiveCollision(WorldCollision):
self.load_collision_model(self.world_model)
def _init_cache(self):
"""Initialize obstacles cache to allow for dynamic addition of obstacles."""
if self.cache is not None and "obb" in self.cache and self.cache["obb"] not in [None, 0]:
self._create_obb_cache(self.cache["obb"])
def load_collision_model(
self, world_config: WorldConfig, env_idx=0, fix_cache_reference: bool = False
):
"""Load world obstacles into collision checker.
Args:
world_config: Obstacles to load into the collision checker.
env_idx: Environment index to load the obstacles.
fix_cache_reference: If True, throws error if number of obstacles is greater than
cache. If False, creates a larger cache. Note that when using collision checker
inside a recorded cuda graph, recreating the cache will break the graph as the
reference pointer to the cache will change.
"""
self._load_collision_model_in_cache(
world_config, env_idx, fix_cache_reference=fix_cache_reference
)
def get_obstacle_names(self, env_idx: int = 0):
def get_obstacle_names(self, env_idx: int = 0) -> List[str]:
"""Get the names of the obstacles in the world.
Args:
env_idx: Environment index to get the obstacle names.
Returns:
Obstacle names in the world.
"""
base_obstacles = super().get_obstacle_names(env_idx)
return self._env_obbs_names[env_idx] + base_obstacles
@@ -615,6 +814,13 @@ class WorldPrimitiveCollision(WorldCollision):
def _load_collision_model_in_cache(
self, world_config: WorldConfig, env_idx: int = 0, fix_cache_reference: bool = False
):
"""Load world obstacles into collision checker cache. This only loads cuboids.
Args:
world_config: World obstacles to load into the collision checker.
env_idx: Environment index to load the obstacles.
fix_cache_reference: If True, does not allow to load more obstacles than cache size.
"""
cube_objs = world_config.cuboid
max_obb = len(cube_objs)
self.world_model = world_config
@@ -646,7 +852,12 @@ class WorldPrimitiveCollision(WorldCollision):
self._env_obbs_names[env_idx][:max_obb] = names_batch
self.collision_types["primitive"] = True
def _create_obb_cache(self, obb_cache):
def _create_obb_cache(self, obb_cache: int):
"""Create cache for cuboid (oriented bounding box) obstacles.
Args:
obb_cache: Number of cuboids to cache for collision checking.
"""
box_dims = (
torch.zeros(
(self.n_envs, obb_cache, 4),
@@ -678,12 +889,18 @@ class WorldPrimitiveCollision(WorldCollision):
env_idx: int,
w_obj_pose: Optional[Pose] = None,
obj_w_pose: Optional[Pose] = None,
):
"""
) -> int:
"""Add cuboid obstacle to world.
Args:
dims: lenght, width, height
position: x,y,z
rotation: matrix (3x3)
name: Name of the obstacle. Must be unique.
dims: Dimensions of the cuboid obstacle [length, width, height].
env_idx: Environment index to add the obstacle to.
w_obj_pose: Pose of the obstacle in world frame.
obj_w_pose: Inverse pose of the obstacle in world frame.
Returns:
Index of the obstacle in the world.
"""
assert w_obj_pose is not None or obj_w_pose is not None
if name in self._env_obbs_names[env_idx]:
@@ -705,7 +922,16 @@ class WorldPrimitiveCollision(WorldCollision):
self,
cuboid: Cuboid,
env_idx: int = 0,
):
) -> int:
"""Add cuboid obstacle to world.
Args:
cuboid: Cuboid to add.
env_idx: Environment index to add the obstacle to.
Returns:
Index of the obstacle in the world.
"""
return self.add_obb_from_raw(
cuboid.name,
self.tensor_args.to_device(cuboid.dims),
@@ -720,12 +946,13 @@ class WorldPrimitiveCollision(WorldCollision):
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update obstacle dimensions
"""Update dimensinots of an existing cuboid obstacle.
Args:
obj_dims (torch.Tensor): [dim.x,dim.y, dim.z], give as [b,3]
obj_idx (torch.Tensor or int):
obj_dims: [dim.x,dim.y, dim.z].
name: Name of the obstacle to update.
env_obj_idx: Index of the obstacle to update. Not required if name is provided.
env_idx: Environment index to update the obstacle.
"""
if env_obj_idx is not None:
self._cube_tensor_list[0][env_obj_idx, :3] = obj_dims
@@ -741,6 +968,13 @@ class WorldPrimitiveCollision(WorldCollision):
enable: bool = True,
env_idx: int = 0,
):
"""Enable/Disable object in collision checking functions.
Args:
name: Name of the obstacle to enable.
enable: True to enable, False to disable.
env_idx: Index of the environment to enable the obstacle in.
"""
return self.enable_obb(enable, name, None, env_idx)
def enable_obb(
@@ -750,12 +984,13 @@ class WorldPrimitiveCollision(WorldCollision):
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update obstacle dimensions
"""Enable/Disable cuboid in collision checking functions.
Args:
obj_dims (torch.Tensor): [dim.x,dim.y, dim.z], give as [b,3]
obj_idx (torch.Tensor or int):
enable: True to enable, False to disable.
name: Name of the obstacle to enable.
env_obj_idx: Index of the obstacle to enable. Not required if name is provided.
env_idx: Index of the environment to enable the obstacle in.
"""
if env_obj_idx is not None:
self._cube_tensor_list[2][env_obj_idx] = int(enable) # enable == 1
@@ -770,11 +1005,28 @@ class WorldPrimitiveCollision(WorldCollision):
name: str,
w_obj_pose: Pose,
env_idx: int = 0,
update_cpu_reference: bool = False,
):
"""Update pose of an existing obstacle.
Args:
name: Name of obstacle.
w_obj_pose: Pose of the obstacle in world frame.
env_idx: Index of the environment to update the obstacle in.
update_cpu_reference: If True, updates the CPU reference with the new pose. This is
useful for debugging and visualization. Only supported for env_idx=0.
"""
if self._env_obbs_names is not None and name in self._env_obbs_names[env_idx]:
self.update_obb_pose(name=name, w_obj_pose=w_obj_pose, env_idx=env_idx)
self.update_obb_pose(
name=name,
w_obj_pose=w_obj_pose,
env_idx=env_idx,
)
else:
log_error("obstacle not found in OBB world model: " + name)
log_warn("obstacle not found in OBB world model: " + name)
if update_cpu_reference:
self.update_obstacle_pose_in_world_model(name, w_obj_pose, env_idx)
def update_obb_pose(
self,
@@ -784,11 +1036,17 @@ class WorldPrimitiveCollision(WorldCollision):
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update pose of a specific objects.
This also updates the signed distance grid to account for the updated object pose.
"""Update pose of an existing cuboid obstacle.
Args:
obj_w_pose: Pose
obj_idx:
w_obj_pose: Pose of the obstacle in world frame.
obj_w_pose: Inverse pose of the obstacle in world frame. Not required if w_obj_pose is
provided.
name: Name of the obstacle to update.
env_obj_idx: Index of the obstacle to update. Not required if name is provided.
env_idx: Index of the environment to update the obstacle.
update_cpu_reference: If True, updates the CPU reference with the new pose. This is
useful for debugging and visualization. Only supported for env_idx=0.
"""
obj_w_pose = self._get_obstacle_poses(w_obj_pose, obj_w_pose)
if env_obj_idx is not None:
@@ -803,12 +1061,21 @@ class WorldPrimitiveCollision(WorldCollision):
w_obj_pose: Optional[Pose] = None,
obj_w_pose: Optional[Pose] = None,
):
"""Get pose of world from obstacle frame of reference.
Args:
w_obj_pose: Pose of the obstacle in world frame.
obj_w_pose: Pose of world in obstacle frame of reference.
Returns:
Pose of world in obstacle frame of reference.
"""
if w_obj_pose is not None:
w_inv_pose = w_obj_pose.inverse()
elif obj_w_pose is not None:
w_inv_pose = obj_w_pose
else:
raise ValueError("Object pose is not given")
log_error("Object pose is not given")
return w_inv_pose
def get_obb_idx(
@@ -816,13 +1083,22 @@ class WorldPrimitiveCollision(WorldCollision):
name: str,
env_idx: int = 0,
) -> int:
"""Get index of the cuboid obstacle in the world.
Args:
name: Name of the obstacle to get the index.
env_idx: Environment index to get the obstacle index.
Returns:
Index of the obstacle in the world.
"""
if name not in self._env_obbs_names[env_idx]:
log_error("Obstacle with name: " + name + " not found in current world", exc_info=True)
return self._env_obbs_names[env_idx].index(name)
def get_sphere_distance(
self,
query_sphere,
query_sphere: torch.Tensor,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
@@ -830,9 +1106,33 @@ class WorldPrimitiveCollision(WorldCollision):
return_loss=False,
sum_collisions: bool = True,
compute_esdf: bool = False,
):
) -> torch.Tensor:
"""Compute the signed distance between query spheres and world obstacles.
This distance can be used as a collision cost for optimization.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Buffer to store collision query results.
weight: Weight of the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
sum_collisions: Sum the collision cost across all obstacles. This variable is currently
not passed to the underlying CUDA kernel as setting this to False caused poor
performance.
compute_esdf: Compute Euclidean signed distance instead of collision cost. When True,
the returned tensor will be the signed distance with positive values inside an
obstacle and negative values outside obstacles.
Returns:
Signed distance between query spheres and world obstacles.
"""
if "primitive" not in self.collision_types or not self.collision_types["primitive"]:
raise ValueError("Primitive Collision has no obstacles")
log_error("Primitive Collision has no obstacles")
b, h, n, _ = query_sphere.shape # This can be read from collision query buffer
use_batch_env = True
@@ -870,18 +1170,32 @@ class WorldPrimitiveCollision(WorldCollision):
def get_sphere_collision(
self,
query_sphere,
query_sphere: torch.Tensor,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
env_query_idx: Optional[torch.Tensor] = None,
return_loss=False,
**kwargs,
):
) -> torch.Tensor:
"""Compute binary collision between query spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Weight to scale the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: True is not supported for binary classification. Set to False.
Returns:
Tensor with binary collision results.
"""
if "primitive" not in self.collision_types or not self.collision_types["primitive"]:
raise ValueError("Primitive Collision has no obstacles")
log_error("Primitive Collision has no obstacles")
if return_loss:
raise ValueError("cannot return loss for classification, use get_sphere_distance")
log_error("cannot return loss for classification, use get_sphere_distance")
b, h, n, _ = query_sphere.shape
use_batch_env = True
if env_query_idx is None:
@@ -915,7 +1229,7 @@ class WorldPrimitiveCollision(WorldCollision):
def get_swept_sphere_distance(
self,
query_sphere,
query_sphere: torch.Tensor,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
@@ -925,14 +1239,38 @@ class WorldPrimitiveCollision(WorldCollision):
env_query_idx: Optional[torch.Tensor] = None,
return_loss=False,
sum_collisions: bool = True,
):
"""
Computes the signed distance via analytic function
) -> torch.Tensor:
"""Compute the signed distance between trajectory of spheres and world obstacles.
Args:
tensor_sphere: b, n, 4
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
sum_collisions: Sum the collision cost across all obstacles. This variable is currently
not passed to the underlying CUDA kernel as setting this to False caused poor
performance.
Returns:
Collision cost between trajectory of spheres and world obstacles.
"""
if "primitive" not in self.collision_types or not self.collision_types["primitive"]:
raise ValueError("Primitive Collision has no obstacles")
log_error("Primitive Collision has no obstacles")
b, h, n, _ = query_sphere.shape
use_batch_env = True
@@ -971,7 +1309,7 @@ class WorldPrimitiveCollision(WorldCollision):
def get_swept_sphere_collision(
self,
query_sphere,
query_sphere: torch.Tensor,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
@@ -980,16 +1318,35 @@ class WorldPrimitiveCollision(WorldCollision):
enable_speed_metric=False,
env_query_idx: Optional[torch.Tensor] = None,
return_loss=False,
):
"""
Computes the signed distance via analytic function
) -> torch.Tensor:
"""Get binary collision between trajectory of spheres and world obstacles.
Args:
tensor_sphere: b, n, 4
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference. This is not used.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization. This is not used.
env_query_idx: Environment index for each batch of query spheres.
return_loss: This is not supported for binary classification. Set to False.
Returns:
Collision value between trajectory of spheres and world obstacles.
"""
if "primitive" not in self.collision_types or not self.collision_types["primitive"]:
raise ValueError("Primitive Collision has no obstacles")
log_error("Primitive Collision has no obstacles")
if return_loss:
raise ValueError("cannot return loss for classify, use get_swept_sphere_distance")
log_error("cannot return loss for classify, use get_swept_sphere_distance")
b, h, n, _ = query_sphere.shape
use_batch_env = True
@@ -1025,6 +1382,7 @@ class WorldPrimitiveCollision(WorldCollision):
return dist
def clear_cache(self):
"""Delete all cuboid obstacles from the world."""
if self._cube_tensor_list is not None:
self._cube_tensor_list[2][:] = 0
self._env_n_obbs[:] = 0

View File

@@ -8,7 +8,7 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""World represented by ESDF layers of nvblox."""
# Standard Library
from typing import List, Optional
@@ -36,19 +36,17 @@ except ImportError:
class WorldBloxCollision(WorldVoxelCollision):
"""World Collision Representaiton using Nvidia's nvblox library.
This class depends on pytorch wrapper for nvblox.
Additionally, this representation does not support batched environments as we only store
one world via nvblox.
This class depends on pytorch wrapper for nvblox. Additionally, this representation does not
support batched environments as we only store one world via nvblox.
There are two ways to use nvblox, one is by loading maps from disk and the other is by
creating maps online. In both these instances, we might load more than one map and need to check
collisions against all maps.
To facilitate online map creation and updation, we build apis in this
class that provide.
creating maps online. In both these instances, we might load more than one map and need to
check collisions against all maps.To facilitate online map creation and updation, we build apis
in this class to process depth images.
"""
def __init__(self, config: WorldCollisionConfig):
"""Initialize with a world collision configuration."""
self._pose_offset = None
self._blox_mapper = None
self._blox_tensor_list = None
@@ -56,6 +54,15 @@ class WorldBloxCollision(WorldVoxelCollision):
super().__init__(config)
def load_collision_model(self, world_model: WorldConfig, fix_cache_reference: bool = False):
"""Load collision model from world obstacles. Only 1 environment is supported.
Args:
world_model: Obstacles in world to load.
fix_cache_reference: If True, throws error if number of obstacles is greater than
cache. If False, creates a larger cache. Note that when using collision checker
inside a recorded cuda graph, recreating the cache will break the graph as the
reference pointer to the cache will change.
"""
# load nvblox mesh
if len(world_model.blox) > 0:
# check if there is a mapper instance:
@@ -109,70 +116,20 @@ class WorldBloxCollision(WorldVoxelCollision):
self._blox_names = names
self.collision_types["blox"] = True
return super().load_collision_model(world_model, fix_cache_reference=fix_cache_reference)
super().load_collision_model(world_model, fix_cache_reference=fix_cache_reference)
def clear_cache(self):
"""Clear obstacle cache, clears nvblox maps and other obstacles."""
self._blox_mapper.clear()
self._blox_mapper.update_hashmaps()
super().clear_cache()
def clear_blox_layer(self, layer_name: str):
"""Clear a specific blox layer."""
index = self._blox_names.index(layer_name)
self._blox_mapper.clear(index)
self._blox_mapper.update_hashmaps()
def _get_blox_sdf(
self,
query_spheres,
collision_query_buffer: CollisionQueryBuffer,
weight,
activation_distance,
return_loss: bool = False,
compute_esdf: bool = False,
):
d = self._blox_mapper.query_sphere_sdf_cost(
query_spheres,
collision_query_buffer.blox_collision_buffer.distance_buffer,
collision_query_buffer.blox_collision_buffer.grad_distance_buffer,
collision_query_buffer.blox_collision_buffer.sparsity_index_buffer,
weight,
activation_distance,
self.max_esdf_distance,
self._blox_tensor_list[0],
self._blox_tensor_list[1],
return_loss,
compute_esdf,
)
return d
def _get_blox_swept_sdf(
self,
query_spheres,
collision_query_buffer,
weight,
activation_distance,
speed_dt,
sweep_steps,
enable_speed_metric,
return_loss=False,
):
d = self._blox_mapper.query_sphere_trajectory_sdf_cost(
query_spheres,
collision_query_buffer.blox_collision_buffer.distance_buffer,
collision_query_buffer.blox_collision_buffer.grad_distance_buffer,
collision_query_buffer.blox_collision_buffer.sparsity_index_buffer,
weight,
activation_distance,
speed_dt,
self._blox_tensor_list[0],
self._blox_tensor_list[1],
sweep_steps,
enable_speed_metric,
return_loss,
use_experimental=False,
)
return d
def get_sphere_distance(
self,
query_sphere: torch.Tensor,
@@ -183,7 +140,31 @@ class WorldBloxCollision(WorldVoxelCollision):
return_loss: bool = False,
sum_collisions: bool = True,
compute_esdf: bool = False,
):
) -> torch.Tensor:
"""Compute the signed distance between query spheres and world obstacles.
This distance can be used as a collision cost for optimization.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Buffer to store collision query results.
weight: Weight of the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
sum_collisions: Sum the collision cost across all obstacles. This variable is currently
not passed to the underlying CUDA kernel as setting this to False caused poor
performance.
compute_esdf: Compute Euclidean signed distance instead of collision cost. When True,
the returned tensor will be the signed distance with positive values inside an
obstacle and negative values outside obstacles.
Returns:
Signed distance between query spheres and world obstacles.
"""
if "blox" not in self.collision_types or not self.collision_types["blox"]:
return super().get_sphere_distance(
query_sphere,
@@ -234,7 +215,21 @@ class WorldBloxCollision(WorldVoxelCollision):
env_query_idx=None,
return_loss: bool = False,
**kwargs,
):
) -> torch.Tensor:
"""Compute binary collision between query spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Weight to scale the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: True is not supported for binary classification. Set to False.
Returns:
Tensor with binary collision results.
"""
if "blox" not in self.collision_types or not self.collision_types["blox"]:
return super().get_sphere_collision(
query_sphere,
@@ -280,7 +275,35 @@ class WorldBloxCollision(WorldVoxelCollision):
env_query_idx: Optional[torch.Tensor] = None,
return_loss: bool = False,
sum_collisions: bool = True,
):
) -> torch.Tensor:
"""Compute the signed distance between trajectory of spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
sum_collisions: Sum the collision cost across all obstacles. This variable is currently
not passed to the underlying CUDA kernel as setting this to False caused poor
performance.
Returns:
Collision cost between trajectory of spheres and world obstacles.
"""
if "blox" not in self.collision_types or not self.collision_types["blox"]:
return super().get_swept_sphere_distance(
query_sphere,
@@ -337,7 +360,30 @@ class WorldBloxCollision(WorldVoxelCollision):
enable_speed_metric=False,
env_query_idx: Optional[torch.Tensor] = None,
return_loss: bool = False,
):
) -> torch.Tensor:
"""Get binary collision between trajectory of spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference. This is not used.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization. This is not used.
env_query_idx: Environment index for each batch of query spheres.
return_loss: This is not supported for binary classification. Set to False.
Returns:
Collision value between trajectory of spheres and world obstacles.
"""
if "blox" not in self.collision_types or not self.collision_types["blox"]:
return super().get_swept_sphere_collision(
query_sphere,
@@ -385,12 +431,25 @@ class WorldBloxCollision(WorldVoxelCollision):
enable: bool = True,
env_idx: int = 0,
):
"""Enable/Disable object in collision checking functions.
Args:
name: Name of the obstacle to enable.
enable: True to enable, False to disable.
env_idx: Index of the environment to enable the obstacle in.
"""
if self._blox_names is not None and name in self._blox_names:
self.enable_blox(enable, name)
else:
super().enable_obstacle(name, enable, env_idx)
def enable_blox(self, enable: bool = True, name: Optional[str] = None):
"""Enable/Disable nvblox layer for collision checking.
Args:
enable: True to enable, False to disable.
name: Name of the nvblox layer to enable.
"""
index = self._blox_names.index(name)
self._blox_tensor_list[1][index] = int(enable)
@@ -400,6 +459,13 @@ class WorldBloxCollision(WorldVoxelCollision):
obj_w_pose: Optional[Pose] = None,
name: Optional[str] = None,
):
"""Update pose of a nvblox layer.
Args:
w_obj_pose: Pose of layer in world frame.
obj_w_pose: Inverse pose of layer. If w_obj_pose is provided, this is not required.
name: Name of the nvblox layer to update.
"""
obj_w_pose = self._get_obstacle_poses(w_obj_pose, obj_w_pose)
index = self._blox_names.index(name)
self._blox_tensor_list[0][index][:7] = obj_w_pose.get_pose_vector()
@@ -409,6 +475,12 @@ class WorldBloxCollision(WorldVoxelCollision):
cuboid: Cuboid,
layer_name: Optional[str] = None,
):
"""Clear occupied regions of a nvblox layer. Not implemented.
Args:
cuboid: Bounding box to clear.
layer_name: Name of nvblox layer.
"""
log_error("Not implemented")
def get_bounding_spheres(
@@ -421,10 +493,34 @@ class WorldBloxCollision(WorldVoxelCollision):
voxelize_method: str = "ray",
pre_transform_pose: Optional[Pose] = None,
clear_region: bool = False,
clear_region_layer: Optional[str] = None,
) -> List[Sphere]:
"""Get spheres within a bounding box.
Args:
bounding_box: Bounding box to find occupied region.
obstacle_name: Name to use for created occupied region. Not useful, use any random
name.
n_spheres: Number of spheres to use for approximating the occupied region.
surface_sphere_radius: Radius to use for surface spheres.
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`.
pre_transform_pose: Optional pose to transform the bounding box before finding spheres.
clear_region: Clear region in nvblox layer. Not supported.
clear_region_layer: Layer of nvblox to clear region.
Returns:
Spheres approximating occupied region.
"""
mesh = self.get_mesh_in_bounding_box(bounding_box, obstacle_name, clear_region=clear_region)
if clear_region:
self.clear_bounding_box(bounding_box, obstacle_name)
self.clear_bounding_box(bounding_box, clear_region_layer)
spheres = mesh.get_bounding_spheres(
n_spheres=n_spheres,
surface_sphere_radius=surface_sphere_radius,
@@ -437,6 +533,12 @@ class WorldBloxCollision(WorldVoxelCollision):
@profiler.record_function("world_blox/add_camera_frame")
def add_camera_frame(self, camera_observation: CameraObservation, layer_name: str):
"""Add a camera image to nvblox layer.
Args:
camera_observation: New image to add to nvblox layer.
layer_name: Name of nvblox layer.
"""
index = self._blox_names.index(layer_name)
pose_mat = camera_observation.pose.get_matrix().view(4, 4)
if camera_observation.rgb_image is not None:
@@ -460,16 +562,29 @@ class WorldBloxCollision(WorldVoxelCollision):
)
def process_camera_frames(self, layer_name: Optional[str] = None, process_aux: bool = False):
"""Integrate ESDF data from camera frames into nvblox layer.
Args:
layer_name: Name of nvblox layer. If None, all layers are processed.
process_aux: Process color frames, useful for visualization.
"""
self.update_blox_esdf(layer_name)
if process_aux:
self.update_blox_mesh(layer_name)
@profiler.record_function("world_blox/update_hashes")
def update_blox_hashes(self):
"""Update hashmaps for nvblox layers. Required after processing camera frames."""
self._blox_mapper.update_hashmaps()
@profiler.record_function("world_blox/update_esdf")
def update_blox_esdf(self, layer_name: Optional[str] = None):
"""Integrate ESDF data from camera frames into nvblox layer.
Args:
layer_name: Name of nvblox layer. If None, all layers are processed.
"""
index = -1
if layer_name is not None:
index = self._blox_names.index(layer_name)
@@ -477,6 +592,11 @@ class WorldBloxCollision(WorldVoxelCollision):
@profiler.record_function("world_blox/update_mesh")
def update_blox_mesh(self, layer_name: Optional[str] = None):
"""Update mesh data for nvblox layer. Requires RGB data.
Args:
layer_name: Name of nvblox layer. If None, all layers are processed.
"""
index = -1
if layer_name is not None:
index = self._blox_names.index(layer_name)
@@ -484,6 +604,17 @@ class WorldBloxCollision(WorldVoxelCollision):
@profiler.record_function("world_blox/get_mesh")
def get_mesh_from_blox_layer(self, layer_name: str, mode: str = "nvblox") -> Mesh:
"""Get Mesh from nvblox layer.
Args:
layer_name: Name of nvblox layer.
mode: If mode is "nvblox", mesh is generated using nvblox's internal mesh construction
method. This relies on RGB data from camera frames. If mode is "voxel", mesh is
generated using occupancy.
Returns:
Mesh object.
"""
index = self._blox_names.index(layer_name)
if mode == "nvblox":
mesh_data = self._blox_mapper.get_mesh(index)
@@ -504,14 +635,133 @@ class WorldBloxCollision(WorldVoxelCollision):
return mesh
def save_layer(self, layer_name: str, file_name: str) -> bool:
"""Save nvblox layer to disk.
Args:
layer_name: Name of nvblox layer.
file_name: File path to save layer.
Returns:
True if successful, False otherwise.
"""
index = self._blox_names.index(layer_name)
status = self._blox_mapper.save_map(file_name, index)
return status
def decay_layer(self, layer_name: str):
"""Decay nvblox layer. This will remove any stale voxels in the layer.
Args:
layer_name: Name of nvblox layer to decay.
"""
index = self._blox_names.index(layer_name)
self._blox_mapper.decay_occupancy(mapper_id=index)
def get_obstacle_names(self, env_idx: int = 0):
def get_obstacle_names(self, env_idx: int = 0) -> List[str]:
"""Get names of all obstacles in the environment.
Args:
env_idx: Environment index to get obstacles from.
Returns:
List of obstacle names.
"""
base_obstacles = super().get_obstacle_names(env_idx)
return self._blox_names + base_obstacles
def _get_blox_sdf(
self,
query_spheres: torch.Tensor,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
return_loss: bool = False,
compute_esdf: bool = False,
) -> torch.Tensor:
"""Compute the signed distance between query spheres and world obstacles.
This distance can be used as a collision cost for optimization.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Buffer to store collision query results.
weight: Weight of the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
compute_esdf: Compute Euclidean signed distance instead of collision cost. When True,
the returned tensor will be the signed distance with positive values inside an
obstacle and negative values outside obstacles.
Returns:
Signed distance between query spheres and world obstacles.
"""
d = self._blox_mapper.query_sphere_sdf_cost(
query_spheres,
collision_query_buffer.blox_collision_buffer.distance_buffer,
collision_query_buffer.blox_collision_buffer.grad_distance_buffer,
collision_query_buffer.blox_collision_buffer.sparsity_index_buffer,
weight,
activation_distance,
self.max_esdf_distance,
self._blox_tensor_list[0],
self._blox_tensor_list[1],
return_loss,
compute_esdf,
)
return d
def _get_blox_swept_sdf(
self,
query_spheres: torch.Tensor,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
speed_dt: torch.Tensor,
sweep_steps: int,
enable_speed_metric: bool,
return_loss: bool = False,
) -> torch.Tensor:
"""Compute the signed distance between trajectory of spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
Returns:
Collision cost between trajectory of spheres and world obstacles.
"""
d = self._blox_mapper.query_sphere_trajectory_sdf_cost(
query_spheres,
collision_query_buffer.blox_collision_buffer.distance_buffer,
collision_query_buffer.blox_collision_buffer.grad_distance_buffer,
collision_query_buffer.blox_collision_buffer.sparsity_index_buffer,
weight,
activation_distance,
speed_dt,
self._blox_tensor_list[0],
self._blox_tensor_list[1],
sweep_steps,
enable_speed_metric,
return_loss,
use_experimental=False,
)
return d

View File

@@ -8,6 +8,7 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""World represented as Meshes can be used with this module for collision checking."""
# Standard Library
from dataclasses import dataclass
@@ -33,22 +34,29 @@ from curobo.util.warp import init_warp
@dataclass(frozen=True)
class WarpMeshData:
"""Data helper to use with warp for representing a mesh"""
#: Name of the mesh.
name: str
#: Mesh ID, created by warp once mesh is loaded to device.
m_id: int
#: Vertices of the mesh.
vertices: wp.array
#: Faces of the mesh.
faces: wp.array
#: Warp mesh instance.
mesh: wp.Mesh
class WorldMeshCollision(WorldPrimitiveCollision):
"""World Mesh Collision using Nvidia's warp library
This currently requires passing int64 array from torch to warp which is only
available when compiled from source.
"""
"""World Mesh Collision using Nvidia's warp library."""
def __init__(self, config: WorldCollisionConfig):
# WorldCollision.(self)
"""Initialize World Mesh Collision with given configuration."""
init_warp()
self.tensor_args = config.tensor_args
@@ -62,6 +70,7 @@ class WorldMeshCollision(WorldPrimitiveCollision):
super().__init__(config)
def _init_cache(self):
"""Initialize cache for storing meshes."""
if (
self.cache is not None
and "mesh" in self.cache
@@ -78,6 +87,17 @@ class WorldMeshCollision(WorldPrimitiveCollision):
load_obb_obs: bool = True,
fix_cache_reference: bool = False,
):
"""Load world obstacles into collision checker.
Args:
world_model: Obstacles to load.
env_idx: Environment index to load obstacles into.
load_obb_obs: Load OBB (cuboid) obstacles.
fix_cache_reference: If True, throws error if number of obstacles is greater than
cache. If False, creates a larger cache. Note that when using collision checker
inside a recorded cuda graph, recreating the cache will break the graph as the
reference pointer to the cache will change.
"""
max_nmesh = len(world_model.mesh)
if max_nmesh > 0:
if self._mesh_tensor_list is None or self._mesh_tensor_list[0].shape[1] < max_nmesh:
@@ -103,6 +123,11 @@ class WorldMeshCollision(WorldPrimitiveCollision):
self.world_model = world_model
def load_batch_collision_model(self, world_config_list: List[WorldConfig]):
"""Load multiple world obstacles into collision checker across environments.
Args:
world_config_list: List of obstacles to load.
"""
max_nmesh = max([len(x.mesh) for x in world_config_list])
if self._mesh_tensor_list is None or self._mesh_tensor_list[0].shape[1] < max_nmesh:
log_warn("Creating new Mesh cache: " + str(max_nmesh))
@@ -112,7 +137,15 @@ class WorldMeshCollision(WorldPrimitiveCollision):
self.load_collision_model(world_model, env_idx=env_idx, load_obb_obs=False)
super().load_batch_collision_model(world_config_list)
def _load_mesh_to_warp(self, mesh: Mesh):
def _load_mesh_to_warp(self, mesh: Mesh) -> WarpMeshData:
"""Load cuRobo mesh into warp.
Args:
mesh: mesh instance to load.
Returns:
loaded mesh data.
"""
verts, faces = mesh.get_mesh_data()
v = wp.array(verts, dtype=wp.vec3, device=self._wp_device)
f = wp.array(np.ravel(faces), dtype=int, device=self._wp_device)
@@ -120,7 +153,14 @@ class WorldMeshCollision(WorldPrimitiveCollision):
return WarpMeshData(mesh.name, new_mesh.id, v, f, new_mesh)
def _load_mesh_into_cache(self, mesh: Mesh) -> WarpMeshData:
#
"""Load cuRobo mesh into cache.
Args:
mesh: mesh instance to load.
Returns:
loaded mesh data.
"""
if mesh.name not in self._wp_mesh_cache:
# load mesh into cache:
self._wp_mesh_cache[mesh.name] = self._load_mesh_to_warp(mesh)
@@ -129,7 +169,15 @@ class WorldMeshCollision(WorldPrimitiveCollision):
log_warn("Object already in warp cache, using existing instance for: " + mesh.name)
return self._wp_mesh_cache[mesh.name]
def _load_batch_mesh_to_warp(self, mesh_list: List[Mesh]):
def _load_batch_mesh_to_warp(self, mesh_list: List[Mesh]) -> List:
"""Load multiple meshes into warp.
Args:
mesh_list: List of meshes to load.
Returns:
List of mesh names, mesh ids, and inverse poses.
"""
# First load all verts and faces:
name_list = []
pose_list = []
@@ -145,6 +193,12 @@ class WorldMeshCollision(WorldPrimitiveCollision):
return name_list, id_list, inv_pose_buffer.get_pose_vector()
def add_mesh(self, new_mesh: Mesh, env_idx: int = 0):
"""Add a mesh to the world.
Args:
new_mesh: Mesh to add.
env_idx: Environment index to add mesh to.
"""
if self._env_n_mesh[env_idx] >= self._mesh_tensor_list[0].shape[1]:
log_error(
"Cannot add new mesh as we are at mesh cache limit, increase cache limit in WorldMeshCollision"
@@ -169,11 +223,32 @@ class WorldMeshCollision(WorldPrimitiveCollision):
name: str,
env_idx: int = 0,
) -> int:
"""Get index of mesh with given name.
Args:
name: Name of the mesh.
env_idx: Environment index to search in.
Returns:
Mesh index.
"""
if name not in self._env_mesh_names[env_idx]:
log_error("Obstacle with name: " + name + " not found in current world", exc_info=True)
return self._env_mesh_names[env_idx].index(name)
def create_collision_cache(self, mesh_cache=None, obb_cache=None, n_envs=None):
def create_collision_cache(
self,
mesh_cache: Optional[int] = None,
obb_cache: Optional[int] = None,
n_envs: Optional[int] = None,
):
"""Create cache of obstacles.
Args:
mesh_cache: Number of mesh obstacles to cache.
obb_cache: Number of OBB (cuboid) obstacles to cache.
n_envs: Number of environments to cache.
"""
if n_envs is not None:
self.n_envs = n_envs
if mesh_cache is not None:
@@ -181,7 +256,12 @@ class WorldMeshCollision(WorldPrimitiveCollision):
if obb_cache is not None:
self._create_obb_cache(obb_cache)
def _create_mesh_cache(self, mesh_cache):
def _create_mesh_cache(self, mesh_cache: int):
"""Create cache for mesh obstacles.
Args:
mesh_cache: Number of mesh obstacles to cache.
"""
# create cache to store meshes, mesh poses and inverse poses
self._env_n_mesh = torch.zeros(
@@ -199,9 +279,7 @@ class WorldMeshCollision(WorldPrimitiveCollision):
obs_ids = torch.zeros(
(self.n_envs, mesh_cache), device=self.tensor_args.device, dtype=torch.int64
)
# v_empty = [[None for _ in range(mesh_cache)] for _ in range(self.n_envs)]
# @f_empty = [[None for _ in range(mesh_cache)] for _ in range(self.n_envs)]
# wp_m_empty = [[None for _ in range(mesh_cache)] for _ in range(self.n_envs)]
# warp requires uint64 for mesh indices, supports conversion from int64 to uint64
self._mesh_tensor_list = [
obs_ids,
@@ -221,6 +299,15 @@ class WorldMeshCollision(WorldPrimitiveCollision):
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update pose of mesh.
Args:
w_obj_pose: Pose of the mesh in world frame.
obj_w_pose: Inverse pose of the mesh.
name: Name of mesh to update.
env_obj_idx: Index of mesh in environment. If name is given, this is ignored.
env_idx: Environment index to update mesh in.
"""
w_inv_pose = self._get_obstacle_poses(w_obj_pose, obj_w_pose)
if name is not None:
@@ -229,53 +316,7 @@ class WorldMeshCollision(WorldPrimitiveCollision):
elif env_obj_idx is not None:
self._mesh_tensor_list[1][env_idx, env_obj_idx, :7] = w_inv_pose.get_pose_vector()
else:
raise ValueError("name or env_obj_idx needs to be given to update mesh pose")
def update_all_mesh_pose(
self,
w_obj_pose: Optional[Pose] = None,
obj_w_pose: Optional[Pose] = None,
name: Optional[List[str]] = None,
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update poses for a list of meshes in the same environment
Args:
w_obj_pose (Optional[Pose], optional): _description_. Defaults to None.
obj_w_pose (Optional[Pose], optional): _description_. Defaults to None.
name (Optional[List[str]], optional): _description_. Defaults to None.
env_obj_idx (Optional[torch.Tensor], optional): _description_. Defaults to None.
env_idx (int, optional): _description_. Defaults to 0.
"""
w_inv_pose = self._get_obstacle_poses(w_obj_pose, obj_w_pose)
raise NotImplementedError
def update_mesh_pose_env(
self,
w_obj_pose: Optional[Pose] = None,
obj_w_pose: Optional[Pose] = None,
name: Optional[str] = None,
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: List[int] = [0],
):
"""Update pose of a single object in a list of environments
Args:
w_obj_pose (Optional[Pose], optional): _description_. Defaults to None.
obj_w_pose (Optional[Pose], optional): _description_. Defaults to None.
name (Optional[List[str]], optional): _description_. Defaults to None.
env_obj_idx (Optional[torch.Tensor], optional): _description_. Defaults to None.
env_idx (List[int], optional): _description_. Defaults to [0].
"""
w_inv_pose = self._get_obstacle_poses(w_obj_pose, obj_w_pose)
# collect index of mesh across environments:
# index_tensor = torch.zeros((1, len(env_idx)), dtype=torch.long, device=self.tensor_args.device)
# for i, e in enumerate[env_idx]:
# index_tensor[0,i] = self.get_mesh_idx(name, e)
raise NotImplementedError
# self._mesh_tensor_list[1][env_idx, obj_idx]
log_error("name or env_obj_idx needs to be given to update mesh pose")
def update_mesh_from_warp(
self,
@@ -286,6 +327,16 @@ class WorldMeshCollision(WorldPrimitiveCollision):
env_idx: int = 0,
name: Optional[str] = None,
):
"""Update mesh information in world cache.
Args:
warp_mesh_idx: New warp mesh index.
w_obj_pose: Pose of the mesh in world frame.
obj_w_pose: Invserse pose of the mesh. Not required if w_obj_pose is given.
obj_idx: Index of mesh in environment. If name is given, this is ignored.
env_idx: Environment index to update mesh in.
name: Name of mesh to update.
"""
if name is not None:
obj_idx = self.get_mesh_idx(name, env_idx)
@@ -305,13 +356,28 @@ class WorldMeshCollision(WorldPrimitiveCollision):
name: str,
w_obj_pose: Pose,
env_idx: int = 0,
update_cpu_reference: bool = False,
):
"""Update pose of obstacle.
Args:
name: Name of the obstacle.
w_obj_pose: Pose of obstacle in world frame.
env_idx: Environment index to update obstacle in.
update_cpu_reference: If True, updates the CPU reference with the new pose. This is
useful for debugging and visualization. Only supported for env_idx=0.
"""
if self._env_mesh_names is not None and name in self._env_mesh_names[env_idx]:
self.update_mesh_pose(name=name, w_obj_pose=w_obj_pose, env_idx=env_idx)
elif self._env_obbs_names is not None and name in self._env_obbs_names[env_idx]:
self.update_obb_pose(name=name, w_obj_pose=w_obj_pose, env_idx=env_idx)
if update_cpu_reference:
self.update_obstacle_pose_in_world_model(name, w_obj_pose, env_idx)
else:
log_error("obstacle not found in OBB world model: " + name)
super().update_obstacle_pose(
name=name,
w_obj_pose=w_obj_pose,
env_idx=env_idx,
update_cpu_reference=update_cpu_reference,
)
def enable_obstacle(
self,
@@ -319,6 +385,13 @@ class WorldMeshCollision(WorldPrimitiveCollision):
enable: bool = True,
env_idx: int = 0,
):
"""Enable/Disable object in collision checking functions.
Args:
name: Name of the obstacle to enable.
enable: True to enable, False to disable.
env_idx: Index of the environment to enable the obstacle in.
"""
if self._env_mesh_names is not None and name in self._env_mesh_names[env_idx]:
self.enable_mesh(enable, name, None, env_idx)
elif self._env_obbs_names is not None and name in self._env_obbs_names[env_idx]:
@@ -327,7 +400,15 @@ class WorldMeshCollision(WorldPrimitiveCollision):
log_error("Obstacle not found in world model: " + name)
self.world_model.objects
def get_obstacle_names(self, env_idx: int = 0):
def get_obstacle_names(self, env_idx: int = 0) -> List[str]:
"""Get names of all obstacles in the environment.
Args:
env_idx: Environment index to get obstacles from.
Returns:
List of obstacle names.
"""
base_obstacles = super().get_obstacle_names(env_idx)
return self._env_mesh_names[env_idx] + base_obstacles
@@ -338,12 +419,13 @@ class WorldMeshCollision(WorldPrimitiveCollision):
env_mesh_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update obstacle dimensions
"""Enable/Disable mesh in collision checking functions.
Args:
obj_dims (torch.Tensor): [dim.x,dim.y, dim.z], give as [b,3]
obj_idx (torch.Tensor or int):
enable: True to enable, False to disable.
name: Name of the mesh to enable.
env_mesh_idx: Index of the mesh in environment. If name is given, this is ignored.
env_idx: Environment index to enable the mesh in.
"""
if env_mesh_idx is not None:
self._mesh_tensor_list[2][env_mesh_idx] = int(enable) # enable == 1
@@ -352,66 +434,6 @@ class WorldMeshCollision(WorldPrimitiveCollision):
obs_idx = self.get_mesh_idx(name, env_idx)
self._mesh_tensor_list[2][env_idx, obs_idx] = int(enable)
def _get_sdf(
self,
query_spheres,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
env_query_idx=None,
return_loss=False,
compute_esdf=False,
):
d = SdfMeshWarpPy.apply(
query_spheres,
collision_query_buffer.mesh_collision_buffer.distance_buffer,
collision_query_buffer.mesh_collision_buffer.grad_distance_buffer,
collision_query_buffer.mesh_collision_buffer.sparsity_index_buffer,
weight,
activation_distance,
self._mesh_tensor_list[0],
self._mesh_tensor_list[1],
self._mesh_tensor_list[2],
self._env_n_mesh,
self.max_distance,
env_query_idx,
return_loss,
compute_esdf,
)
return d
def _get_swept_sdf(
self,
query_spheres,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
speed_dt: torch.Tensor,
sweep_steps: int,
enable_speed_metric=False,
env_query_idx=None,
return_loss: bool = False,
):
d = SweptSdfMeshWarpPy.apply(
query_spheres,
collision_query_buffer.mesh_collision_buffer.distance_buffer,
collision_query_buffer.mesh_collision_buffer.grad_distance_buffer,
collision_query_buffer.mesh_collision_buffer.sparsity_index_buffer,
weight,
activation_distance,
speed_dt,
self._mesh_tensor_list[0],
self._mesh_tensor_list[1],
self._mesh_tensor_list[2],
self._env_n_mesh,
self.max_distance,
sweep_steps,
enable_speed_metric,
env_query_idx,
return_loss,
)
return d
def get_sphere_distance(
self,
query_sphere: torch.Tensor,
@@ -423,7 +445,30 @@ class WorldMeshCollision(WorldPrimitiveCollision):
sum_collisions: bool = True,
compute_esdf: bool = False,
):
# TODO: if no mesh object exist, call primitive
"""Compute the signed distance between query spheres and world obstacles.
This distance can be used as a collision cost for optimization.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Buffer to store collision query results.
weight: Weight of the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
sum_collisions: Sum the collision cost across all obstacles. This variable is currently
not passed to the underlying CUDA kernel as setting this to False caused poor
performance.
compute_esdf: Compute Euclidean signed distance instead of collision cost. When True,
the returned tensor will be the signed distance with positive values inside an
obstacle and negative values outside obstacles.
Returns:
Signed distance between query spheres and world obstacles.
"""
if "mesh" not in self.collision_types or not self.collision_types["mesh"]:
return super().get_sphere_distance(
query_sphere,
@@ -474,6 +519,20 @@ class WorldMeshCollision(WorldPrimitiveCollision):
return_loss=False,
**kwargs,
):
"""Compute binary collision between query spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Weight to scale the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: True is not supported for binary classification. Set to False.
Returns:
Tensor with binary collision results.
"""
if "mesh" not in self.collision_types or not self.collision_types["mesh"]:
return super().get_sphere_collision(
query_sphere,
@@ -521,6 +580,34 @@ class WorldMeshCollision(WorldPrimitiveCollision):
return_loss: bool = False,
sum_collisions: bool = True,
):
"""Compute the signed distance between trajectory of spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
sum_collisions: Sum the collision cost across all obstacles. This variable is currently
not passed to the underlying CUDA kernel as setting this to False caused poor
performance.
Returns:
Collision cost between trajectory of spheres and world obstacles.
"""
# log_warn("Swept: Mesh + Primitive Collision Checking is experimental")
if "mesh" not in self.collision_types or not self.collision_types["mesh"]:
return super().get_swept_sphere_distance(
@@ -578,6 +665,29 @@ class WorldMeshCollision(WorldPrimitiveCollision):
env_query_idx: Optional[torch.Tensor] = None,
return_loss: bool = False,
):
"""Get binary collision between trajectory of spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference. This is not used.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization. This is not used.
env_query_idx: Environment index for each batch of query spheres.
return_loss: This is not supported for binary classification. Set to False.
Returns:
Collision value between trajectory of spheres and world obstacles.
"""
if "mesh" not in self.collision_types or not self.collision_types["mesh"]:
return super().get_swept_sphere_collision(
query_sphere,
@@ -620,6 +730,7 @@ class WorldMeshCollision(WorldPrimitiveCollision):
return d_val
def clear_cache(self):
"""Delete all cuboid and mesh obstacles from the world."""
self._wp_mesh_cache = {}
if self._mesh_tensor_list is not None:
self._mesh_tensor_list[2][:] = 0
@@ -631,3 +742,107 @@ class WorldMeshCollision(WorldPrimitiveCollision):
]
super().clear_cache()
def _get_sdf(
self,
query_spheres: torch.Tensor,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
env_query_idx: Optional[torch.Tensor] = None,
return_loss: bool = False,
compute_esdf: bool = False,
) -> torch.Tensor:
"""Compute signed distance for mesh obstacles using warp kernel.
Args:
query_spheres: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Weight to scale the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
compute_esdf: Compute Euclidean signed distance instead of collision cost. When True,
the returned tensor will be the signed distance with positive values inside an
obstacle and negative values outside obstacles.
Returns:
Collision cost between query spheres and world obstacles.
"""
d = SdfMeshWarpPy.apply(
query_spheres,
collision_query_buffer.mesh_collision_buffer.distance_buffer,
collision_query_buffer.mesh_collision_buffer.grad_distance_buffer,
collision_query_buffer.mesh_collision_buffer.sparsity_index_buffer,
weight,
activation_distance,
self._mesh_tensor_list[0],
self._mesh_tensor_list[1],
self._mesh_tensor_list[2],
self._env_n_mesh,
self.max_distance,
env_query_idx,
return_loss,
compute_esdf,
)
return d
def _get_swept_sdf(
self,
query_spheres: torch.Tensor,
collision_query_buffer: CollisionQueryBuffer,
weight: torch.Tensor,
activation_distance: torch.Tensor,
speed_dt: torch.Tensor,
sweep_steps: int,
enable_speed_metric=False,
env_query_idx=None,
return_loss: bool = False,
) -> torch.Tensor:
"""Compute signed distance for mesh obstacles using warp kernel.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
Returns:
Collision cost between trajectory of spheres and world obstacles.
"""
d = SweptSdfMeshWarpPy.apply(
query_spheres,
collision_query_buffer.mesh_collision_buffer.distance_buffer,
collision_query_buffer.mesh_collision_buffer.grad_distance_buffer,
collision_query_buffer.mesh_collision_buffer.sparsity_index_buffer,
weight,
activation_distance,
speed_dt,
self._mesh_tensor_list[0],
self._mesh_tensor_list[1],
self._mesh_tensor_list[2],
self._env_n_mesh,
self.max_distance,
sweep_steps,
enable_speed_metric,
env_query_idx,
return_loss,
)
return d

View File

@@ -8,6 +8,8 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""World represented by euclidean signed distance grids."""
# Standard Library
import math
from typing import Any, Dict, List, Optional
@@ -29,6 +31,7 @@ class WorldVoxelCollision(WorldMeshCollision):
"""Voxel grid representation of World, with each voxel containing Euclidean Signed Distance."""
def __init__(self, config: WorldCollisionConfig):
"""Initialize with a world collision configuration."""
self._env_n_voxels = None
self._voxel_tensor_list = None
self._env_voxel_names = None
@@ -36,6 +39,7 @@ class WorldVoxelCollision(WorldMeshCollision):
super().__init__(config)
def _init_cache(self):
"""Initialize the cache for the world."""
if (
self.cache is not None
and "voxel" in self.cache
@@ -45,6 +49,14 @@ class WorldVoxelCollision(WorldMeshCollision):
return super()._init_cache()
def _create_voxel_cache(self, voxel_cache: Dict[str, Any]):
"""Create a cache for voxel grid representation of the world.
Args:
voxel_cache: Parameters for the voxel grid representation. The dictionary should
contain the following keys: layers, dims, voxel_size, feature_dtype. Current
implementation assumes that all voxel grids have the same number of voxels. Though
different layers can have different resolutions, this is not yet thoroughly tested.
"""
n_layers = voxel_cache["layers"]
dims = voxel_cache["dims"]
voxel_size = voxel_cache["voxel_size"]
@@ -93,24 +105,35 @@ class WorldVoxelCollision(WorldMeshCollision):
def load_collision_model(
self, world_model: WorldConfig, env_idx=0, fix_cache_reference: bool = False
):
"""Load collision representation from world obstacles.
Args:
world_model: Obstacles in world to load.
env_idx: Environment index to load obstacles into.
fix_cache_reference: If True, throws error if number of obstacles is greater than
cache. If False, creates a larger cache. Note that when using collision checker
inside a recorded cuda graph, recreating the cache will break the graph as the
reference pointer to the cache will change.
"""
self._load_voxel_collision_model_in_cache(
world_model, env_idx, fix_cache_reference=fix_cache_reference
)
return super().load_collision_model(
super().load_collision_model(
world_model, env_idx=env_idx, fix_cache_reference=fix_cache_reference
)
def _load_voxel_collision_model_in_cache(
self, world_config: WorldConfig, env_idx: int = 0, fix_cache_reference: bool = False
):
"""TODO:
_extended_summary_
"""Load voxel grid representation of the world into the cache.
Args:
world_config: _description_
env_idx: _description_
fix_cache_reference: _description_
world_config: Obstacles in world to load.
env_idx: Environment index to load obstacles into.
fix_cache_reference: If True, throws error if number of obstacles is greater than
cache. If False, creates a larger cache. Note that when using collision checker
inside a recorded cuda graph, recreating the cache will break the graph as the
reference pointer to the cache will change.
"""
voxel_objs = world_config.voxel
max_obs = len(voxel_objs)
@@ -153,7 +176,17 @@ class WorldVoxelCollision(WorldMeshCollision):
def _batch_tensor_voxel(
self, pose: List[List[float]], dims: List[float], voxel_size: List[float]
):
) -> List[torch.Tensor]:
"""Create a list of tensors that represent the voxel parameters.
Args:
pose: Pose of voxel grids.
dims: Dimensions of voxel grids.
voxel_size: Resolution of voxel grids.
Returns:
List of tensors representing the voxel parameters.
"""
w_T_b = Pose.from_batch_list(pose, tensor_args=self.tensor_args)
b_T_w = w_T_b.inverse()
dims_t = torch.as_tensor(
@@ -170,13 +203,8 @@ class WorldVoxelCollision(WorldMeshCollision):
def load_batch_collision_model(self, world_config_list: List[WorldConfig]):
"""Load voxel grid for batched environments
_extended_summary_
Args:
world_config_list: _description_
Returns:
_description_
world_config_list: List of world obstacles for each environment.
"""
log_error("Not Implemented")
# First find largest number of cuboid:
@@ -244,7 +272,7 @@ class WorldVoxelCollision(WorldMeshCollision):
)
self.collision_types["voxel"] = True
return super().load_batch_collision_model(world_config_list)
super().load_batch_collision_model(world_config_list)
def enable_obstacle(
self,
@@ -252,12 +280,27 @@ class WorldVoxelCollision(WorldMeshCollision):
enable: bool = True,
env_idx: int = 0,
):
"""Enable/Disable object in collision checking functions.
Args:
name: Name of the obstacle to enable.
enable: True to enable, False to disable.
env_idx: Index of the environment to enable the obstacle in.
"""
if self._env_voxel_names is not None and name in self._env_voxel_names[env_idx]:
self.enable_voxel(enable, name, None, env_idx)
else:
return super().enable_obstacle(name, enable, env_idx)
def get_obstacle_names(self, env_idx: int = 0):
def get_obstacle_names(self, env_idx: int = 0) -> List[str]:
"""Get names of all obstacles in the environment.
Args:
env_idx: Environment index to get obstacles from.
Returns:
List of obstacle names.
"""
base_obstacles = super().get_obstacle_names(env_idx)
return self._env_voxel_names[env_idx] + base_obstacles
@@ -268,12 +311,13 @@ class WorldVoxelCollision(WorldMeshCollision):
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update obstacle dimensions
"""Enable/Disable voxel grid in collision checking functions.
Args:
obj_dims (torch.Tensor): [dim.x,dim.y, dim.z], give as [b,3]
obj_idx (torch.Tensor or int):
enable: True to enable, False to disable.
name: Name of voxel grid to enable.
env_obj_idx: Index of voxel grid. If name is provided, this is ignored.
env_idx: Environment index to enable the voxel grid in.
"""
if env_obj_idx is not None:
self._voxel_tensor_list[2][env_obj_idx] = int(enable) # enable == 1
@@ -288,13 +332,31 @@ class WorldVoxelCollision(WorldMeshCollision):
name: str,
w_obj_pose: Pose,
env_idx: int = 0,
update_cpu_reference: bool = False,
):
"""Update pose of obstacle.
Args:
name: Name of the obstacle.
w_obj_pose: Pose of obstacle in world frame.
env_idx: Environment index to update obstacle in.
update_cpu_reference: If True, updates the CPU reference with the new pose. This is
useful for debugging and visualization. Only supported for env_idx=0.
"""
if self._env_voxel_names is not None and name in self._env_voxel_names[env_idx]:
self.update_voxel_pose(name=name, w_obj_pose=w_obj_pose, env_idx=env_idx)
if update_cpu_reference:
self.update_obstacle_pose_in_world_model(name, w_obj_pose, env_idx)
else:
log_error("obstacle not found in OBB world model: " + name)
super().update_obstacle_pose(name, w_obj_pose, env_idx, update_cpu_reference)
def update_voxel_data(self, new_voxel: VoxelGrid, env_idx: int = 0):
"""Update parameters of a voxel grid. This can also updates signed distance values.
Args:
new_voxel: New parameters.
env_idx: Environment index to update voxel grid in.
"""
obs_idx = self.get_voxel_idx(new_voxel.name, env_idx)
self._voxel_tensor_list[3][env_idx, obs_idx, :, :] = new_voxel.feature_tensor.view(
new_voxel.feature_tensor.shape[0], -1
@@ -315,12 +377,15 @@ class WorldVoxelCollision(WorldMeshCollision):
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update pose of a specific objects.
This also updates the signed distance grid to account for the updated object pose.
"""Update signed distance values in a voxel grid.
Args:
obj_w_pose: Pose
obj_idx:
features: New signed distance values.
name: Name of voxel grid obstacle.
env_obj_idx: Index of voxel grid. If name is provided, this is ignored.
env_idx: Environment index to update voxel grid in.
"""
if env_obj_idx is not None:
self._voxel_tensor_list[3][env_obj_idx, :] = features.to(
dtype=self._voxel_tensor_list[3].dtype
@@ -339,11 +404,14 @@ class WorldVoxelCollision(WorldMeshCollision):
env_obj_idx: Optional[torch.Tensor] = None,
env_idx: int = 0,
):
"""Update pose of a specific objects.
This also updates the signed distance grid to account for the updated object pose.
"""Update pose of voxel grid.
Args:
obj_w_pose: Pose
obj_idx:
w_obj_pose: Pose of voxel grid in world frame.
obj_w_pose: Inverse pose of voxel grid. If provided, w_obj_pose is ignored.
name: Name of the voxel grid.
env_obj_idx: Index of voxel grid. If name is provided, this is ignored.
env_idx: Environment index to update voxel grid in.
"""
obj_w_pose = self._get_obstacle_poses(w_obj_pose, obj_w_pose)
if env_obj_idx is not None:
@@ -357,6 +425,15 @@ class WorldVoxelCollision(WorldMeshCollision):
name: str,
env_idx: int = 0,
) -> int:
"""Get index of voxel grid in the environment.
Args:
name: Name of the voxel grid.
env_idx: Environment index to get voxel grid from.
Returns:
Index of voxel grid.
"""
if name not in self._env_voxel_names[env_idx]:
log_error("Obstacle with name: " + name + " not found in current world", exc_info=True)
return self._env_voxel_names[env_idx].index(name)
@@ -365,7 +442,16 @@ class WorldVoxelCollision(WorldMeshCollision):
self,
name: str,
env_idx: int = 0,
):
) -> VoxelGrid:
"""Get voxel grid from world obstacles.
Args:
name: Name of voxel grid.
env_idx: Environment index to get voxel grid from.
Returns:
Voxel grid object.
"""
obs_idx = self.get_voxel_idx(name, env_idx)
voxel_params = np.round(
self._voxel_tensor_list[0][env_idx, obs_idx, :].cpu().numpy().astype(np.float64), 6
@@ -394,7 +480,31 @@ class WorldVoxelCollision(WorldMeshCollision):
return_loss=False,
sum_collisions: bool = True,
compute_esdf: bool = False,
):
) -> torch.Tensor:
"""Compute the signed distance between query spheres and world obstacles.
This distance can be used as a collision cost for optimization.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Buffer to store collision query results.
weight: Weight of the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
sum_collisions: Sum the collision cost across all obstacles. This variable is currently
not passed to the underlying CUDA kernel as setting this to False caused poor
performance.
compute_esdf: Compute Euclidean signed distance instead of collision cost. When True,
the returned tensor will be the signed distance with positive values inside an
obstacle and negative values outside obstacles.
Returns:
Signed distance between query spheres and world obstacles.
"""
if "voxel" not in self.collision_types or not self.collision_types["voxel"]:
return super().get_sphere_distance(
query_sphere,
@@ -469,7 +579,21 @@ class WorldVoxelCollision(WorldMeshCollision):
env_query_idx: Optional[torch.Tensor] = None,
return_loss=False,
**kwargs,
):
) -> torch.Tensor:
"""Compute binary collision between query spheres and world obstacles.
Args:
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Weight to scale the collision cost.
activation_distance: Distance outside the object to start computing the cost.
env_query_idx: Environment index for each batch of query spheres.
return_loss: True is not supported for binary classification. Set to False.
Returns:
Tensor with binary collision results.
"""
if "voxel" not in self.collision_types or not self.collision_types["voxel"]:
return super().get_sphere_collision(
query_sphere,
@@ -481,7 +605,7 @@ class WorldVoxelCollision(WorldMeshCollision):
)
if return_loss:
raise ValueError("cannot return loss for classification, use get_sphere_distance")
log_error("cannot return loss for classification, use get_sphere_distance")
b, h, n, _ = query_sphere.shape
use_batch_env = True
env_query_idx_voxel = env_query_idx
@@ -541,11 +665,34 @@ class WorldVoxelCollision(WorldMeshCollision):
env_query_idx: Optional[torch.Tensor] = None,
return_loss=False,
sum_collisions: bool = True,
):
"""
Computes the signed distance via analytic function
) -> torch.Tensor:
"""Compute the signed distance between trajectory of spheres and world obstacles.
Args:
tensor_sphere: b, n, 4
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization.
env_query_idx: Environment index for each batch of query spheres.
return_loss: If the returned tensor will be scaled or changed before calling backward,
set this to True. If the returned tensor will be used directly through addition,
set this to False.
sum_collisions: Sum the collision cost across all obstacles. This variable is currently
not passed to the underlying CUDA kernel as setting this to False caused poor
performance.
Returns:
Collision cost between trajectory of spheres and world obstacles.
"""
if "voxel" not in self.collision_types or not self.collision_types["voxel"]:
return super().get_swept_sphere_distance(
@@ -625,11 +772,29 @@ class WorldVoxelCollision(WorldMeshCollision):
enable_speed_metric=False,
env_query_idx: Optional[torch.Tensor] = None,
return_loss=False,
):
"""
Computes the signed distance via analytic function
) -> torch.Tensor:
"""Get binary collision between trajectory of spheres and world obstacles.
Args:
tensor_sphere: b, n, 4
query_sphere: Input tensor with query spheres [batch, horizon, number of spheres, 4].
With [x, y, z, radius] as the last column for each sphere.
collision_query_buffer: Collision query buffer to store the results.
weight: Collision cost weight.
activation_distance: Distance outside the object to start computing the cost. A smooth
scaling is applied to the cost starting from this distance. See
:ref:`research_page` for more details.
speed_dt: Length of time (seconds) to use when calculating the speed of the sphere
using finite difference. This is not used.
sweep_steps: Number of steps to sweep the sphere along the trajectory. More steps will
allow for catching small obstacles, taking more time to compute.
enable_speed_metric: True will scale the collision cost by the speed of the sphere.
This has the effect of slowing down the robot when near obstacles. This also has
shown to improve convergence from poor initialization. This is not used.
env_query_idx: Environment index for each batch of query spheres.
return_loss: This is not supported for binary classification. Set to False.
Returns:
Collision value between trajectory of spheres and world obstacles.
"""
if "voxel" not in self.collision_types or not self.collision_types["voxel"]:
return super().get_swept_sphere_collision(
@@ -644,7 +809,7 @@ class WorldVoxelCollision(WorldMeshCollision):
return_loss=return_loss,
)
if return_loss:
raise ValueError("cannot return loss for classify, use get_swept_sphere_distance")
log_error("cannot return loss for classify, use get_swept_sphere_distance")
b, h, n, _ = query_sphere.shape
use_batch_env = True
@@ -698,6 +863,7 @@ class WorldVoxelCollision(WorldMeshCollision):
return d_val
def clear_cache(self):
"""Clear obstacles in world cache."""
if self._voxel_tensor_list is not None:
self._voxel_tensor_list[2][:] = 0
if self._voxel_tensor_list[3].dtype in [torch.float32, torch.float16, torch.bfloat16]:
@@ -710,5 +876,14 @@ class WorldVoxelCollision(WorldMeshCollision):
self._env_n_voxels[:] = 0
super().clear_cache()
def get_voxel_grid_shape(self, env_idx: int = 0, obs_idx: int = 0):
def get_voxel_grid_shape(self, env_idx: int = 0, obs_idx: int = 0) -> torch.Size:
"""Get dimensions of the voxel grid.
Args:
env_idx: Environment index.
obs_idx: Obstacle index.
Returns:
Shape of the voxel grid.
"""
return self._voxel_tensor_list[3][env_idx, obs_idx].shape

View File

@@ -8,12 +8,14 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""Approximate mesh geometry with spheres."""
# Standard Library
from enum import Enum
from typing import List, Tuple
from typing import List, Tuple, Union
# Third Party
import numpy
import numpy as np
import torch
import trimesh
@@ -42,13 +44,37 @@ class SphereFitType(Enum):
VOXEL_VOLUME_SAMPLE_SURFACE = "voxel_volume_sample_surface"
def sample_even_fit_mesh(mesh: trimesh.Trimesh, n_spheres: int, sphere_radius: float):
def sample_even_fit_mesh(
mesh: trimesh.Trimesh,
n_spheres: int,
sphere_radius: float,
) -> Tuple[numpy.array, List[float]]:
"""Sample even points on the surface of the mesh and return them with the given radius.
Args:
mesh: Mesh to sample points from.
n_spheres: Number of spheres to sample.
sphere_radius: Sphere radius.
Returns:
Tuple of points and radius.
"""
n_pts = trimesh.sample.sample_surface_even(mesh, n_spheres)[0]
n_radius = [sphere_radius for _ in range(len(n_pts))]
return n_pts, n_radius
def get_voxel_pitch(mesh: trimesh.Trimesh, n_cubes: int):
def get_voxel_pitch(mesh: trimesh.Trimesh, n_cubes: int) -> float:
"""Get the pitch of the voxel grid based on the mesh and number of cubes.
Args:
mesh: Mesh to get the pitch from.
n_cubes: Number of voxels to fit.
Returns:
float: Pitch of the voxel grid.
"""
d = mesh.extents
cube_volume = d[0] * d[1] * d[2]
v = mesh.volume
@@ -68,7 +94,18 @@ def voxel_fit_surface_mesh(
n_spheres: int,
sphere_radius: float,
voxelize_method: str = "ray",
):
) -> Tuple[numpy.array, List[float]]:
"""Get voxel grid from mesh and fit spheres to the surface.
Args:
mesh: Input mesh.
n_spheres: Number of spheres to fit.
sphere_radius: Radius of the spheres.
voxelize_method: TriMesh Voxelization method. Defaults to "ray".
Returns:
Tuple of sphere positions and sphere radius.
"""
pts, rad = get_voxelgrid_from_mesh(mesh, n_spheres, voxelize_method)
if pts is None:
return pts, rad
@@ -82,7 +119,7 @@ def voxel_fit_surface_mesh(
dist = pr.signed_distance(pts)
# calculate distance to boundary:
dist = np.abs(dist - rad)
dist = numpy.abs(dist - rad)
# get the first n points closest to boundary:
_, idx = torch.topk(torch.as_tensor(dist), k=n_spheres, largest=False)
@@ -91,15 +128,28 @@ def voxel_fit_surface_mesh(
return n_pts, n_radius
def get_voxelgrid_from_mesh(mesh: trimesh.Trimesh, n_spheres: int, voxelize_method: str = "ray"):
"""Get voxel grid from mesh using :py:func:`trimesh.voxel.creation.voxelize`."""
def get_voxelgrid_from_mesh(
mesh: trimesh.Trimesh, n_spheres: int, voxelize_method: str = "ray"
) -> Tuple[Union[numpy.array, None], Union[numpy.array, None]]:
"""Get voxel grid from mesh using :py:func:`trimesh.voxel.creation.voxelize`.
Args:
mesh: Input mesh.
n_spheres: Number of voxels to fit.
voxelize_method: Voxelize method. Defaults to "ray".
Returns:
Tuple of occupied voxels and side of voxels (length of cube). Returns [None, None] if
voxelization fails.
"""
pitch = get_voxel_pitch(mesh, n_spheres)
radius = pitch / 2.0
try:
voxel = voxelize(mesh, pitch, voxelize_method)
voxel = voxel.fill("base")
pts = voxel.points
rad = np.ravel([radius for _ in range(len(pts))])
rad = numpy.ravel([radius for _ in range(len(pts))])
except:
log_warn("voxelization failed")
pts = rad = None
@@ -111,10 +161,25 @@ def voxel_fit_mesh(
n_spheres: int,
surface_sphere_radius: float,
voxelize_method: str = "ray",
):
) -> Tuple[numpy.array, List[float]]:
"""Voxelize mesh, fit spheres to volume and near surface. Return the fitted spheres.
Args:
mesh: Input mesh.
n_spheres: Number of spheres to fit.
surface_sphere_radius: Radius of the spheres on the surface. This radius will be added
to points on the surface of the mesh, causing the spheres to inflate the mesh volume
by this amount.
voxelize_method: Voxelization method to use, select from
:py:func:`trimesh.voxel.creation.voxelize`.
Returns:
Tuple of sphere positions and their radius.
"""
pts, rad = get_voxelgrid_from_mesh(mesh, n_spheres, voxelize_method)
if pts is None:
return pts, rad
# compute signed distance:
pr = trimesh.proximity.ProximityQuery(mesh)
dist = pr.signed_distance(pts)
@@ -132,9 +197,9 @@ def voxel_fit_mesh(
inside_idx = dist >= 0.0
inside_pts = pts[inside_idx]
if len(inside_pts) < n_spheres:
new_pts = np.zeros((n_spheres, 3))
new_pts = numpy.zeros((n_spheres, 3))
new_pts[: len(inside_pts)] = inside_pts
new_radius = np.zeros(n_spheres)
new_radius = numpy.zeros(n_spheres)
new_radius[: len(inside_pts)] = rad[inside_idx]
new_pts[len(inside_pts) :] = surface_pts[: n_spheres - len(inside_pts)]
@@ -148,34 +213,22 @@ def voxel_fit_mesh(
return n_pts, n_radius
def voxel_fit_volume_sample_surface_mesh(
mesh: trimesh.Trimesh,
n_spheres: int,
surface_sphere_radius: float,
voxelize_method: str = "ray",
):
pts, rad = voxel_fit_volume_inside_mesh(mesh, 0.75 * n_spheres, voxelize_method)
if pts is None:
return pts, rad
# compute surface points:
if len(pts) >= n_spheres:
return pts, rad
sample_count = n_spheres - (len(pts))
surface_sample_pts, sample_radius = sample_even_fit_mesh(
mesh, sample_count, surface_sphere_radius
)
pts = np.concatenate([pts, surface_sample_pts])
rad = np.concatenate([rad, sample_radius])
return pts, rad
def voxel_fit_volume_inside_mesh(
mesh: trimesh.Trimesh,
n_spheres: int,
voxelize_method: str = "ray",
):
) -> Tuple[numpy.ndarray, numpy.array]:
"""Voxelize mesh, fit spheres to volume. Return the fitted spheres.
Args:
mesh: Input mesh.
n_spheres: Number of spheres to fit.
voxelize_method: Voxelization method to use, select from
:py:func:`trimesh.voxel.creation.voxelize`.
Returns:
Tuple of sphere positions and their radius.
"""
pts, rad = get_voxelgrid_from_mesh(mesh, 2 * n_spheres, voxelize_method)
if pts is None:
return pts, rad
@@ -192,25 +245,63 @@ def voxel_fit_volume_inside_mesh(
return n_pts, n_radius
def voxel_fit_volume_sample_surface_mesh(
mesh: trimesh.Trimesh,
n_spheres: int,
surface_sphere_radius: float,
voxelize_method: str = "ray",
) -> Tuple[numpy.ndarray, numpy.array]:
"""Voxelize mesh, fit spheres to volume, and sample surface for points.
Args:
mesh: Input mesh.
n_spheres: Number of spheres to fit.
surface_sphere_radius: Radius of the spheres on the surface. This radius will be added
to points on the surface of the mesh, causing the spheres to inflate the mesh volume
by this amount.
voxelize_method: Voxelization method to use, select from
:py:func:`trimesh.voxel.creation.voxelize`.
Returns:
Tuple of sphere positions and their radius.
"""
pts, rad = voxel_fit_volume_inside_mesh(mesh, 0.75 * n_spheres, voxelize_method)
if pts is None:
return pts, rad
# compute surface points:
if len(pts) >= n_spheres:
return pts, rad
sample_count = n_spheres - (len(pts))
surface_sample_pts, sample_radius = sample_even_fit_mesh(
mesh, sample_count, surface_sphere_radius
)
pts = numpy.concatenate([pts, surface_sample_pts])
rad = numpy.concatenate([rad, sample_radius])
return pts, rad
def fit_spheres_to_mesh(
mesh: trimesh.Trimesh,
n_spheres: int,
surface_sphere_radius: float = 0.01,
fit_type: SphereFitType = SphereFitType.VOXEL_VOLUME_SAMPLE_SURFACE,
voxelize_method: str = "ray",
) -> Tuple[np.ndarray, List[float]]:
) -> Tuple[numpy.ndarray, numpy.array]:
"""Approximate a mesh with spheres. See :ref:`attach_object_note` for more details.
Args:
mesh: Input trimesh
n_spheres: _description_
surface_sphere_radius: _description_. Defaults to 0.01.
fit_type: _description_. Defaults to SphereFitType.VOXEL_VOLUME_SAMPLE_SURFACE.
voxelize_method: _description_. Defaults to "ray".
mesh: Input mesh.
n_spheres: Number of spheres to fit.
surface_sphere_radius: Radius of the spheres on the surface. This radius will be added
to points on the surface of the mesh, causing the spheres to inflate the mesh volume
by this amount.
fit_type: Sphere fit type, select from :py:class:`~SphereFitType`.
voxelize_method: Voxelization method to use, select from
:py:func:`trimesh.voxel.creation.voxelize`.
Returns:
_description_
Tuple of spehre positions and their radius.
"""
n_pts = n_radius = None
if fit_type == SphereFitType.SAMPLE_SURFACE:
@@ -236,5 +327,5 @@ def fit_spheres_to_mesh(
dist = torch.linalg.norm(samples - torch.mean(samples, dim=-1).unsqueeze(1), dim=-1)
_, knn_i = dist.topk(n_spheres, largest=True)
n_pts = samples[knn_i].cpu().numpy()
n_radius = np.ravel(n_radius)[knn_i.cpu().flatten().tolist()].tolist()
n_radius = numpy.ravel(n_radius)[knn_i.cpu().flatten().tolist()].tolist()
return n_pts, n_radius

View File

@@ -8,8 +8,12 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""
Implements differentiable point and pose transformations leveraging Warp kernels.
Most of these implementations are available through :class:`~curobo.types.math.Pose`.
"""
# Standard Library
from typing import Optional
from typing import Optional, Tuple
# Third Party
import torch
@@ -23,8 +27,35 @@ from curobo.util.warp import init_warp
def transform_points(
position, quaternion, points, out_points=None, out_gp=None, out_gq=None, out_gpt=None
):
position: torch.Tensor,
quaternion: torch.Tensor,
points: torch.Tensor,
out_points: Optional[torch.Tensor] = None,
out_gp: Optional[torch.Tensor] = None,
out_gq: Optional[torch.Tensor] = None,
out_gpt: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""
Transforms the given points using the provided position and quaternion.
Args:
position: The position tensor representing the translation of the transformation.
quaternion: The quaternion tensor representing the rotation of the transformation.
Quaternion format is [w, x, y, z].
points: The points to be transformed.
out_points: If provided, the transformed points will be stored in this tensor. If not
provided, a new tensor will be created.
out_gp: If provided, the gradient of the transformed points with respect to the position
will be stored in this tensor. If not provided, a new tensor will be created.
out_gq: If provided, the gradient of the transformed points with respect to the quaternion
will be stored in this tensor. If not provided, a new tensor will be created.
out_gpt: If provided, the gradient of the transformed points with respect to the original
points will be stored in this tensor. If not provided, a new tensor will be created.
Returns:
torch.Tensor: The transformed points.
"""
if out_points is None:
out_points = torch.zeros((points.shape[0], 3), device=points.device, dtype=points.dtype)
if out_gp is None:
@@ -40,8 +71,35 @@ def transform_points(
def batch_transform_points(
position, quaternion, points, out_points=None, out_gp=None, out_gq=None, out_gpt=None
):
position: torch.Tensor,
quaternion: torch.Tensor,
points: torch.Tensor,
out_points: Optional[torch.Tensor] = None,
out_gp: Optional[torch.Tensor] = None,
out_gq: Optional[torch.Tensor] = None,
out_gpt: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""
Transforms the given points using the provided batch of position and quaternion.
Args:
position: The position tensor representing the translation of the transformation. Shape
should be (batch_size, 3).
quaternion: The quaternion tensor representing the rotation of the transformation.
Quaternion format is [w, x, y, z]. Shape should be (batch_size, 4).
points: The points to be transformed. Shape should be (batch_size, num_points, 3).
out_points: If provided, the transformed points will be stored in this tensor. If not
provided, a new tensor will be created.
out_gp: If provided, the gradient of the transformed points with respect to the position
will be stored in this tensor. If not provided, a new tensor will be created.
out_gq: If provided, the gradient of the transformed points with respect to the quaternion
will be stored in this tensor. If not provided, a new tensor will be created.
out_gpt: If provided, the gradient of the transformed points with respect to the original
points will be stored in this tensor. If not provided, a new tensor will be created.
Returns:
torch.Tensor: The transformed points with shape (batch_size, num_points, 3).
"""
if out_points is None:
out_points = torch.zeros(
(points.shape[0], points.shape[1], 3), device=points.device, dtype=points.dtype
@@ -61,33 +119,69 @@ def batch_transform_points(
@get_torch_jit_decorator()
def get_inv_transform(w_rot_c, w_trans_c):
# type: (Tensor, Tensor) -> Tuple[Tensor, Tensor]
def get_inv_transform(
w_rot_c: torch.Tensor, w_trans_c: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Get the inverse of the given transformation.
Args:
w_rot_c: Rotation matrix in world frame.
w_trans_c: Translation vector in world frame.
Returns:
Tuple[torch.Tensor, torch.Tensor]: The inverse rotation matrix and translation vector.
"""
c_rot_w = w_rot_c.transpose(-1, -2)
c_trans_w = -1.0 * (c_rot_w @ w_trans_c.unsqueeze(-1)).squeeze(-1)
return c_rot_w, c_trans_w
@get_torch_jit_decorator()
def transform_point_inverse(point, rot, trans):
# type: (Tensor, Tensor, Tensor) -> Tensor
def transform_point_inverse(
point: torch.Tensor, rot: torch.Tensor, trans: torch.Tensor
) -> torch.Tensor:
"""Transforms the given point using the inverse of the provided transformation.
Args:
point: Input point to be transformed.
rot: Rotation matrix.
trans: Translation vector.
Returns:
torch.Tensor: The transformed point.
"""
# new_point = (rot @ (point).unsqueeze(-1)).squeeze(-1) + trans
n_rot, n_trans = get_inv_transform(rot, trans)
new_point = (point @ n_rot.transpose(-1, -2)) + n_trans
return new_point
def matrix_to_quaternion(matrix, out_quat=None, adj_matrix=None):
def matrix_to_quaternion(
matrix: torch.Tensor,
out_quat: Optional[torch.Tensor] = None,
adj_matrix: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Converts the given rotation matrix to quaternion.
Args:
matrix: Rotation matrices as tensor of shape (..., 3, 3).
out_quat: Output tensor to store the quaternions. If not provided, a new tensor will be
created.
adj_matrix: Gradient tensor, if not provided, a new tensor will be created.
Returns:
torch.Tensor: Quaternions with real part first, as tensor of shape (..., 4) [qw, qx,qy,qz].
"""
matrix = matrix.view(-1, 3, 3)
out_quat = MatrixToQuaternion.apply(matrix, out_quat, adj_matrix)
# out_quat = cuda_matrix_to_quaternion(matrix)
return out_quat
def cuda_matrix_to_quaternion(matrix):
"""
Convert rotations given as rotation matrices to quaternions.
def cuda_matrix_to_quaternion(matrix: torch.Tensor) -> torch.Tensor:
"""Convert rotations given as rotation matrices to quaternions.
This is not differentiable. Use :func:`~matrix_to_quaternion` for differentiable conversion.
Args:
matrix: Rotation matrices as tensor of shape (..., 3, 3).
@@ -108,19 +202,32 @@ def cuda_matrix_to_quaternion(matrix):
return out_quat
def quaternion_to_matrix(quaternions, out_mat=None, adj_quaternion=None):
def quaternion_to_matrix(
quaternions: torch.Tensor,
out_mat: Optional[torch.Tensor] = None,
adj_quaternion: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Convert quaternion to rotation matrix.
Args:
quaternions: Input quaternions with real part first, as tensor of shape (..., 4).
out_mat: Output rotation matrices as tensor of shape (..., 3, 3). If not provided, a new
tensor will be created.
adj_quaternion: Gradient tensor, if not provided, a new tensor will be created.
Returns:
torch.Tensor: Rotation matrices as tensor of shape (..., 3, 3).
"""
# return torch_quaternion_to_matrix(quaternions)
out_mat = QuatToMatrix.apply(quaternions, out_mat, adj_quaternion)
return out_mat
def torch_quaternion_to_matrix(quaternions):
"""
Convert rotations given as quaternions to rotation matrices.
def torch_quaternion_to_matrix(quaternions: torch.Tensor) -> torch.Tensor:
"""Convert rotations given as quaternions to rotation matrices.
Args:
quaternions: quaternions with real part first,
as tensor of shape (..., 4).
quaternions: quaternions with real part first, as tensor of shape (..., 4).
Returns:
Rotation matrices as tensor of shape (..., 3, 3).
@@ -149,7 +256,20 @@ def torch_quaternion_to_matrix(quaternions):
def pose_to_matrix(
position: torch.Tensor, quaternion: torch.Tensor, out_matrix: Optional[torch.Tensor] = None
):
) -> torch.Tensor:
"""Converts the given pose to a transformation matrix.
Args:
position: The position tensor representing the translation of the transformation.
quaternion: The quaternion tensor representing the rotation of the transformation.
Quaternion format is [w, x, y, z].
out_matrix: If provided, the transformation matrix will be stored in this tensor. If not
provided, a new tensor will be created.
Returns:
torch.Tensor: The transformation matrix.
"""
if out_matrix is None:
if len(position.shape) == 2:
out_matrix = torch.zeros(
@@ -168,17 +288,44 @@ def pose_to_matrix(
def pose_multiply(
position,
quaternion,
position2,
quaternion2,
out_position=None,
out_quaternion=None,
adj_pos=None,
adj_quat=None,
adj_pos2=None,
adj_quat2=None,
):
position: torch.Tensor,
quaternion: torch.Tensor,
position2: torch.Tensor,
quaternion2: torch.Tensor,
out_position: Optional[torch.Tensor] = None,
out_quaternion: Optional[torch.Tensor] = None,
adj_pos: Optional[torch.Tensor] = None,
adj_quat: Optional[torch.Tensor] = None,
adj_pos2: Optional[torch.Tensor] = None,
adj_quat2: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Multiplies two poses.
The input poses can either be of shape (3,) or (batch_size, 3).
Args:
position: The position tensor representing the translation of the first transformation.
quaternion: The quaternion tensor representing the rotation of the first transformation.
The quaternion format is [w, x, y, z].
position2: The position tensor representing the translation of the second transformation.
quaternion2: The quaternion tensor representing the rotation of the second transformation.
out_position: If provided, the position tensor of the multiplied pose will be stored in
this tensor. If not provided, a new tensor will be created.
out_quaternion: If provided, the quaternion tensor of the multiplied pose will be stored in
this tensor. If not provided, a new tensor will be created.
adj_pos: Gradient tensor for the position of the first pose. If not provided, a new tensor
will be created.
adj_quat: Gradient tensor for the quaternion of the first pose. If not provided, a new
tensor will be created.
adj_pos2: Gradient tensor for the position of the second pose. If not provided, a new
tensor will be created.
adj_quat2: Gradient tensor for the quaternion of the second pose. If not provided, a new
tensor will be created.
Returns:
Tuple[torch.Tensor, torch.Tensor]: The position and quaternion tensors of the multiplied
pose.
"""
if position.shape == position2.shape:
out_position, out_quaternion = BatchTransformPose.apply(
position,
@@ -212,13 +359,31 @@ def pose_multiply(
def pose_inverse(
position,
quaternion,
out_position=None,
out_quaternion=None,
adj_pos=None,
adj_quat=None,
):
position: torch.Tensor,
quaternion: torch.Tensor,
out_position: Optional[torch.Tensor] = None,
out_quaternion: Optional[torch.Tensor] = None,
adj_pos: Optional[torch.Tensor] = None,
adj_quat: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Get the inverse of the given pose.
Args:
position: The position tensor representing the translation of the transformation.
quaternion: The quaternion tensor representing the rotation of the transformation.
out_position: If provided, the position tensor of the inverse pose will be stored in this
tensor. If not provided, a new tensor will be created.
out_quaternion: If provided, the quaternion tensor of the inverse pose will be stored in
this tensor. If not provided, a new tensor will be created.
adj_pos: Gradient tensor for the position of the pose. If not provided, a new tensor will
be created.
adj_quat: Gradient tensor for the quaternion of the pose. If not provided, a new tensor
will be created.
Returns:
Tuple[torch.Tensor, torch.Tensor]: The position and quaternion tensors of the inverse pose.
"""
out_position, out_quaternion = PoseInverse.apply(
position,
quaternion,
@@ -237,7 +402,17 @@ def compute_pose_inverse(
quat: wp.array(dtype=wp.vec4),
out_position: wp.array(dtype=wp.vec3),
out_quat: wp.array(dtype=wp.vec4),
): # b pose_1 and b pose_2, compute pose_1 * pose_2
):
"""Compute inverse of pose. This is a warp kernel.
Args:
position: Input position.
quat: Input quaternion.
out_position: Output position.
out_quat: Output quaternion.
"""
# b pose_1 and b pose_2, compute pose_1 * pose_2
b_idx = wp.tid()
# read data:
@@ -267,6 +442,7 @@ def compute_matrix_to_quat(
in_mat: wp.array(dtype=wp.mat33),
out_quat: wp.array(dtype=wp.vec4),
):
"""A warp kernel to convert rotation matrix to quaternion."""
# b pose_1 and b pose_2, compute pose_1 * pose_2
b_idx = wp.tid()
# read data:
@@ -294,7 +470,9 @@ def compute_transform_point(
n_pts: wp.int32,
n_poses: wp.int32,
out_pt: wp.array(dtype=wp.vec3),
): # given n,3 points and b poses, get b,n,3 transformed points
):
"""A warp kernel to transform the given points using the provided position and quaternion."""
# given n,3 points and b poses, get b,n,3 transformed points
# we tile as
tid = wp.tid()
b_idx = tid / (n_pts)
@@ -325,7 +503,10 @@ def compute_batch_transform_point(
n_pts: wp.int32,
n_poses: wp.int32,
out_pt: wp.array(dtype=wp.vec3),
): # given n,3 points and b poses, get b,n,3 transformed points
):
"""A warp kernel to transform batch of points by batch of poses."""
# given n,3 points and b poses, get b,n,3 transformed points
# we tile as
tid = wp.tid()
b_idx = tid / (n_pts)
@@ -356,7 +537,9 @@ def compute_batch_pose_multipy(
quat2: wp.array(dtype=wp.vec4),
out_position: wp.array(dtype=wp.vec3),
out_quat: wp.array(dtype=wp.vec4),
): # b pose_1 and b pose_2, compute pose_1 * pose_2
):
"""A warp kernel multiplying two batch of poses."""
# b pose_1 and b pose_2, compute pose_1 * pose_2
b_idx = wp.tid()
# read data:
@@ -394,6 +577,7 @@ def compute_quat_to_matrix(
quat: wp.array(dtype=wp.vec4),
out_mat: wp.array(dtype=wp.mat33),
):
"""A warp kernel to convert quaternion to rotation matrix."""
# b pose_1 and b pose_2, compute pose_1 * pose_2
b_idx = wp.tid()
# read data:
@@ -417,7 +601,9 @@ def compute_pose_multipy(
quat2: wp.array(dtype=wp.vec4),
out_position: wp.array(dtype=wp.vec3),
out_quat: wp.array(dtype=wp.vec4),
): # b pose_1 and b pose_2, compute pose_1 * pose_2
):
"""A warp kernel to multiply a batch of poses (position2) by a pose."""
# b pose_1 and b pose_2, compute pose_1 * pose_2
b_idx = wp.tid()
# read data:
@@ -451,6 +637,8 @@ def compute_pose_multipy(
class TransformPoint(torch.autograd.Function):
"""A differentiable function to transform batch of points by a pose."""
@staticmethod
def forward(
ctx,
@@ -549,6 +737,8 @@ class TransformPoint(torch.autograd.Function):
class BatchTransformPoint(torch.autograd.Function):
"""A differentiable function to transform batch of points by a batch of poses."""
@staticmethod
def forward(
ctx,
@@ -596,7 +786,6 @@ class BatchTransformPoint(torch.autograd.Function):
adj_points,
) = ctx.saved_tensors
init_warp()
# print(adj_quaternion.shape)
wp_adj_out_points = wp.from_torch(grad_output.view(-1, 3).contiguous(), dtype=wp.vec3)
adj_position = 0.0 * adj_position
@@ -645,6 +834,8 @@ class BatchTransformPoint(torch.autograd.Function):
class BatchTransformPose(torch.autograd.Function):
"""A differentiable function to transform batch of poses by a pose."""
@staticmethod
def forward(
ctx,
@@ -790,6 +981,8 @@ class BatchTransformPose(torch.autograd.Function):
class TransformPose(torch.autograd.Function):
"""A differentiable function to transform a batch of poses by another batch of poses."""
@staticmethod
def forward(
ctx,
@@ -934,6 +1127,8 @@ class TransformPose(torch.autograd.Function):
class PoseInverse(torch.autograd.Function):
"""A differentiable function to get the inverse of a pose (also supports batch)."""
@staticmethod
def forward(
ctx,
@@ -1048,6 +1243,8 @@ class PoseInverse(torch.autograd.Function):
class QuatToMatrix(torch.autograd.Function):
"""A differentiable function for converting quaternions to rotation matrices."""
@staticmethod
def forward(
ctx,
@@ -1097,7 +1294,7 @@ class QuatToMatrix(torch.autograd.Function):
wp_adj_out_mat = wp.from_torch(grad_out_mat.view(-1, 3, 3).contiguous(), dtype=wp.mat33)
adj_quaternion = 0.0 * adj_quaternion
adj_quaternion[:] = 0.0 * adj_quaternion
wp_adj_quat = wp.from_torch(adj_quaternion.view(-1, 4), dtype=wp.vec4)
@@ -1131,6 +1328,8 @@ class QuatToMatrix(torch.autograd.Function):
class MatrixToQuaternion(torch.autograd.Function):
"""A differentiable function for converting rotation matrices to quaternions."""
@staticmethod
def forward(
ctx,

View File

@@ -15,12 +15,13 @@ from __future__ import annotations
# Standard Library
import math
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Sequence, Union
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
# Third Party
import numpy as np
import torch
import trimesh
import trimesh.scene
# CuRobo
from curobo.geom.sphere_fit import SphereFitType, fit_spheres_to_mesh
@@ -83,6 +84,12 @@ class Obstacle:
raise NotImplementedError
def save_as_mesh(self, file_path: str, transform_with_pose: bool = False):
"""Save obstacle as a mesh file.
Args:
file_path: Path to save mesh file.
transform_with_pose: Transform obstacle with pose before saving.
"""
mesh_scene = self.get_trimesh_mesh()
if transform_with_pose:
mesh_scene.apply_transform(self.get_transform_matrix())
@@ -246,10 +253,19 @@ class Cuboid(Obstacle):
dims: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
def __post_init__(self):
"""Post initialization checks if pose was set."""
if self.pose is None:
log_error("Cuboid Obstacle requires Pose")
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True):
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True) -> trimesh.Trimesh:
"""Create a trimesh instance from the obstacle representation.
Args:
process: Flag is not used.
process_color: Flag is not used.
Returns:
trimesh.Trimesh: Instance of obstacle as a trimesh.
"""
m = trimesh.creation.box(extents=self.dims)
if self.color is not None:
color_visual = trimesh.visual.color.ColorVisuals(
@@ -261,11 +277,27 @@ class Cuboid(Obstacle):
@dataclass
class Capsule(Obstacle):
"""Represent obstacle as a capsule."""
#: Radius of capsule in meters.
radius: float = 0.0
#: Base of capsule in meters [x, y, z].
base: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
#: Tip of capsule in meters [x, y, z].
tip: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True):
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True) -> trimesh.Trimesh:
"""Create a trimesh instance from the obstacle representation.
Args:
process: Flag is not used.
process_color: Flag is not used.
Returns:
trimesh.Trimesh: Instance of obstacle as a trimesh.
"""
height = self.tip[2] - self.base[2]
if (
height < 0
@@ -288,10 +320,24 @@ class Capsule(Obstacle):
@dataclass
class Cylinder(Obstacle):
"""Obstacle represented as a cylinder."""
#: Radius of cylinder in meters.
radius: float = 0.0
#: Height of cylinder in meters.
height: float = 0.0
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True):
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True) -> trimesh.Trimesh:
"""Create a trimesh instance from the obstacle representation.
Args:
process: Flag is not used.
process_color: Flag is not used.
Returns:
trimesh.Trimesh: Instance of obstacle as a trimesh.
"""
m = trimesh.creation.cylinder(radius=self.radius, height=self.height)
if self.color is not None:
color_visual = trimesh.visual.color.ColorVisuals(
@@ -303,19 +349,32 @@ class Cylinder(Obstacle):
@dataclass
class Sphere(Obstacle):
"""Obstacle represented as a sphere."""
#: Radius of sphere in meters.
radius: float = 0.0
#: position is deprecated, use pose instead
#: Position is deprecated, use pose instead
position: Optional[List[float]] = None
def __post_init__(self):
"""Post initialization checks if position was set, logs warning to use pose instead."""
if self.position is not None:
self.pose = self.position + [1, 0, 0, 0]
log_warn("Sphere.position is deprecated, use Sphere.pose instead")
if self.pose is not None:
self.position = self.pose[:3]
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True):
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True) -> trimesh.Trimesh:
"""Create a trimesh instance from the obstacle representation.
Args:
process: Flag is not used.
process_color: Flag is not used.
Returns:
trimesh.Trimesh: Instance of obstacle as a trimesh.
"""
m = trimesh.creation.icosphere(radius=self.radius)
if self.color is not None:
color_visual = trimesh.visual.color.ColorVisuals(
@@ -366,18 +425,35 @@ class Sphere(Obstacle):
@dataclass
class Mesh(Obstacle):
"""Obstacle represented as mesh."""
#: Path to mesh file.
file_path: Optional[str] = None
file_string: Optional[str] = (
None # storing full mesh as a string, loading from this is not implemented yet.
)
urdf_path: Optional[str] = None # useful for visualization in isaac gym.
#: Full mesh as a string, loading from this is not implemented yet.
file_string: Optional[str] = None
#: Path to urdf file, does not support loading from this yet.
urdf_path: Optional[str] = None
#: Vertices of mesh.
vertices: Optional[List[List[float]]] = None
#: Faces of mesh.
faces: Optional[List[int]] = None
#: Vertex colors of mesh.
vertex_colors: Optional[List[List[float]]] = None
#: Vertex normals of mesh.
vertex_normals: Optional[List[List[float]]] = None
#: Face colors of mesh.
face_colors: Optional[List[List[float]]] = None
def __post_init__(self):
"""Post initialization adds absolute path to file_path and scales vertices."""
if self.file_path is not None:
self.file_path = join_path(get_assets_path(), self.file_path)
if self.urdf_path is not None:
@@ -386,7 +462,17 @@ class Mesh(Obstacle):
self.vertices = np.ravel(self.scale) * self.vertices
self.scale = None
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True):
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True) -> trimesh.Trimesh:
"""Create a trimesh instance from the obstacle representation.
Args:
process: process flag passed to :class:`trimesh.load`.
process_color: if True, load mesh visual from texture.
Returns:
trimesh.Trimesh: Instance of obstacle as a trimesh.
"""
# load mesh from filepath or verts and faces:
if self.file_path is not None:
m = trimesh.load(self.file_path, process=process, force="mesh")
@@ -412,6 +498,8 @@ class Mesh(Obstacle):
return m
def update_material(self):
"""Load material into vertex_colors and face_colors."""
if (
self.color is None
and self.vertex_colors is None
@@ -431,7 +519,15 @@ class Mesh(Obstacle):
else:
self.vertex_colors = [m.visual.vertex_colors for x in range(len(m.vertices))]
def get_mesh_data(self, process: bool = True):
def get_mesh_data(self, process: bool = True) -> Tuple[List[List[float]], List[int]]:
"""Get vertices and faces of mesh.
Args:
process: process flag passed to :class:`trimesh.load`.
Returns:
Tuple[List[List[float]], List[int]]: vertices and faces of mesh.
"""
verts = faces = None
if self.file_path is not None:
m = self.get_trimesh_mesh(process=process)
@@ -453,6 +549,19 @@ class Mesh(Obstacle):
pose: List[float] = [0, 0, 0, 1, 0, 0, 0],
filter_close_points: float = 0.0,
):
"""Create a mesh from a pointcloud using marching cubes.
Args:
pointcloud: Input pointcloud of shape [n_pts, 3].
pitch: Pitch of marching cubes.
name: Name to asiign to created mesh.
pose: Pose to assign to created mesh.
filter_close_points: filter points that are closer than this threshold. Threshold
is in meters and should be positive.
Returns:
Mesh: Mesh created from pointcloud.
"""
if filter_close_points > 0.0:
# remove points that are closer than given threshold
dist = np.linalg.norm(pointcloud, axis=-1)
@@ -465,16 +574,31 @@ class Mesh(Obstacle):
@dataclass
class BloxMap(Obstacle):
"""Obstacle represented as a nvblox ESDF layer."""
#: Path to nvblox map file.
map_path: Optional[str] = None
#: Scale of the map.
scale: List[float] = field(default_factory=lambda: [1.0, 1.0, 1.0])
#: Voxel size of the map.
voxel_size: float = 0.02
#: integrator type to use in nvblox. Options: ["tsdf", "occupancy"]
#: Integrator type to use in nvblox. Options: ["tsdf", "occupancy"]
integrator_type: str = "tsdf"
#: File path to mesh file if available, useful for rendering.
mesh_file_path: Optional[str] = None
#: Instance of nvblox mapper. Useful for passing a pre-initialized mapper.
mapper_instance: Any = None
#: Mesh representation of the map. Useful for rendering. Not used in collision checking.
mesh: Optional[Mesh] = None
def __post_init__(self):
"""Post initialization adds absolute path to map_path, mesh_file_path, and loads mesh."""
if self.map_path is not None:
self.map_path = join_path(get_assets_path(), self.map_path)
if self.mesh_file_path is not None:
@@ -482,9 +606,20 @@ class BloxMap(Obstacle):
name=self.name + "_mesh", file_path=self.mesh_file_path, pose=self.pose
)
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True):
def get_trimesh_mesh(
self, process: bool = True, process_color: bool = True
) -> Union[trimesh.Trimesh, None]:
"""Get trimesh mesh representation of the map. Only available if mesh_file_path is set.
Args:
process: Process flag passed to :class:`trimesh.load`.
process_color: Load mesh visual from texture.
Returns:
Union[trimesh.Trimesh, None]: Trimesh mesh representation of the map.
"""
if self.mesh is not None:
return self.mesh.get_trimesh_mesh(process)
return self.mesh.get_trimesh_mesh(process, process_color=process_color)
else:
log_warn("no mesh found for obstacle: " + self.name)
return None
@@ -492,15 +627,31 @@ class BloxMap(Obstacle):
@dataclass
class PointCloud(Obstacle):
"""Obstacle represented as a pointcloud."""
#: Points of pointcloud.
points: Union[torch.Tensor, np.ndarray, List[List[float]]] = None
#: Features of pointcloud.
points_features: Union[torch.Tensor, np.ndarray, List[List[float]], None] = None
def __post_init__(self):
"""Post initialization scales points if scale is set."""
if self.scale is not None and self.points is not None:
self.points = np.ravel(self.scale) * self.points
self.scale = None
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True):
def get_trimesh_mesh(self, process: bool = True, process_color: bool = True) -> trimesh.Trimesh:
"""Create a trimesh instance from the obstacle representation.
Args:
process: Not used.
process_color: Not used.
Returns:
trimesh.Trimesh: Instance of obstacle as a trimesh.
"""
points = self.points
if isinstance(points, torch.Tensor):
points = points.view(-1, 3).cpu().numpy()
@@ -510,7 +661,15 @@ class PointCloud(Obstacle):
mesh = Mesh.from_pointcloud(points, pose=self.pose)
return mesh.get_trimesh_mesh()
def get_mesh_data(self, process: bool = True):
def get_mesh_data(self, process: bool = True) -> Tuple[List[List[float]], List[int]]:
"""Get mesh data from pointcloud.
Args:
process: process flag passed to :class:`trimesh.load`.
Returns:
verts, faces: vertices and faces of mesh.
"""
verts = faces = None
m = self.get_trimesh_mesh(process=process)
verts = m.vertices.view(np.ndarray)
@@ -519,75 +678,50 @@ class PointCloud(Obstacle):
@staticmethod
def from_camera_observation(
camera_obs: CameraObservation, name: str = "pc_obstacle", pose: Optional[List[float]] = None
):
return PointCloud(name=name, pose=pose, points=camera_obs.get_pointcloud())
def get_bounding_spheres(
self,
n_spheres: int = 1,
surface_sphere_radius: float = 0.002,
fit_type: SphereFitType = SphereFitType.VOXEL_VOLUME_SAMPLE_SURFACE,
voxelize_method: str = "ray",
pre_transform_pose: Optional[Pose] = None,
tensor_args: TensorDeviceType = TensorDeviceType(),
) -> List[Sphere]:
"""Compute n spheres that fits in the volume of the object.
camera_obs: CameraObservation,
name: str = "pc_obstacle",
pose: Optional[List[float]] = None,
) -> PointCloud:
"""Create a pointcloud from a camera observation.
Args:
n: number of spheres
camera_obs: Input camera observation.
name: Name to assign to created pointcloud.
pose: Pose to assign to created pointcloud.
Returns:
spheres
PointCloud: Pointcloud created from camera observation.
"""
# sample points in pointcloud:
# mesh = self.get_trimesh_mesh()
# pts, n_radius = fit_spheres_to_mesh(
# mesh, n_spheres, surface_sphere_radius, fit_type, voxelize_method=voxelize_method
# )
obj_pose = Pose.from_list(self.pose, tensor_args)
# transform object:
# transform points:
if pre_transform_pose is not None:
obj_pose = pre_transform_pose.multiply(obj_pose) # convert object pose to another frame
if pts is None or len(pts) == 0:
log_warn("spheres could not be fit!, adding one sphere at origin")
pts = np.zeros((1, 3))
pts[0, :] = mesh.centroid
n_radius = [0.02]
obj_pose = Pose.from_list([0, 0, 0, 1, 0, 0, 0], tensor_args)
points_cuda = tensor_args.to_device(pts)
pts = obj_pose.transform_points(points_cuda).cpu().view(-1, 3).numpy()
new_spheres = [
Sphere(
name="sph_" + str(i),
pose=[pts[i, 0], pts[i, 1], pts[i, 2], 1, 0, 0, 0],
radius=n_radius[i],
)
for i in range(pts.shape[0])
]
return new_spheres
return PointCloud(name=name, pose=pose, points=camera_obs.get_pointcloud())
@dataclass
class VoxelGrid(Obstacle):
"""VoxelGrid representation of an obstacle. Requires voxel to contain ESDF."""
#: Dimensions of voxel grid in meters [x_length, y_length, z_length].
dims: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
#: Voxel size in meters.
voxel_size: float = 0.02 # meters
#: Feature tensor of voxel grid, typically ESDF.
feature_tensor: Optional[torch.Tensor] = None
#: XYZR tensor of voxel grid.
xyzr_tensor: Optional[torch.Tensor] = None
#: Data type of feature tensor.
feature_dtype: torch.dtype = torch.float32
def __post_init__(self):
"""Post initialization checks."""
if self.feature_tensor is not None:
self.feature_dtype = self.feature_tensor.dtype
def get_grid_shape(self):
def get_grid_shape(self) -> Tuple[List[int], List[float], List[float]]:
"""Get shape of voxel grid."""
bounds = self.dims
low = [-bounds[0] / 2, -bounds[1] / 2, -bounds[2] / 2]
high = [bounds[0] / 2, bounds[1] / 2, bounds[2] / 2]
@@ -599,7 +733,17 @@ class VoxelGrid(Obstacle):
def create_xyzr_tensor(
self, transform_to_origin: bool = False, tensor_args: TensorDeviceType = TensorDeviceType()
):
) -> torch.Tensor:
"""Create XYZR tensor of voxel grid.
Args:
transform_to_origin: Transform points to origin.
tensor_args: Device and floating point precision to use for tensors.
Returns:
xyzr_tensor: XYZR tensor of voxel grid with r referring to voxel size.
"""
trange, low, high = self.get_grid_shape()
x = torch.linspace(low[0], high[0], trange[0], device=tensor_args.device)
@@ -618,7 +762,15 @@ class VoxelGrid(Obstacle):
return xyzr
def get_occupied_voxels(self, feature_threshold: Optional[float] = None):
def get_occupied_voxels(self, feature_threshold: Optional[float] = None) -> torch.Tensor:
"""Get occupied voxels from voxel grid.
Args:
feature_threshold: esdf value threshold to consider as occupied.
Returns:
occupied voxels as a tensor of shape [occupied_voxels].
"""
if feature_threshold is None:
feature_threshold = -0.5 * self.voxel_size
if self.xyzr_tensor is None or self.feature_tensor is None:
@@ -628,12 +780,15 @@ class VoxelGrid(Obstacle):
occupied = xyzr[self.feature_tensor > feature_threshold]
return occupied
def clone(self):
def clone(self) -> VoxelGrid:
"""Clone data of voxel grid."""
return VoxelGrid(
name=self.name,
pose=self.pose.copy(),
dims=self.dims.copy(),
feature_tensor=self.feature_tensor.clone() if self.feature_tensor is not None else None,
feature_tensor=(
self.feature_tensor.clone() if self.feature_tensor is not None else None
),
xyzr_tensor=self.xyzr_tensor.clone() if self.xyzr_tensor is not None else None,
feature_dtype=self.feature_dtype,
voxel_size=self.voxel_size,
@@ -662,12 +817,14 @@ class WorldConfig(Sequence):
#: BloxMap obstacle.
blox: Optional[List[BloxMap]] = None
#: List of ESDF voxel grid obstacles.
voxel: Optional[List[VoxelGrid]] = None
#: List of all obstacles in world.
objects: Optional[List[Obstacle]] = None
def __post_init__(self):
"""Post initialization checks, also creates a list of all obstacles."""
# create objects list:
if self.sphere is None:
self.sphere = []
@@ -694,13 +851,16 @@ class WorldConfig(Sequence):
+ self.voxel
)
def __len__(self):
def __len__(self) -> int:
"""Get number of obstacles."""
return len(self.objects)
def __getitem__(self, idx):
def __getitem__(self, idx: int) -> Obstacle:
"""Get obstacle at index."""
return self.objects[idx]
def clone(self):
def clone(self) -> WorldConfig:
"""Clone world configuration."""
return WorldConfig(
cuboid=self.cuboid.copy() if self.cuboid is not None else None,
sphere=self.sphere.copy() if self.sphere is not None else None,
@@ -713,6 +873,14 @@ class WorldConfig(Sequence):
@staticmethod
def from_dict(data_dict: Dict[str, Any]) -> WorldConfig:
"""Load world configuration from dictionary.
Args:
data_dict: World configuration dictionary.
Returns:
Instance of WorldConfig.
"""
cuboid = None
sphere = None
capsule = None
@@ -748,7 +916,8 @@ class WorldConfig(Sequence):
# load world config as obbs: convert all types to obbs
@staticmethod
def create_obb_world(current_world: WorldConfig):
def create_obb_world(current_world: WorldConfig) -> WorldConfig:
"""Approximate all obstcales to oriented bounding boxes."""
sphere_obb = []
capsule_obb = []
cylinder_obb = []
@@ -778,7 +947,16 @@ class WorldConfig(Sequence):
)
@staticmethod
def create_mesh_world(current_world: WorldConfig, process: bool = False):
def create_mesh_world(current_world: WorldConfig, process: bool = False) -> WorldConfig:
"""Convert all obstacles to meshes. Does not convert :class:`VoxelGrid`, :class:`BloxMap`.
Args:
current_world: Current world configuration.
process: process flag passed to :class:`trimesh.load`.
Returns:
WorldConfig: World configuration with all obstacles converted to meshes.
"""
sphere_obb = []
capsule_obb = []
cuboid_obb = []
@@ -812,7 +990,19 @@ class WorldConfig(Sequence):
)
@staticmethod
def create_collision_support_world(current_world: WorldConfig, process: bool = True):
def create_collision_support_world(
current_world: WorldConfig, process: bool = True
) -> WorldConfig:
"""Converts all obstacles to only supported collision types.
Args:
current_world: Current world configuration.
process: process flag passed to :class:`trimesh.load`.
Returns:
WorldConfig: World configuration with all obstacles converted to supported collision
types.
"""
sphere_obb = []
capsule_obb = []
cuboid_obb = []
@@ -841,7 +1031,18 @@ class WorldConfig(Sequence):
)
@staticmethod
def get_scene_graph(current_world: WorldConfig, process_color: bool = True):
def get_scene_graph(
current_world: WorldConfig, process_color: bool = True
) -> trimesh.scene.scene.Scene:
"""Get trimesh scene graph of world.
Args:
current_world: Current world configuration.
process_color: Load color of meshes.
Returns:
trimesh.scene.scene.Scene: Scene graph of world.
"""
m_world = WorldConfig.create_mesh_world(current_world)
mesh_scene = trimesh.scene.scene.Scene(base_frame="world_origin")
mesh_list = m_world
@@ -858,7 +1059,17 @@ class WorldConfig(Sequence):
@staticmethod
def create_merged_mesh_world(
current_world: WorldConfig, process: bool = True, process_color: bool = True
):
) -> WorldConfig:
"""Merge all obstacles in the world to a single mesh.
Args:
current_world: Current world configuration.
process: process flag passed to :class:`trimesh.load`.
process_color: Load color of meshes.
Returns:
WorldConfig: World configuration with a single merged mesh as obstacle.
"""
mesh_scene = WorldConfig.get_scene_graph(current_world, process_color=process_color)
mesh_scene = mesh_scene.dump(concatenate=True)
new_mesh = Mesh(
@@ -869,21 +1080,31 @@ class WorldConfig(Sequence):
)
return WorldConfig(mesh=[new_mesh])
def get_obb_world(self):
def get_obb_world(self) -> WorldConfig:
"""Get world with all obstacles as oriented bounding boxes."""
return WorldConfig.create_obb_world(self)
def get_mesh_world(self, merge_meshes: bool = False, process: bool = False):
def get_mesh_world(self, merge_meshes: bool = False, process: bool = False) -> WorldConfig:
"""Get world with all obstacles as meshes."""
if merge_meshes:
return WorldConfig.create_merged_mesh_world(self, process=process)
else:
return WorldConfig.create_mesh_world(self, process=process)
def get_collision_check_world(self, mesh_process: bool = False):
def get_collision_check_world(self, mesh_process: bool = False) -> WorldConfig:
"""Get world with all obstacles converted to supported collision types."""
return WorldConfig.create_collision_support_world(self, process=mesh_process)
def save_world_as_mesh(
self, file_path: str, save_as_scene_graph=False, process_color: bool = True
):
"""Save world as a mesh file.
Args:
file_path: Path to save mesh file.
save_as_scene_graph: Save as scene graph.
process_color: Load color of meshes.
"""
mesh_scene = WorldConfig.get_scene_graph(self, process_color=process_color)
if save_as_scene_graph:
mesh_scene = mesh_scene.dump(concatenate=True)
@@ -891,15 +1112,16 @@ class WorldConfig(Sequence):
mesh_scene.export(file_path)
def get_cache_dict(self) -> Dict[str, int]:
"""Computes the number of obstacles in each type
Returns:
_description_
"""
"""Computes the number of obstacles in each type."""
cache = {"obb": len(self.cuboid), "mesh": len(self.mesh)}
return cache
def add_obstacle(self, obstacle: Obstacle):
"""Add obstacle to world.
Args:
obstacle: Obstacle to add to world.
"""
if isinstance(obstacle, Mesh):
self.mesh.append(obstacle)
elif isinstance(obstacle, Cuboid):
@@ -920,12 +1142,9 @@ class WorldConfig(Sequence):
"""Randomize color of objects within the given range
Args:
r: _description_. Defaults to [0,1].
g: _description_. Defaults to [0,1].
b: _description_. Defaults to [0,1].
Returns:
_description_
r: range of red color.
g: range of green color.
b: range of blue color.
"""
n = len(self.objects)
r_l = np.random.uniform(r[0], r[1], n)
@@ -935,32 +1154,72 @@ class WorldConfig(Sequence):
i_val.color = [r_l[i], g_l[i], b_l[i], 1.0]
def add_color(self, rgba=[0.0, 0.0, 0.0, 1.0]):
"""Update color of obstacles.
Args:
rgba: red, green, blue, alpha values.
"""
for i, i_val in enumerate(self.objects):
i_val.color = rgba
def add_material(self, material=Material()):
"""Add material to all obstacles.
Args:
material: material to add to obstacles.
"""
for i, i_val in enumerate(self.objects):
i_val.material = material
def get_obstacle(self, name: str) -> Union[None, Obstacle]:
"""Get obstacle by name.
Args:
name: Name of obstacle.
Returns:
Obstacle with given name. If not found, returns None.
"""
for i in self.objects:
if i.name == name:
return i
return None
def remove_obstacle(self, name: str):
"""Remove obstacle by name.
Args:
name: Name of obstacle to remove.
"""
for i in range(len(self.objects)):
if self.objects[i].name == name:
del self.objects[i]
return
def remove_absolute_paths(self) -> WorldConfig:
def remove_absolute_paths(self):
"""Remove absolute paths from file paths in obstacles. May not work on Windows."""
for obj in self.objects:
if obj.name.startswith("/"):
obj.name = obj.name[1:]
def tensor_sphere(pt, radius, tensor=None, tensor_args=TensorDeviceType()):
def tensor_sphere(
pt: Union[List[float], np.array, torch.Tensor],
radius: float,
tensor: Optional[torch.Tensor] = None,
tensor_args: TensorDeviceType = TensorDeviceType(),
) -> torch.Tensor:
"""Tensor representation of a sphere.
Args:
pt: Input point.
radius: Radius of sphere.
tensor: Tensor to update. If None, creates a new tensor.
tensor_args: Device and floating point precision to use for tensors.
Returns:
tensor: Tensor representation of sphere.
"""
if tensor is None:
tensor = torch.empty(4, device=tensor_args.device, dtype=tensor_args.dtype)
tensor[:3] = torch.as_tensor(pt, device=tensor_args.device, dtype=tensor_args.dtype)
@@ -968,7 +1227,26 @@ def tensor_sphere(pt, radius, tensor=None, tensor_args=TensorDeviceType()):
return tensor
def tensor_capsule(base, tip, radius, tensor=None, tensor_args=TensorDeviceType()):
def tensor_capsule(
base: Union[List[float], torch.Tensor, np.array],
tip: Union[List[float], torch.Tensor, np.array],
radius: float,
tensor: Optional[torch.Tensor] = None,
tensor_args: TensorDeviceType = TensorDeviceType(),
) -> torch.Tensor:
"""Tensor representation of a capsule.
Args:
base: Base of capsule.
tip: Tip of capsule.
radius: radius of capsule.
tensor: Tensor to update. If None, creates a new tensor.
tensor_args: Device and floating point precision to use for tensors.
Returns:
torch.Tensor: Tensor representation of capsule.
"""
if tensor is None:
tensor = torch.empty(7, device=tensor_args.device, dtype=tensor_args.dtype)
tensor[:3] = torch.as_tensor(base, device=tensor_args.device, dtype=tensor_args.dtype)
@@ -977,16 +1255,19 @@ def tensor_capsule(base, tip, radius, tensor=None, tensor_args=TensorDeviceType(
return tensor
def tensor_cube(pose, dims, tensor_args=TensorDeviceType()):
"""
def tensor_cube(
pose: List[float], dims: List[float], tensor_args: TensorDeviceType = TensorDeviceType()
) -> List[torch.Tensor, torch.Tensor]:
"""Tensor representation of a cube.
Args:
pose (_type_): x,y,z, qw,qx,qy,qz
dims (_type_): _description_
tensor_args (_type_, optional): _description_. Defaults to TensorDeviceType().
pose: x,y,z, qw, qx, qy, qz.
dims: length, width, height in meters. Frame is at the center of the cube.
tensor_args: Device and floating point precision to use for tensors.
Returns:
_type_: _description_
List[torch.Tensor, torch.Tensor]: Tensor representation of cube, first tensor is
dimensions and second tensor is inverse of pose.
"""
w_T_b = Pose.from_list(pose, tensor_args=tensor_args)
b_T_w = w_T_b.inverse()
@@ -997,16 +1278,20 @@ def tensor_cube(pose, dims, tensor_args=TensorDeviceType()):
return cube
def batch_tensor_cube(pose, dims, tensor_args=TensorDeviceType()):
"""
def batch_tensor_cube(
pose: List[List[float]],
dims: List[List[float]],
tensor_args: TensorDeviceType = TensorDeviceType(),
) -> List[torch.Tensor]:
"""Tensor representation of a batch of cubes
Args:
pose (_type_): x,y,z, qw,qx,qy,qz
dims (_type_): _description_
tensor_args (_type_, optional): _description_. Defaults to TensorDeviceType().
pose : Poses of the cubes in x,y,z, qw,qx,qy,qz.
dims : Dimensions of the cubes. Frame is at the center of the cube.
tensor_args: Device and floating point precision to use for tensors.
Returns:
_type_: _description_
List[torch.Tensor]: Tensor representation of cubes, first tensor is dimensions and
second tensor is inverse of poses.
"""
w_T_b = Pose.from_batch_list(pose, tensor_args=tensor_args)
b_T_w = w_T_b.inverse()

View File

@@ -408,7 +408,9 @@ class DistCost(CostBase, DistCostConfig):
if self.terminal and self.run_weight is not None:
if self._run_weight_vec is None or self._run_weight_vec.shape[1] != cost.shape[1]:
self._run_weight_vec = torch.ones(
(1, cost.shape[1]), device=self.tensor_args.device, dtype=self.tensor_args.dtype
(1, cost.shape[1]),
device=self.tensor_args.device,
dtype=self.tensor_args.dtype,
)
self._run_weight_vec[:, :-1] *= self.run_weight
if RETURN_GOAL_DIST:
@@ -430,7 +432,9 @@ class DistCost(CostBase, DistCostConfig):
if self.terminal and self.run_weight is not None:
if self._run_weight_vec is None or self._run_weight_vec.shape[1] != cost.shape[1]:
self._run_weight_vec = torch.ones(
(1, cost.shape[1]), device=self.tensor_args.device, dtype=self.tensor_args.dtype
(1, cost.shape[1]),
device=self.tensor_args.device,
dtype=self.tensor_args.dtype,
)
self._run_weight_vec[:, :-1] *= self.run_weight
cost = self._run_weight_vec * dist

View File

@@ -112,13 +112,17 @@ class CameraObservation:
return self
def get_pointcloud(self):
def get_pointcloud(self, project_to_pose: bool = False):
if self.projection_rays is None:
self.update_projection_rays()
depth_image = self.depth_image
if len(self.depth_image.shape) == 2:
depth_image = self.depth_image.unsqueeze(0)
point_cloud = project_depth_using_rays(depth_image, self.projection_rays)
if project_to_pose and self.pose is not None:
point_cloud = self.pose.batch_transform(point_cloud)
return point_cloud
def get_image_from_pointcloud(self, pointcloud, output_image: Optional[torch.Tensor] = None):

View File

@@ -143,6 +143,13 @@ def get_batch_interpolated_trajectory(
# given the dt required to run trajectory at maximum velocity,
# we find the number of timesteps required:
if optimize_dt:
if max_vel is None:
log_error("Max velocity not provided")
if max_acc is None:
log_error("Max acceleration not provided")
if max_jerk is None:
log_error("Max jerk not provided")
if max_vel is not None and max_acc is not None and max_jerk is not None:
traj_vel = raw_traj.velocity
traj_acc = raw_traj.acceleration
traj_jerk = raw_traj.jerk
@@ -168,7 +175,8 @@ def get_batch_interpolated_trajectory(
)
else:
traj_steps, steps_max = calculate_traj_steps(raw_dt, interpolation_dt, horizon)
opt_dt = raw_dt
opt_dt = torch.zeros(b, device=tensor_args.device)
opt_dt[:] = raw_dt
# traj_steps contains the tsteps for each trajectory
if steps_max <= 0:
log_error("Steps max is less than 0")

View File

@@ -26,7 +26,10 @@ def init_warp(quiet=True, tensor_args: TensorDeviceType = TensorDeviceType()):
# wp.config.print_launches = True
# wp.config.verbose = True
# wp.config.mode = "debug"
# wp.config.verify_cuda = True
# wp.config.enable_backward = True
# wp.config.verify_autograd_array_access = True
# wp.config.cache_kernels = False
wp.init()
# wp.force_load(wp.device_from_torch(tensor_args.device))

View File

@@ -8,11 +8,12 @@
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
#
"""Contains helper functions for interacting with file systems."""
# Standard Library
import os
import shutil
import sys
from typing import Dict, List, Union
from typing import Any, Dict, List, Union
# Third Party
import yaml
@@ -23,41 +24,70 @@ from curobo.util.logger import log_warn
# get paths
def get_module_path():
def get_module_path() -> str:
"""Get absolute path of cuRobo library."""
path = os.path.dirname(__file__)
return path
def get_root_path():
def get_root_path() -> str:
"""Get absolute path of cuRobo library."""
path = os.path.dirname(get_module_path())
return path
def get_content_path():
def get_content_path() -> str:
"""Get path to content directory in cuRobo.
Content directory contains configuration parameters for different tasks, some robot
parameters for using in examples, and some world assets. Use
:class:`~curobo.util.file_path.ContentPath` when running cuRobo with assets from a different
location.
Returns:
str: path to content directory.
"""
root_path = get_module_path()
path = os.path.join(root_path, "content")
return path
def get_configs_path():
def get_configs_path() -> str:
"""Get path to configuration parameters for different tasks(e.g., IK, TrajOpt, MPC) in cuRobo.
Returns:
str: path to configuration directory.
"""
content_path = get_content_path()
path = os.path.join(content_path, "configs")
return path
def get_assets_path():
def get_assets_path() -> str:
"""Get path to assets (robot urdf, meshes, world meshes) directory in cuRobo."""
content_path = get_content_path()
path = os.path.join(content_path, "assets")
return path
def get_weights_path():
"""Get path to neural network weights directory in cuRobo. Currently not used in cuRobo."""
content_path = get_content_path()
path = os.path.join(content_path, "weights")
return path
def join_path(path1, path2):
def join_path(path1: str, path2: str) -> str:
"""Join two paths, considering OS specific path separators.
Args:
path1: Path prefix.
path2: Path suffix. If path2 is an absolute path, path1 is ignored.
Returns:
str: Joined path.
"""
if path1[-1] == os.sep:
log_warn("path1 has trailing slash, removing it")
if isinstance(path2, str):
@@ -66,7 +96,15 @@ def join_path(path1, path2):
return path2
def load_yaml(file_path):
def load_yaml(file_path: Union[str, Dict]) -> Dict:
"""Load yaml file and return as dictionary. If file_path is a dictionary, return as is.
Args:
file_path: File path to yaml file or dictionary.
Returns:
Dict: Dictionary containing yaml file content.
"""
if isinstance(file_path, str):
with open(file_path) as file_p:
yaml_params = yaml.load(file_p, Loader=Loader)
@@ -75,47 +113,109 @@ def load_yaml(file_path):
return yaml_params
def write_yaml(data: Dict, file_path):
def write_yaml(data: Dict, file_path: str):
"""Write dictionary to yaml file.
Args:
data: Dictionary to write to yaml file.
file_path: Path to write the yaml file.
"""
with open(file_path, "w") as file:
yaml.dump(data, file)
def get_robot_path():
def get_robot_path() -> str:
"""Get path to robot directory in cuRobo.
Deprecated: Use :func:`~curobo.util_file.get_robot_configs_path` instead.
Robot directory contains robot configuration files in yaml format. See
:ref:`tut_robot_configuration` for how to create a robot configuration file.
Returns:
str: path to robot directory.
"""
config_path = get_configs_path()
path = os.path.join(config_path, "robot")
return path
def get_task_configs_path():
def get_task_configs_path() -> str:
"""Get path to task configuration directory in cuRobo.
Task directory contains configuration parameters for different tasks (e.g., IK, TrajOpt, MPC).
Returns:
str: path to task configuration directory.
"""
config_path = get_configs_path()
path = os.path.join(config_path, "task")
return path
def get_robot_configs_path():
def get_robot_configs_path() -> str:
"""Get path to robot configuration directory in cuRobo.
Robot configuration directory contains robot configuration files in yaml format. See
:ref:`tut_robot_configuration` for how to create a robot configuration file.
Returns:
str: path to robot configuration directory.
"""
config_path = get_configs_path()
path = os.path.join(config_path, "robot")
return path
def get_world_configs_path():
def get_world_configs_path() -> str:
"""Get path to world configuration directory in cuRobo.
World configuration directory contains world configuration files in yaml format. World
information includes obstacles represented with respect to the robot base frame.
Returns:
str: path to world configuration directory.
"""
config_path = get_configs_path()
path = os.path.join(config_path, "world")
return path
def get_debug_path():
def get_debug_path() -> str:
"""Get path to debug directory in cuRobo.
Debug directory can be used to store logs and debug information.
Returns:
str: path to debug directory.
"""
asset_path = get_assets_path()
path = join_path(asset_path, "debug")
return path
def get_cpp_path():
"""Get path to cpp directory in cuRobo.
Directory contains CUDA implementations (kernels) of robotics algorithms, which are wrapped
in C++ and compiled with PyTorch to enable usage in Python.
Returns:
str: path to cpp directory.
"""
path = os.path.dirname(__file__)
return os.path.join(path, "curobolib/cpp")
def add_cpp_path(sources):
def add_cpp_path(sources: List[str]) -> List[str]:
"""Add cpp path to list of source files.
Args:
sources: List of source files.
Returns:
List[str]: List of source files with cpp path added.
"""
cpp_path = get_cpp_path()
new_list = []
for s in sources:
@@ -124,8 +224,16 @@ def add_cpp_path(sources):
return new_list
def copy_file_to_path(source_file, destination_path):
#
def copy_file_to_path(source_file: str, destination_path: str) -> str:
"""Copy file from source to destination.
Args:
source_file: Path of source file.
destination_path: Path of destination directory.
Returns:
str: Destination path of copied file.
"""
isExist = os.path.exists(destination_path)
if not isExist:
os.makedirs(destination_path)
@@ -137,19 +245,47 @@ def copy_file_to_path(source_file, destination_path):
return new_path
def get_filename(file_path, remove_extension: bool = False):
def get_filename(file_path: str, remove_extension: bool = False) -> str:
"""Get file name from file path, removing extension if required.
Args:
file_path: Path of file.
remove_extension: If True, remove file extension.
Returns:
str: File name.
"""
_, file_name = os.path.split(file_path)
if remove_extension:
file_name = os.path.splitext(file_name)[0]
return file_name
def get_path_of_dir(file_path):
def get_path_of_dir(file_path: str) -> str:
"""Get path of directory containing the file.
Args:
file_path: Path of file.
Returns:
str: Path of directory containing the file.
"""
dir_path, _ = os.path.split(file_path)
return dir_path
def get_files_from_dir(dir_path, extension: List[str], contains: str):
def get_files_from_dir(dir_path, extension: List[str], contains: str) -> List[str]:
"""Get list of files from directory with specified extension and containing a string.
Args:
dir_path: Path of directory.
extension: List of file extensions to filter.
contains: String to filter file names.
Returns:
List[str]: List of file names. Does not include path.
"""
file_names = [
fn
for fn in os.listdir(dir_path)
@@ -159,7 +295,15 @@ def get_files_from_dir(dir_path, extension: List[str], contains: str):
return file_names
def file_exists(path):
def file_exists(path: str) -> bool:
"""Check if file exists.
Args:
path: Path of file.
Returns:
bool: True if file exists, False otherwise.
"""
if path is None:
return False
isExist = os.path.exists(path)
@@ -167,11 +311,7 @@ def file_exists(path):
def get_motion_gen_robot_list() -> List[str]:
"""returns list of robots available in curobo for motion generation
Returns:
_description_
"""
"""Get list of robot configuration examples in cuRobo for motion generation."""
robot_list = [
"franka.yml",
"ur5e.yml",
@@ -187,10 +327,12 @@ def get_motion_gen_robot_list() -> List[str]:
def get_robot_list() -> List[str]:
"""Get list of robots example configurations in cuRobo."""
return get_motion_gen_robot_list()
def get_multi_arm_robot_list() -> List[str]:
"""Get list of multi-arm robot configuration examples in cuRobo."""
robot_list = [
"dual_ur10e.yml",
"tri_ur10e.yml",
@@ -199,23 +341,43 @@ def get_multi_arm_robot_list() -> List[str]:
return robot_list
def merge_dict_a_into_b(a, b):
def merge_dict_a_into_b(a: Dict[str, Any], b: Dict[str, Any]) -> Dict[str, Any]:
"""Merge dictionary values in "a" into dictionary "b". Overwrite values in "b" if key exists.
Args:
a: New dictionary to merge.
b: Base dictionary to merge into.
Returns:
Merged dictionary.
"""
for k, v in a.items():
if isinstance(v, dict):
merge_dict_a_into_b(v, b[k])
else:
b[k] = v
return b
def is_platform_windows():
def is_platform_windows() -> bool:
"""Check if platform is Windows."""
return sys.platform == "win32"
def is_platform_linux():
def is_platform_linux() -> bool:
"""Check if platform is Linux."""
return sys.platform == "linux"
def is_file_xrdf(file_path: str) -> bool:
"""Check if file is an `XRDF <https://nvidia-isaac-ros.github.io/concepts/manipulation/xrdf.html>`_ file.
Args:
file_path: Path of file.
Returns:
bool: True if file is xrdf, False otherwise.
"""
if file_path.endswith(".xrdf") or file_path.endswith(".XRDF"):
return True
return False

View File

@@ -35,11 +35,7 @@ from curobo.types.robot import RobotConfig
from curobo.types.state import JointState
from curobo.util.logger import log_error
from curobo.util.sample_lib import HaltonGenerator
from curobo.util.torch_utils import (
get_torch_compile_options,
get_torch_jit_decorator,
is_torch_compile_available,
)
from curobo.util.torch_utils import get_torch_jit_decorator
from curobo.util.warp import init_warp
from curobo.util_file import get_robot_configs_path, get_world_configs_path, join_path, load_yaml

View File

@@ -1792,7 +1792,7 @@ class MotionGen(MotionGenConfig):
world: New world configuration for collision checking.
"""
self.world_coll_checker.load_collision_model(world, fix_cache_reference=self.use_cuda_graph)
self.graph_planner.reset_graph()
self.graph_planner.reset_buffer()
def clear_world_cache(self):
"""Remove all collision objects from collision cache."""
@@ -2214,7 +2214,6 @@ class MotionGen(MotionGenConfig):
for rollout in rollouts
if isinstance(rollout, ArmReacher)
]
torch.cuda.synchronize(device=self.tensor_args.device)
return True
def get_all_rollout_instances(self) -> List[RolloutBase]:
@@ -3471,21 +3470,22 @@ class MotionGen(MotionGenConfig):
opt_dt = traj_result.optimized_dt
if plan_config.parallel_finetune:
opt_dt = torch.min(opt_dt[traj_result.success])
seed_override = solve_state.num_trajopt_seeds * self.noisy_trajopt_seeds
if self.optimize_dt:
opt_dt = torch.min(opt_dt[traj_result.success])
finetune_time = 0
newton_iters = None
for k in range(plan_config.finetune_attempts):
scaled_dt = torch.clamp(
opt_dt
* plan_config.finetune_dt_scale
* (plan_config.finetune_dt_decay ** (k)),
self.trajopt_solver.minimum_trajectory_dt,
)
if self.optimize_dt:
scaled_dt = torch.clamp(
opt_dt
* plan_config.finetune_dt_scale
* (plan_config.finetune_dt_decay ** (k)),
self.trajopt_solver.minimum_trajectory_dt,
)
self.finetune_trajopt_solver.update_solver_dt(scaled_dt.item())
traj_result = self._solve_trajopt_from_solve_state(

View File

@@ -51,8 +51,11 @@ from curobo.opt.newton.lbfgs import LBFGSOpt, LBFGSOptConfig
from curobo.opt.particle.parallel_es import ParallelES, ParallelESConfig
from curobo.opt.particle.parallel_mppi import ParallelMPPI, ParallelMPPIConfig
from curobo.rollout.arm_reacher import ArmReacher, ArmReacherConfig
from curobo.rollout.cost.pose_cost import PoseCostMetric
from curobo.rollout.dynamics_model.kinematic_model import KinematicModelState
from curobo.rollout.rollout_base import Goal
from curobo.types.base import TensorDeviceType
from curobo.types.math import Pose
from curobo.types.robot import JointState, RobotConfig
from curobo.util.logger import log_error, log_info, log_warn
from curobo.util_file import (
@@ -113,6 +116,7 @@ class MpcSolverConfig:
use_mppi: bool = True,
particle_file: str = "particle_mpc.yml",
override_particle_file: str = None,
project_pose_to_goal_frame: bool = True,
):
"""Create an MPC solver configuration from robot and world configuration.
@@ -160,6 +164,9 @@ class MpcSolverConfig:
particle_file: Particle based MPC config file.
override_particle_file: Optional config file for overriding the parameters in the
particle based MPC config file.
project_pose_to_goal_frame: Project pose to goal frame when calculating distance
between reached and goal pose. Use this to constrain motion to specific axes
either in the global frame or the goal frame.
Returns:
MpcSolverConfig: Configuration for the MPC solver.
@@ -184,6 +191,9 @@ class MpcSolverConfig:
if n_collision_envs is not None:
base_cfg["world_collision_checker_cfg"]["n_envs"] = n_collision_envs
base_cfg["cost"]["pose_cfg"]["project_distance"] = project_pose_to_goal_frame
base_cfg["convergence"]["pose_cfg"]["project_distance"] = project_pose_to_goal_frame
config_data["cost"]["pose_cfg"]["project_distance"] = project_pose_to_goal_frame
if collision_activation_distance is not None:
config_data["cost"]["primitive_collision_cfg"][
"activation_distance"
@@ -217,6 +227,7 @@ class MpcSolverConfig:
config_data["model"] = grad_config_data["model"]
if use_cuda_graph is not None:
grad_config_data["lbfgs"]["use_cuda_graph"] = use_cuda_graph
grad_config_data["cost"]["pose_cfg"]["project_distance"] = project_pose_to_goal_frame
cfg = ArmReacherConfig.from_dict(
robot_cfg,
@@ -604,6 +615,99 @@ class MpcSolver(MpcSolverConfig):
"""Get rollouts for debugging."""
return self.solver.optimizers[0].get_rollouts()
def update_pose_cost_metric(
self,
metric: PoseCostMetric,
start_state: Optional[JointState] = None,
goal_pose: Optional[Pose] = None,
check_validity: bool = True,
) -> bool:
"""Update the pose cost metric.
Only supports for the main end-effector. Does not support for multiple links that are
specified with `link_poses` in planning methods.
Args:
metric: Type and parameters for pose constraint to add.
start_state: Start joint state for the constraint.
goal_pose: Goal pose for the constraint.
Returns:
bool: True if the constraint can be added, False otherwise.
"""
if check_validity:
# check if constraint is valid:
if metric.hold_partial_pose and metric.offset_tstep_fraction < 0.0:
if start_state is None:
log_error("Need start state to hold partial pose")
if goal_pose is None:
log_error("Need goal pose to hold partial pose")
start_pose = self.compute_kinematics(start_state).ee_pose.clone()
if self.project_pose_to_goal_frame:
# project start pose to goal frame:
projected_pose = goal_pose.compute_local_pose(start_pose)
if torch.count_nonzero(metric.hold_vec_weight[:3] > 0.0) > 0:
# angular distance should be zero:
distance = projected_pose.angular_distance(
Pose.from_list([0, 0, 0, 1, 0, 0, 0], tensor_args=self.tensor_args)
)
if torch.max(distance) > 0.05:
log_warn(
"Partial orientation between start and goal is not equal"
+ str(distance)
)
return False
# check linear distance:
if (
torch.count_nonzero(
torch.abs(
projected_pose.position[..., metric.hold_vec_weight[3:] > 0.0]
)
> 0.005
)
> 0
):
log_warn("Partial position between start and goal is not equal.")
return False
else:
# project start pose to goal frame:
projected_position = goal_pose.position - start_pose.position
# check linear distance:
if (
torch.count_nonzero(
torch.abs(projected_position[..., metric.hold_vec_weight[3:] > 0.0])
> 0.005
)
> 0
):
log_warn("Partial position between start and goal is not equal.")
return False
rollout_list = []
for opt in self.solver.optimizers:
rollout_list.append(opt.rollout_fn)
rollout_list += [self.solver.safety_rollout, self.rollout_fn]
[
rollout.update_pose_cost_metric(metric)
for rollout in rollout_list
if isinstance(rollout, ArmReacher)
]
return True
def compute_kinematics(self, state: JointState) -> KinematicModelState:
"""Compute kinematics for a given joint state.
Args:
state: Joint state of the robot. Only :attr:`JointState.position` is used.
Returns:
KinematicModelState: Kinematic state of the robot.
"""
out = self.rollout_fn.compute_kinematics(state)
return out
@property
def joint_names(self):
"""Get the ordered joint names of the robot."""
@@ -624,6 +728,11 @@ class MpcSolver(MpcSolverConfig):
"""Get the world collision checker."""
return self.world_coll_checker
@property
def project_pose_to_goal_frame(self) -> bool:
"""Check if the pose cost metric is projected to goal frame."""
return self.rollout_fn.goal_cost.project_distance
def _step_once(
self,
current_state: JointState,