Motion Planning#
The Planner
is the core of the Jacobi library and calculates the actual trajectories. We also introduce the motion interface using the Motion
class for complex use-cases, e.g. to change robot parameters such as joint limits or end-effector obstacles for each motion easily.
Waypoints#
Waypoints - both in joint as well as Cartesian space - are the foundation for specifying robot motions. Waypoints are usually used to define a position
, but they hold information about the velocity
and acceleration
as well. For now, we want to plan a motion from a Cartesian waypoint (e.g. given by a grasp planner) to a fixed joint position:
start = CartesianWaypoint(Frame(x=0.5, z=0.75)) # [m]
goal = Waypoint([0.5, 0.2, 0.3, 0.4, 0.5, 0.1]) # [rad]
auto start = CartesianWaypoint(Frame::from_translation(0.5, 0.0, 0.75)); // [m]
auto goal = Waypoint {{0.5, 0.2, 0.3, 0.4, 0.5, 0.1}}; // [rad]
In general, we can pass either joint-space Waypoint
, a CartesianWaypoint
, a joint-space position Config
, or a Frame
position as start and goal points. The planner allows to mix Cartesian and joint spaces freely, however we recommend to let the planner calculate the inverse kinematics by itself so that Jacobi can optimize for the best possible joint position.
Query the Planner#
Collision-free Motions#
We construct the main Planner
object that takes in the environment and the cycle time of the system (called delta_time
). The cycle time is going to be the sampling rate for the computed trajectories, and it makes sense to use the control rate of the robot here.
planner = Planner(environment, delta_time=0.002) # [s]
Planner planner {environment, 0.002}; // delta_time in [s]
The planner can also take the robot as input directly if you want to plan motions without environment obstacles.
Now, we can actually query the planner and calculate highly time-optimized, jerk-limited, and collision-free trajectories. The simplest way to get started is to construct a planner to pass a start and goal to the planner:
# Start and goal can either be:
# 1. a Waypoint in joint space,
# 2. a CartesianWaypoint, or
# 3. a Config (as a List[float] of the joint position)
trajectory = planner.plan(start, goal)
// Start and goal can either be:
// 1. a Waypoint in joint space,
// 2. a CartesianWaypoint, or
// 3. a Config (as a std::vector<double> of the joint position)
auto trajectory = planner.plan(start, goal);
Motions along Waypoints#
A common use-case is to plan a motion along a given list of waypoints. Here, Jacobi does not check for collision but assumes that the waypoints will result in a collision-free trajectory.
# In case the start and goal are known exactly
waypoints = [
[ 0.20567, 0.34335, 0.26038, 0.84611, -0.34577, -0.60799],
[ 0.09326, -0.14990, 0.02635, 0.25723, 0.24147, -0.40426],
[-0.07329, 0.27970, 0.10302, 0.37514, -0.06689, 0.20232],
[ 0.19213, -0.25143, 0.25726, -0.01240, 0.45425, -0.13753]
]
trajectory = planner.plan(waypoints)
std::vector<Config> waypoints = {
{ 0.20567, 0.34335, 0.26038, 0.84611, -0.34577, -0.60799},
{ 0.09326, -0.14990, 0.02635, 0.25723, 0.24147, -0.40426},
{-0.07329, 0.27970, 0.10302, 0.37514, -0.06689, 0.20232},
{ 0.19213, -0.25143, 0.25726, -0.01240, 0.45425, -0.13753}
};
auto trajectory = planner.plan(waypoints);
Jacobi works best for a smaller number of waypoints. Then, it will pass exactly through the waypoints. For a larger number of waypoints (more than 20), Jacobi might interpolate the waypoints in a way that the resulting trajectory will stay close, however not reaching the waypoints exactly.
Motion Interface#
More complex motions can be planned using the motion interface. First, we construct a Motion
with a unique name, the robot that should execute the motion, and a start and goal waypoint:
grasp_to_place_motion = Motion('grasp-to-place', robot,
start=grasp_waypoint,
goal=place_waypoint,
)
grasp_to_place_motion.robot.max_acceleration = [5.0, 5.0, 6.0, 10.0, 10.0, 12.0] # [rad/s^2]
grasp_to_place_motion.robot.max_jerk = [50.0, 50.0, 50.0, 100.0, 100.0, 100.0] # [rad/s^3]
Motion grasp_to_place_motion {"grasp-to-place", robot,
grasp_waypoint,
place_waypoint
};
grasp_to_place_motion.robot.max_acceleration = {5.0, 5.0, 6.0, 10.0, 10.0, 12.0}; // [rad/s^2]
grasp_to_place_motion.robot.max_jerk = {50.0, 50.0, 50.0, 100.0, 100.0, 100.0}; // [rad/s^3]
Note that a copy of the robot instance is made, and thus you can change each parameter of the motion without changing the original robot afterwards. This way, you can keep default settings for all motions (or even make a copy of the robot yourself for really complex setups). In this example, we want to reduce the maximum acceleration so that the robot wonβt loose the object during the motion.
Before using the motion interface for planning, we need to add all defined motions to the planner via
planner.add_motion(grasp_to_place_motion)
planner.add_motion(grasp_to_place_motion);
Then, we can plan a motion given its name only and, in case the start or goal were not specified, the exact start and goal waypoints.
# In case the start and goal are known exactly
trajectory = planner.plan('home-to-bin')
# In case the goal is a region
trajectory = planner.plan('home-to-bin', goal=goal)
// In case the start and goal are known exactly
auto trajectory = planner.plan("home-to-bin");
// In case the goal is a region
auto trajectory = planner.plan("home-to-bin", std::nullopt, goal);
Linear Approach & Retraction#
Oftentimes, the robot should approach a waypoint along a linear motion in Cartesian space. For this, a motion can take a relative transformation (given in the end-effector frame) at both the start and goal waypoint via
grasp_to_place_motion.linear_cartesian_start = Frame(z=-0.1) # [m]
grasp_to_place_motion.linear_cartesian_start = Frame::from_translation(0, 0, -0.1); // [m]
e.g. to approach the grasp point with a 10cm linear motion in this example. Of course, Jacobi will optimize the complete motion end-to-end and does not stop at the intermediate waypoint.
Soft Collision Checking#
Sometimes, the start or goal pose might be in collision as the collision geometries are usually bounding volumes. By default, Jacobi returns an error if a motion would be requested then. Jacobi supports soft collision checking to calculate a motion that leaves the state in collision as fast as possible without moving into a collision again.
motion.soft_collision_start = True
motion.soft_collision_goal = False
motion.linear_cartesian_start = true;
motion.linear_cartesian_goal = false;
End-Effector Orientation#
Jaocbi supports a soft constraint on the end-effector (TCP) orientation of the robot through the motion. Precesily, it will minimize the maximum deviation from a target orientation, and does this jointly with the time minimization. The orientation_loss_weight
allows to weigh the orientation loss in comparison to the trajectory duration.
motion.orientation_loss_weight = 0.5
motion.orientation_target = [0, 0, 0.1] # Vector in world frame
motion.orientation_loss_weight = 0.5;
motion.orientation_target = {0, 0, 0.1}; // Vector in world frame
End-Effector Forces#
Jacobi allows to limit the Cartesian acceleration on the end-effector. Please let us know if you are interested in this feature!
Trajectory#
The Trajectory
class includes all information about the calculated joint positions, velocities, accelerations and their timings. To loop over the positons with their respective timings, you can
print('Exact trajectory duration:', trajectory.duration, '[s]')
for t, p in zip(trajectory.times, trajectory.positions):
print(t, p)
std::cout << "Exact trajectory duration: " << trajectory.duration << " [s]" << std::endl;
for (size_t i = 0; i < trajectory.size(); ++i) {
auto& p = trajectory.positions[i];
std::cout << trajectory.times[i] << " " << p[0] << std::endl; // ... p[1] ...
}
You can access the velocities
and accelerations
at the discrete time steps similarly. The sample rate of the trajectory is given by the delta_time
argument of the planner. Note that the last sampling duration might be a bit shorter due to the overall trajectory duration.
We have methods to append
to or slice
from a trajectory, as well as to filter a path of sparse waypoints within a given distance of the original trajectory.
Most importantly, you can pass a Trajectory
directly to our robot drivers to run the motion on a real robot.