Kinematics Tutorial#

Prerequisites#

The following tutorials are recommended prior to starting this tutorial:

Objectives#

Through this tutorial, you will learn how to:

  1. Check for frames defined in the kinematics,

  2. Query the position and orientation of a frame given the c-space coordinates (i.e. forward kinematics)

What are Kinematics?#

Robot Kinematics#

Here’s a useful definition:

The branch of mechanics concerned with the motion of objects without reference to the forces which cause the motion.

For a robot, this is the mathematical relationship between the c-space positions (which correspond to the joint angles for a traditional serial-link manipulator with revolute joints) and the position and orientation (pose) of each link of the robot. Typically we care about the pose of a frame attached to the last link, or the “tool” of the robot. We refer to this frame as the tool frame.

Forward kinematics refers to the well-defined mapping from c-space coordinates to the tool frame pose. For a given set of c-space positions, there is always a single tool frame pose.

Inverse kinematics is the mapping from tool frame pose to the c-space positions. This is a much harder problem, since there are generally many (often infinite) c-space positions that may generate the same tool frame pose. Alternatively, there may not be any valid c-space positions that produce a given tool frame pose, such as when:

  • The pose is outside the workspace of the robot, due to being too far away

  • The pose cannot be reached due to joint limits

  • The robot does not have enough degrees of freedom (e.g. a 3D gantry robot has a fixed orientation, and therefore cannot achieve an arbitrary tool frame orientation)

  • The tool frame would result in self-collision (e.g. the desired tool frame is in the middle of the base of the robot).

This tutorial is focused on the utilities provided by cuMotion for forward kinematics. See Inverse Kinematics Tutorial to learn more about using cuMotion for solving the inverse kinematics.

Kinematics in cuMotion#

For computational and representational efficiency, cuMotion assumes the kinematics of a robot can be represented by a directional tree graph structure. This means:

  • There is a single “base” link

  • Each link has a single parent

  • A parent can have any number of children

C-space Coordinates#

Configuration space (or c-space) is the mathematical space defined by the span of the minimal set of coordinates required to describe the forward kinematics of the robot. They are a generalization of “joint positions,” since the motion of physical joints may not, in general, be completely independent. This may be a result of physical linkages, or a choice by the manufacturer that is implemented in software (such as when both sides of a gripper are controlled using a single position command, even when they are in fact driven by independent motors).

We start by loading a robot description and using it to create the kinematics for the robot. See Robot Description Tutorial for more details.

std::unique_ptr<cumotion::RobotDescription> robot_description =
    cumotion::LoadRobotFromFile(xrdf_file, urdf_file);
std::unique_ptr<cumotion::Kinematics> kinematics = robot_description->kinematics();
robot_description: cumotion.RobotDescription = cumotion.load_robot_from_file(xrdf_path, urdf_path)
kinematics: cumotion.Kinematics = robot_description.kinematics()

These functions create a new instance of the robot kinematics each time it is called. It is highly recommended that the user call this function once and save the result.

C-space Coordinate Names#

Each c-space coordinate is given a descriptive name, which is useful for debugging and analysis. Just like the robot description, the name for a given coordinate can be queried from the kinematics:

const int num_coords = kinematics->numCSpaceCoords();
std::vector<std::string> coord_names(num_coords);
for (int coord_index = 0; coord_index < num_coords; ++coord_index) {
  coord_names[coord_index] = robot_description->cSpaceCoordName(coord_index);
}
std::cout << "Number of c-space coordinates: " << num_coords << "\n";
for (int coord_index = 0; coord_index < num_coords; ++coord_index) {
  std::cout << "Coordinate " << coord_index << " name: " << coord_names[coord_index] << "\n";
}
num_coords = kinematics.num_cspace_coords()
print("Number of c-space coordinates:", num_coords)
for coord_index in range(num_coords):
    coord_name = kinematics.cspace_coord_name(coord_index)
    print("Coordinate {} name: {}".format(coord_index, coord_name))

Output:

Number of c-space coordinates: 7
Coordinate 0 name: panda_joint1
Coordinate 1 name: panda_joint2
Coordinate 2 name: panda_joint3
Coordinate 3 name: panda_joint4
Coordinate 4 name: panda_joint5
Coordinate 5 name: panda_joint6
Coordinate 6 name: panda_joint7

C-space Limits#

Once we have the robot kinematics, we can query the limits on the c-space positions, velocities, accelerations, and jerks:

Eigen::VectorXd position_lower_limits(num_coords);
Eigen::VectorXd position_upper_limits(num_coords);
for (int coord_index = 0; coord_index < num_coords; ++coord_index) {
  auto [lower, upper] = kinematics->cSpaceCoordLimits(coord_index);
  position_lower_limits[coord_index] = lower;
  position_upper_limits[coord_index] = upper;
}

std::cout << "\nC-space limits:\n";
for (int coord_index = 0; coord_index < num_coords; ++coord_index) {
  std::cout << "Coordinate " << coord_index << " limits: [" << position_lower_limits[coord_index]
            << ", " << position_upper_limits[coord_index] << "]\n";
}
print("\nC-space limits:")
for coord_index in range(num_coords):
    limits = kinematics.cspace_coord_limits(coord_index)
    print("Coordinate {} limits: [{}, {}]".format(coord_index, limits.lower, limits.upper))

Output:

C-space limits:
Coordinate 0 limits: [-2.8973, 2.8973]
Coordinate 1 limits: [-1.7628, 1.7628]
Coordinate 2 limits: [-2.8973, 2.8973]
Coordinate 3 limits: [-3.0718, -0.0698]
Coordinate 4 limits: [-2.8973, 2.8973]
Coordinate 5 limits: [-0.0175, 3.7525]
Coordinate 6 limits: [-2.8973, 2.8973]

It’s often useful to know whether a given c-space position is within the limits, which can be done with:

Eigen::VectorXd test_position = position_upper_limits * 1.1;
bool within_limits = kinematics->withinCSpaceLimits(test_position, true);
std::cout << "\nTest position within limits: " << (within_limits ? "true" : "false") << "\n";
test_position = np.zeros(num_coords)
within_limits = kinematics.within_cspace_limits(test_position, True)
print("\033[0mTest position within limits:", within_limits)

which will produce output like the following if log_warnings is true and the c-space position violates any limits:

C-space position [3.18703, 1.93908, 3.18703, -0.07678, 3.18703, 4.12775, 3.18703] exceeds limits:
  - Coordinate "panda_joint1" [index: 0, value: 3.18703] exceeds limits: (-2.8973, 2.8973).
  - Coordinate "panda_joint2" [index: 1, value: 1.93908] exceeds limits: (-1.7628, 1.7628).
  - Coordinate "panda_joint3" [index: 2, value: 3.18703] exceeds limits: (-2.8973, 2.8973).
  - Coordinate "panda_joint5" [index: 4, value: 3.18703] exceeds limits: (-2.8973, 2.8973).
  - Coordinate "panda_joint6" [index: 5, value: 4.12775] exceeds limits: (-0.0175, 3.7525).
  - Coordinate "panda_joint7" [index: 6, value: 3.18703] exceeds limits: (-2.8973, 2.8973).

Velocity, Acceleration, and Jerk Limits#

In addition to position limits, we can also query the velocity, acceleration, and jerk limits for each c-space coordinate:

Eigen::VectorXd velocity_limits(num_coords);
Eigen::VectorXd acceleration_limits(num_coords);
Eigen::VectorXd jerk_limits(num_coords);
for (int coord_index = 0; coord_index < num_coords; ++coord_index) {
  velocity_limits[coord_index] = kinematics->cSpaceCoordVelocityLimit(coord_index);
  acceleration_limits[coord_index] = kinematics->cSpaceCoordAccelerationLimit(coord_index);
  jerk_limits[coord_index] = kinematics->cSpaceCoordJerkLimit(coord_index);
}

std::cout << "\nVelocity limits:\n";
for (int coord_index = 0; coord_index < num_coords; ++coord_index) {
  std::cout << "Coordinate " << coord_index << ": " << velocity_limits[coord_index] << "\n";
}

std::cout << "\nAcceleration limits:\n";
for (int coord_index = 0; coord_index < num_coords; ++coord_index) {
  std::cout << "Coordinate " << coord_index << ": " << acceleration_limits[coord_index] << "\n";
}

std::cout << "\nJerk limits:\n";
for (int coord_index = 0; coord_index < num_coords; ++coord_index) {
  std::cout << "Coordinate " << coord_index << ": " << jerk_limits[coord_index] << "\n";
}
print("\nVelocity limits:")
for coord_index in range(num_coords):
    vel_limit = kinematics.cspace_coord_velocity_limit(coord_index)
    print("Coordinate {} velocity limit: {}".format(coord_index, vel_limit))

print("\nAcceleration limits:")
for coord_index in range(num_coords):
    acc_limit = kinematics.cspace_coord_acceleration_limit(coord_index)
    print("Coordinate {} acceleration limit: {}".format(coord_index, acc_limit))

print("\nJerk limits:")
for coord_index in range(num_coords):
    jerk_limit = kinematics.cspace_coord_jerk_limit(coord_index)
    print("Coordinate {} jerk limit: {}".format(coord_index, jerk_limit))

Output:

Velocity limits:
Coordinate 0 velocity limit: 2.175
Coordinate 1 velocity limit: 2.175
Coordinate 2 velocity limit: 2.175
Coordinate 3 velocity limit: 2.175
Coordinate 4 velocity limit: 2.61
Coordinate 5 velocity limit: 2.61
Coordinate 6 velocity limit: 2.61

Acceleration limits:
Coordinate 0 acceleration limit: 15.0
Coordinate 1 acceleration limit: 7.5
Coordinate 2 acceleration limit: 10.0
Coordinate 3 acceleration limit: 12.5
Coordinate 4 acceleration limit: 15.0
Coordinate 5 acceleration limit: 20.0
Coordinate 6 acceleration limit: 20.0

Jerk limits:
Coordinate 0 jerk limit: 7500.0
Coordinate 1 jerk limit: 3750.0
Coordinate 2 jerk limit: 5000.0
Coordinate 3 jerk limit: 6250.0
Coordinate 4 jerk limit: 7500.0
Coordinate 5 jerk limit: 10000.0
Coordinate 6 jerk limit: 10000.0

Coordinate Frames#

The kinematics define multiple coordinate frames, which are all rigidly attached to a particular link of the robot. The kinematics (both forward and inverse) are defined between two frames, which are typically the base frame (a frame rigidly attached to the environment) and the tool frame.

To get a list of all the frame names in the robot, use Kinematics::frameNames():

std::vector<std::string> frame_names = kinematics->frameNames();
std::cout << "\nFrame names:\n";
for (const auto &frame_name : frame_names) {
  std::cout << "  " << frame_name << "\n";
}
frame_names = kinematics.frame_names()
print("\nFrame names:")
for frame_name in frame_names:
    print("  {}".format(frame_name))

Output:

Frame names:
  base_link
  panda_link0
  panda_link1
  panda_link2
  panda_link3
  panda_link4
  panda_forearm_end_pt
  panda_forearm_mid_pt
  panda_link5
  panda_forearm_distal
  panda_forearm_mid_pt_shifted
  panda_link6
  panda_link7
  panda_link8
  panda_hand
  camera_bottom_screw_frame
  camera_link
  camera_depth_frame
  camera_color_frame
  camera_color_optical_frame
  camera_depth_optical_frame
  camera_left_ir_frame
  camera_left_ir_optical_frame
  camera_right_ir_frame
  camera_right_ir_optical_frame
  panda_face_back_left
  panda_face_back_right
  panda_face_left
  panda_face_right
  panda_leftfinger
  panda_leftfingertip
  panda_rightfinger
  panda_rightfingertip
  right_gripper
  panda_wrist_end_pt

The base frame (the root of the kinematic tree structure), is given by Kinematics::baseFrame():

std::string base_frame_name = kinematics->frameName(kinematics->baseFrame());
std::cout << "\nBase frame: " << base_frame_name << "\n";
base_frame_name = kinematics.base_frame_name()
print("\nBase frame: {}".format(base_frame_name))

Output:

Base frame: base_link

Note

What is a FrameHandle? In the C++ API, most kinematics functions require a FrameHandle, instead of a std::string. To avoid the overhead of string processing and lookup, the user is expected to create the FrameHandle for the frame of interest once, and then re-use it.

Do NOT attempt to use a FrameHandle created from a difference instance of the kinematics. This will result in silent errors. For this reason, it is strongly recommended that the user create a single instance of the kinematics (which can be moved to a std::shared_ptr, if needed) and create a FrameHandle only when it can be guaranteed that the handle will always be used with the kinematics from which it was created.

Forward Kinematics#

Once we know the name of the frame we want to work with, we can use Kinematics::position(), Kinematics::orientation(), and Kinematics::pose() to compute the forward kinematics of the given frame, with respect to the baseFrame().

Eigen::VectorXd cspace_position = robot_description->defaultCSpaceConfiguration();
std::string tool_frame_name = robot_description->toolFrameNames().front();
cumotion::Kinematics::FrameHandle tool_frame = kinematics->frame(tool_frame_name);
cumotion::Kinematics::FrameHandle base_frame = kinematics->baseFrame();

cumotion::Pose3 tool_pose = kinematics->pose(cspace_position, tool_frame);
cumotion::Rotation3 tool_orientation = kinematics->orientation(cspace_position, tool_frame);
Eigen::Vector3d tool_position = kinematics->position(cspace_position, tool_frame);

std::cout << "\nTool frame pose (position): [" << tool_pose.translation.transpose() << "]\n";
std::cout << "Tool frame pose (orientation quaternion): [" << tool_pose.rotation.w() << ", "
          << tool_pose.rotation.x() << ", " << tool_pose.rotation.y() << ", "
          << tool_pose.rotation.z() << "]\n";
cspace_position = robot_description.default_cspace_configuration()
tool_frame_name = robot_description.tool_frame_names()[0]

tool_pose: cumotion.Pose3 = kinematics.pose(cspace_position, tool_frame_name)
tool_position = kinematics.position(cspace_position, tool_frame_name)
tool_orientation: cumotion.Rotation3 = kinematics.orientation(cspace_position, tool_frame_name)

print("\nTool frame pose (position): {}".format(tool_pose.translation))
print("Tool frame pose (orientation quaternion): [{}, {}, {}, {}]".format(
    tool_pose.rotation.w, tool_pose.rotation.x,
    tool_pose.rotation.y, tool_pose.rotation.z))

Output:

Tool frame pose (position): [  0.270021 -0.0249843   0.425636]
Tool frame pose (orientation quaternion): [0.213314, -0.0172907, 0.976823, -0.00377586]

Alternatively, if we want to compute the forward kinematics with respect to a different frame, we can pass in the reference frame as the last argument:

cumotion::Kinematics::FrameHandle link1_frame = kinematics->frame(frame_names[2]);
tool_pose = kinematics->pose(cspace_position, tool_frame, link1_frame);
tool_orientation = kinematics->orientation(cspace_position, tool_frame, link1_frame);
tool_position = kinematics->position(cspace_position, tool_frame, link1_frame);

std::cout << "\nTool frame pose (position): [" << tool_pose.translation.transpose() << "]\n";
std::cout << "Tool frame pose (orientation quaternion): [" << tool_pose.rotation.w() << ", "
          << tool_pose.rotation.x() << ", " << tool_pose.rotation.y() << ", "
          << tool_pose.rotation.z() << "]\n";
link1_name = kinematics.frame_names()[2]
tool_pose = kinematics.pose(cspace_position, tool_frame_name, link1_name)
tool_position = kinematics.position(cspace_position, tool_frame_name, link1_name)
tool_orientation = kinematics.orientation(cspace_position, tool_frame_name, link1_name)

print("\nTool position: {}".format(tool_position))
print("Tool orientation (quaternion): [{}, {}, {}, {}]".format(
    tool_orientation.w, tool_orientation.x,
    tool_orientation.y, tool_orientation.z))

Output:

Tool frame pose (position): [  0.270021 -0.0249843   0.092636]
Tool frame pose (orientation quaternion): [0.213314, -0.0172907, 0.976823, -0.00377586]

Kinematic Jacobians#

The kinematic Jacobian is the derivative of a frame’s position and orientation, with respect to the c-space positions. In other words, it tells us how much the frame will move in each direction when we move a given joint. There are many use-cases for these Jacobians; however, here we will simply demonstrate how to compute them.

The position Jacobian returns a 3 x N Eigen matrix, where N is the number of c-space coordinates (cumotion::Kinematics::numCSpaceCoords()), where column i of the Jacobian tells us how much the frame will translate in the world frame when we change coordinate i:

Eigen::Matrix<double, 3, Eigen::Dynamic> position_jacobian =
    kinematics->positionJacobian(cspace_position, tool_frame);
std::cout << "Position Jacobian shape: " << position_jacobian.rows() << " x "
          << position_jacobian.cols() << "\n";
position_jacobian = kinematics.position_jacobian(cspace_position, tool_frame_name)
print("Position Jacobian shape: {} x {}".format(position_jacobian.shape[0],
                                                position_jacobian.shape[1]))

Output:

Position Jacobian shape: 3 x 7

The orientation Jacobian also returns a 3 x N Eigen matrix Here column i of the Jacobian tells us how much the frame will rotate about the given axis in the world frame when we change coordinate i:

Eigen::Matrix<double, 3, Eigen::Dynamic> orientation_jacobian =
    kinematics->orientationJacobian(cspace_position, tool_frame);
std::cout << "Orientation Jacobian shape: " << orientation_jacobian.rows() << " x "
          << orientation_jacobian.cols() << "\n";
orientation_jacobian = kinematics.orientation_jacobian(cspace_position, tool_frame_name)
print("Orientation Jacobian shape: {} x {}".format(orientation_jacobian.shape[0],
                                                   orientation_jacobian.shape[1]))

Output:

Orientation Jacobian shape: 3 x 7

To compute both at the same time, use cumotion::Kinematics::jacobian(), which stacks the position Jacobian on top of the orientation Jacobian:

Eigen::Matrix<double, 6, Eigen::Dynamic> full_jacobian =
    kinematics->jacobian(cspace_position, tool_frame);
std::cout << "\nFull Jacobian shape: " << full_jacobian.rows() << " x " << full_jacobian.cols()
          << "\n";
full_jacobian = kinematics.jacobian(cspace_position, tool_frame_name)
print("\nFull Jacobian shape: {} x {}".format(full_jacobian.shape[0], full_jacobian.shape[1]))

Output:

Full Jacobian shape: 6 x 7