After mastering kinematics and dynamics in the previous post, the next question is: How does the robot move from point A to point B? This is the trajectory planning problem — and how you plan the trajectory determines whether the robot moves fast or slow, smooth or jerky, safe or dangerous.
There are two fundamental spaces for planning: Joint Space and Cartesian Space. Each has its own strengths and weaknesses, and choosing wrong can lead to collisions, singularities, or inefficient trajectories.
Joint Space vs Cartesian Space — Core Differences
Joint Space Planning
In Joint Space, the trajectory is planned directly on joint angles $q(t)$. The robot interpolates from initial joint configuration $q_0$ to the goal $q_f$.
Characteristics:
- TCP (Tool Center Point) path in Cartesian is unpredictable — can curve arbitrarily
- No singularity — IK is not needed during motion
- Fastest — each joint moves along the shortest path
- Joint limits guaranteed — easy to check since we work directly with joint angles
Cartesian Space Planning
In Cartesian Space, the trajectory is planned on the end-effector position and orientation in 3D space. The robot follows a straight line (or arc) in Cartesian coordinates.
Characteristics:
- TCP path is predictable — exact straight line or arc
- May encounter singularities — IK must be solved at every point along the trajectory
- Slower — constrained by the Cartesian path
- Essential for many applications — welding, painting, assembly all require straight lines
| Criterion | Joint Space | Cartesian Space |
|---|---|---|
| TCP Path | Unpredictable | Straight line/arc |
| Speed | Fastest | Constrained |
| Singularity | Never | Possible |
| Joint limits | Easy to ensure | May violate |
| Use when | Free movement | Path control needed |
Joint Space Trajectory — Point-to-Point
Linear Interpolation
The simplest approach — linear interpolation between $q_0$ and $q_f$:
q(s) = q_0 + s × (q_f - q_0), s ∈ [0, 1]
But linear interpolation is not smooth — velocity jumps abruptly at start and end.
Cubic Polynomial Interpolation
Add constraints for zero velocity at start and end:
q(t) = a₀ + a₁t + a₂t² + a₃t³
With 4 constraints:
- $q(0) = q_0$, $q(T) = q_f$
- $\dot{q}(0) = 0$, $\dot{q}(T) = 0$
import numpy as np
import matplotlib.pyplot as plt
def cubic_trajectory(q0, qf, T, dt=0.01):
"""Joint space cubic polynomial trajectory."""
a0 = q0
a1 = 0 # Zero initial velocity
a2 = 3 * (qf - q0) / T**2
a3 = -2 * (qf - q0) / T**3
t = np.arange(0, T + dt, dt)
q = a0 + a1*t + a2*t**2 + a3*t**3
qd = a1 + 2*a2*t + 3*a3*t**2
qdd = 2*a2 + 6*a3*t
return t, q, qd, qdd
# Trajectory for 1 joint: 0° to 90° in 2 seconds
T = 2.0
t, q, qd, qdd = cubic_trajectory(0, np.pi/2, T)
fig, axes = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
axes[0].plot(t, np.degrees(q), 'b-', linewidth=2)
axes[0].set_ylabel('Position (deg)')
axes[0].set_title('Cubic Polynomial Joint Trajectory')
axes[0].grid(True)
axes[1].plot(t, np.degrees(qd), 'r-', linewidth=2)
axes[1].set_ylabel('Velocity (deg/s)')
axes[1].grid(True)
axes[2].plot(t, np.degrees(qdd), 'g-', linewidth=2)
axes[2].set_ylabel('Acceleration (deg/s²)')
axes[2].set_xlabel('Time (s)')
axes[2].grid(True)
plt.tight_layout()
plt.savefig('cubic_trajectory.png', dpi=150)
plt.show()
Quintic Polynomial Interpolation
Add zero acceleration constraints at start and end — smoother than cubic:
def quintic_trajectory(q0, qf, T, dt=0.01):
"""Joint space quintic polynomial trajectory."""
a0 = q0
a1 = 0
a2 = 0
a3 = 10 * (qf - q0) / T**3
a4 = -15 * (qf - q0) / T**4
a5 = 6 * (qf - q0) / T**5
t = np.arange(0, T + dt, dt)
q = a0 + a1*t + a2*t**2 + a3*t**3 + a4*t**4 + a5*t**5
qd = a1 + 2*a2*t + 3*a3*t**2 + 4*a4*t**3 + 5*a5*t**4
qdd = 2*a2 + 6*a3*t + 12*a4*t**2 + 20*a5*t**3
return t, q, qd, qdd
Cartesian Space Trajectory — Straight Lines in 3D
Position Interpolation
Linear position interpolation in Cartesian is straightforward:
p(s) = p_0 + s × (p_f - p_0), s ∈ [0, 1]
But the complication lies in orientation — rotation matrices cannot be linearly interpolated.
SLERP — Spherical Linear Interpolation for Orientation
SLERP interpolates between two quaternions on the hypersphere surface, ensuring:
- Shortest rotation between two orientations
- Constant angular velocity
- Result is always a valid unit quaternion
q(s) = q_0 × sin((1-s)Ω) / sin(Ω) + q_f × sin(sΩ) / sin(Ω)
Where $\Omega = \arccos(q_0 \cdot q_f)$.
import numpy as np
from spatialmath import SE3, UnitQuaternion
import roboticstoolbox as rtb
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def cartesian_trajectory(T_start, T_end, n_points=100):
"""Linear Cartesian trajectory with SLERP orientation."""
positions = []
orientations = []
q_start = UnitQuaternion(T_start.R)
q_end = UnitQuaternion(T_end.R)
for i in range(n_points):
s = i / (n_points - 1)
pos = T_start.t + s * (T_end.t - T_start.t)
q = q_start.interp(q_end, s)
positions.append(pos)
orientations.append(q)
return np.array(positions), orientations
T_start = SE3(0.4, -0.2, 0.3) * SE3.Rz(0)
T_end = SE3(0.4, 0.3, 0.5) * SE3.Rz(np.pi/2)
positions, orientations = cartesian_trajectory(T_start, T_end)
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.plot(positions[:, 0], positions[:, 1], positions[:, 2],
'b-', linewidth=2, label='Cartesian path')
ax.scatter(*T_start.t, c='green', s=100, label='Start')
ax.scatter(*T_end.t, c='red', s=100, label='End')
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title('Cartesian Linear Trajectory')
ax.legend()
plt.savefig('cartesian_trajectory.png', dpi=150)
plt.show()
Visual Comparison: Joint vs Cartesian TCP Path
This is the most visible difference — same start and end points, but completely different TCP paths:
import roboticstoolbox as rtb
import numpy as np
import matplotlib.pyplot as plt
from spatialmath import SE3
ur5e = rtb.models.UR5()
q_start = np.array([0, -np.pi/3, np.pi/3, -np.pi/2, np.pi/2, 0])
q_end = np.array([np.pi/2, -np.pi/4, np.pi/4, -np.pi/3, np.pi/3, np.pi/4])
n_points = 100
# === JOINT SPACE trajectory ===
joint_positions = []
for i in range(n_points):
s = i / (n_points - 1)
q = q_start + s * (q_end - q_start)
T = ur5e.fkine(q)
joint_positions.append(T.t)
joint_positions = np.array(joint_positions)
# === CARTESIAN SPACE trajectory ===
T_start = ur5e.fkine(q_start)
T_end = ur5e.fkine(q_end)
cart_positions = []
for i in range(n_points):
s = i / (n_points - 1)
pos = T_start.t + s * (T_end.t - T_start.t)
cart_positions.append(pos)
cart_positions = np.array(cart_positions)
fig = plt.figure(figsize=(14, 6))
ax1 = fig.add_subplot(121, projection='3d')
ax1.plot(joint_positions[:, 0], joint_positions[:, 1], joint_positions[:, 2],
'b-', linewidth=2)
ax1.scatter(*joint_positions[0], c='green', s=100)
ax1.scatter(*joint_positions[-1], c='red', s=100)
ax1.set_title('Joint Space — TCP Path (curved)')
ax2 = fig.add_subplot(122, projection='3d')
ax2.plot(cart_positions[:, 0], cart_positions[:, 1], cart_positions[:, 2],
'r-', linewidth=2)
ax2.scatter(*cart_positions[0], c='green', s=100)
ax2.scatter(*cart_positions[-1], c='red', s=100)
ax2.set_title('Cartesian Space — TCP Path (straight)')
plt.tight_layout()
plt.savefig('joint_vs_cartesian.png', dpi=150)
plt.show()
Time Parameterization — Controlling Speed Along the Path
A geometric path only describes the shape of the trajectory. To execute it, you need time parameterization — mapping $s \in [0,1]$ to $t \in [0, T]$.
Common Time Parameterization Methods
| Method | Description | Use case |
|---|---|---|
| Linear | $s(t) = t/T$ | Constant velocity |
| Trapezoidal | Accel -> cruise -> decel | Most common |
| S-curve | Trapezoidal + bounded jerk | Smooth motion |
| Polynomial | $s(t)$ = 3rd/5th/7th order poly | Zero start/end velocity |
def trapezoidal_time_scaling(T, v_max, dt=0.01):
"""Trapezoidal velocity time scaling."""
t_acc = T - 1.0 / v_max
if t_acc < 0:
t_acc = T / 3
a = v_max / t_acc
t = np.arange(0, T + dt, dt)
s = np.zeros_like(t)
sd = np.zeros_like(t)
for i, ti in enumerate(t):
if ti < t_acc:
s[i] = 0.5 * a * ti**2
sd[i] = a * ti
elif ti < T - t_acc:
s[i] = 0.5 * a * t_acc**2 + v_max * (ti - t_acc)
sd[i] = v_max
else:
t_rem = T - ti
s[i] = 1.0 - 0.5 * a * t_rem**2
sd[i] = a * t_rem
return t, s, sd
Waypoint Planning — Multiple Intermediate Points
In practice, robots rarely move between just 2 points. Waypoint planning plans through multiple intermediate points:
import roboticstoolbox as rtb
import numpy as np
ur5e = rtb.models.UR5()
# Define 5 waypoints (joint configurations)
waypoints = np.array([
[0, -np.pi/3, np.pi/3, -np.pi/2, np.pi/2, 0],
[np.pi/4, -np.pi/4, np.pi/4, -np.pi/3, np.pi/3, np.pi/6],
[np.pi/2, -np.pi/6, np.pi/6, -np.pi/4, np.pi/4, np.pi/4],
[np.pi/4, -np.pi/3, np.pi/2, -np.pi/2, np.pi/2, np.pi/6],
[0, -np.pi/4, np.pi/3, -np.pi/3, np.pi/3, 0],
])
traj = rtb.mstraj(waypoints, dt=0.05, tacc=0.5, qdmax=[2.0]*6)
print(f"Trajectory duration: {traj.t[-1]:.2f}s")
print(f"Number of points: {len(traj.t)}")
tcp_positions = []
for q in traj.q:
T = ur5e.fkine(q)
tcp_positions.append(T.t)
tcp_positions = np.array(tcp_positions)
When to Use Which?
| Scenario | Choice | Reason |
|---|---|---|
| Fast free movement between distant points | Joint Space | Fastest, no singularity |
| Straight-line welding | Cartesian | Need precise straight line |
| Surface spray painting | Cartesian | Need uniform distance |
| Obstacle avoidance | Depends on planner | Use motion planner (RRT, PRM) |
| Pick-and-place (approach) | Joint Space | Free movement to vicinity |
| Pick-and-place (grasp) | Cartesian | Vertical approach needed |
Handling Singularity in Cartesian Planning
When planning in Cartesian space, singularity is the biggest challenge. The most common solution:
Damped Least Squares (DLS)
Instead of $\dot{q} = J^{-1}v$, use:
q̇ = J^T (JJ^T + λ²I)^{-1} v
The damping parameter $\lambda$ prevents joint velocity blow-up near singularities.
def damped_least_squares(J, v, damping=0.01):
"""Compute joint velocities using Damped Least Squares."""
JJT = J @ J.T
identity = np.eye(JJT.shape[0])
q_dot = J.T @ np.linalg.solve(JJT + damping**2 * identity, v)
return q_dot
References
- Gasparetto, A., Boscariol, P., Lanzutti, A., Vidoni, R. "Path Planning and Trajectory Planning Algorithms: A General Overview." Motion and Operation Planning of Robotic Systems, Springer, 2015. DOI: 10.1007/978-3-319-14705-5_1
- Siciliano, B. et al. Robotics: Modelling, Planning and Control. Springer, 2009. DOI: 10.1007/978-1-84628-642-1
- Corke, P. Robotics, Vision and Control (3rd ed.). Springer, 2023. DOI: 10.1007/978-3-031-07262-8
Conclusion
In this post, we analyzed the two fundamental trajectory planning approaches:
- Joint Space — fast, safe, but no TCP path control
- Cartesian Space — precise TCP path but faces singularity issues
- SLERP — accurate orientation interpolation on quaternions
- Time parameterization — converting geometric paths into executable trajectories
In the next post, we will dive into MoveJ and MoveL — the two most fundamental commands in industrial robot programming, which are the practical realization of Joint Space and Cartesian Space planning.
Related Posts
- Robot Arm Fundamentals: Kinematics & Dynamics — Mathematical foundation for trajectory planning
- PID Control for Robots — Joint position and velocity control
- Top 5 Cobot Comparison — Comparing popular collaborative robots