In the previous post, we compared Joint Space and Cartesian Space planning at a theoretical level. Now it is time for practice — MoveJ and MoveL are the two most fundamental commands that every robot engineer must master. They are the direct implementation of Joint Space and Cartesian Space trajectories.
In this post, we will dive deep into how they work, their parameters, limitations, and demonstrate with URScript and Python.
MoveJ — Joint Interpolation Motion
How It Works
MoveJ interpolates in Joint Space. The controller computes a separate trajectory for each joint:
- Compute angle difference: $\Delta q_i = q_{target,i} - q_{current,i}$
- Identify the joint requiring the longest time (the "bottleneck" joint)
- Synchronize all joints to finish simultaneously
- Apply velocity profile (typically trapezoidal or S-curve)
Result: Each joint moves smoothly, but the TCP path in Cartesian space is curved and unpredictable.
When to Use MoveJ
| Scenario | Reason |
|---|---|
| Moving between tasks (approach) | Fastest, saves cycle time |
| Moving to home position | No straight line needed |
| Clearance moves around obstacles | Speed is priority |
| Initial power-on movement | Safe, avoids singularity |
URScript MoveJ
# URScript — runs on UR controller
# movej(q, a=1.4, v=1.05, t=0, r=0)
# q: joint angles [rad]
# a: acceleration [rad/s²]
# v: velocity [rad/s]
# t: time [s] (0 = auto-compute)
# r: blend radius [m] (0 = stop at point)
# Move to home position
movej([0, -1.5708, 1.5708, -1.5708, -1.5708, 0], a=1.4, v=1.05)
# Slow, safe movement
movej(get_inverse_kin(p[0.4, 0.3, 0.2, 0, 3.14, 0]),
a=0.5, v=0.3)
# Fast movement with blend radius (no stopping at waypoint)
movej(q_waypoint1, a=1.4, v=1.05, r=0.05)
movej(q_waypoint2, a=1.4, v=1.05, r=0.05)
movej(q_final, a=1.4, v=1.05, r=0) # Stop at final point
Python with roboticstoolbox
import roboticstoolbox as rtb
import numpy as np
import matplotlib.pyplot as plt
ur5e = rtb.models.UR5()
# Start and target configurations
q_start = np.array([0, -np.pi/3, np.pi/3, -np.pi/2, np.pi/2, 0])
q_target = np.array([np.pi/2, -np.pi/4, np.pi/4, -np.pi/3, np.pi/3, np.pi/4])
# MoveJ: Joint space trajectory (quintic polynomial)
traj = rtb.jtraj(q_start, q_target, t=100)
# Compute TCP positions along trajectory
tcp_positions = []
for q in traj.q:
T = ur5e.fkine(q)
tcp_positions.append(T.t)
tcp_positions = np.array(tcp_positions)
# Visualize TCP path
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.plot(tcp_positions[:, 0], tcp_positions[:, 1], tcp_positions[:, 2],
'b-', linewidth=2, label='MoveJ TCP path')
ax.scatter(*tcp_positions[0], c='green', s=100, zorder=5, label='Start')
ax.scatter(*tcp_positions[-1], c='red', s=100, zorder=5, label='Target')
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title('MoveJ — TCP Path (curved, unpredictable)')
ax.legend()
plt.tight_layout()
plt.savefig('movej_tcp_path.png', dpi=150)
plt.show()
# Plot joint trajectories
fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)
for i in range(6):
axes[0].plot(np.degrees(traj.q[:, i]), label=f'Joint {i+1}')
axes[1].plot(np.degrees(traj.qd[:, i]), label=f'Joint {i+1}')
axes[2].plot(np.degrees(traj.qdd[:, i]), label=f'Joint {i+1}')
axes[0].set_ylabel('Position (deg)')
axes[0].set_title('MoveJ Joint Profiles')
axes[0].legend(ncol=3)
axes[0].grid(True)
axes[1].set_ylabel('Velocity (deg/s)')
axes[1].grid(True)
axes[2].set_ylabel('Acceleration (deg/s²)')
axes[2].set_xlabel('Time step')
axes[2].grid(True)
plt.tight_layout()
plt.savefig('movej_joint_profiles.png', dpi=150)
plt.show()
MoveL — Linear Interpolation Motion
How It Works
MoveL interpolates in Cartesian space. The controller:
- Computes a straight line in Cartesian between current TCP and target
- Divides into many small interpolation points
- At each point: solves IK to find joint angles
- Applies velocity profile on Cartesian distance
Result: TCP follows a precise straight line in 3D, but joints may move in complex patterns.
When to Use MoveL
| Scenario | Reason |
|---|---|
| Straight-line welding | Need precise weld path |
| Object approach for grasping | Vertical approach, no collision |
| Flat surface spray painting | Uniform distance from surface |
| Assembly (insertion) | Need straight, precise motion |
| Object retreat | Straight up, avoid collision |
URScript MoveL
# URScript
# movel(pose, a=1.2, v=0.25, t=0, r=0)
# pose: target TCP pose [x, y, z, rx, ry, rz] (axis-angle)
# a: acceleration [m/s²]
# v: velocity [m/s]
# t: time [s] (0 = auto)
# r: blend radius [m]
# Straight-line move to pick position
movel(p[0.4, 0.3, 0.1, 0, 3.14, 0], a=1.2, v=0.1)
# Slow approach (insertion)
movel(p[0.4, 0.3, 0.05, 0, 3.14, 0], a=0.5, v=0.02)
# Retreat upward — faster
movel(p[0.4, 0.3, 0.2, 0, 3.14, 0], a=1.2, v=0.25)
Python Implementation
import roboticstoolbox as rtb
import numpy as np
import matplotlib.pyplot as plt
from spatialmath import SE3
ur5e = rtb.models.UR5()
# Start configuration
q_start = np.array([0, -np.pi/3, np.pi/3, -np.pi/2, np.pi/2, 0])
T_start = ur5e.fkine(q_start)
# Target pose (Cartesian)
T_target = SE3(0.3, 0.4, 0.2) * SE3.Rx(np.pi)
# MoveL: Cartesian straight line
n_points = 100
tcp_positions = []
joint_traj = []
for i in range(n_points):
s = i / (n_points - 1)
T_interp = T_start.interp(T_target, s)
sol = ur5e.ikine_LM(T_interp, q0=q_start if i == 0 else joint_traj[-1])
if sol.success:
joint_traj.append(sol.q)
T_actual = ur5e.fkine(sol.q)
tcp_positions.append(T_actual.t)
else:
print(f"IK failed at s={s:.2f}")
break
tcp_positions = np.array(tcp_positions)
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.plot(tcp_positions[:, 0], tcp_positions[:, 1], tcp_positions[:, 2],
'r-', linewidth=2, label='MoveL TCP path')
ax.scatter(*tcp_positions[0], c='green', s=100, zorder=5, label='Start')
ax.scatter(*tcp_positions[-1], c='red', s=100, zorder=5, label='Target')
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title('MoveL — TCP Path (straight line guaranteed)')
ax.legend()
plt.tight_layout()
plt.savefig('movel_tcp_path.png', dpi=150)
plt.show()
MoveJ vs MoveL — Visual Comparison
import roboticstoolbox as rtb
import numpy as np
import matplotlib.pyplot as plt
ur5e = rtb.models.UR5()
q_start = np.array([0, -np.pi/3, np.pi/3, -np.pi/2, np.pi/2, 0])
q_target = np.array([np.pi/2, -np.pi/4, np.pi/4, -np.pi/3, np.pi/3, np.pi/4])
n_points = 100
# === MoveJ ===
movej_tcp = []
for i in range(n_points):
s = i / (n_points - 1)
q = q_start + s * (q_target - q_start)
T = ur5e.fkine(q)
movej_tcp.append(T.t)
movej_tcp = np.array(movej_tcp)
# === MoveL ===
T_start = ur5e.fkine(q_start)
T_end = ur5e.fkine(q_target)
movel_tcp = []
for i in range(n_points):
s = i / (n_points - 1)
pos = T_start.t + s * (T_end.t - T_start.t)
movel_tcp.append(pos)
movel_tcp = np.array(movel_tcp)
fig = plt.figure(figsize=(14, 6))
ax1 = fig.add_subplot(121, projection='3d')
ax1.plot(movej_tcp[:, 0], movej_tcp[:, 1], movej_tcp[:, 2], 'b-', linewidth=2)
ax1.scatter(*movej_tcp[0], c='green', s=80)
ax1.scatter(*movej_tcp[-1], c='red', s=80)
ax1.set_title('MoveJ — Curved TCP Path')
ax2 = fig.add_subplot(122, projection='3d')
ax2.plot(movel_tcp[:, 0], movel_tcp[:, 1], movel_tcp[:, 2], 'r-', linewidth=2)
ax2.scatter(*movel_tcp[0], c='green', s=80)
ax2.scatter(*movel_tcp[-1], c='red', s=80)
ax2.set_title('MoveL — Straight TCP Path')
plt.tight_layout()
plt.savefig('movej_vs_movel.png', dpi=150)
plt.show()
Key Parameters
Speed and Acceleration
| Parameter | MoveJ | MoveL | Units |
|---|---|---|---|
| Velocity | 0.1 — 3.14 | 0.001 — 1.0 | rad/s / m/s |
| Acceleration | 0.1 — 40 | 0.01 — 5.0 | rad/s² / m/s² |
| Typical safe | v=1.05, a=1.4 | v=0.25, a=1.2 | — |
| Slow approach | v=0.3, a=0.5 | v=0.05, a=0.3 | — |
Blend Radius
Blend radius (r) allows the robot to not stop at a waypoint but "cut the corner" — saving cycle time.
# URScript — palletizing with blend radius
def palletize():
movej(q_above_pallet, a=1.4, v=1.05, r=0.05)
movel(p_place, a=1.2, v=0.1, r=0)
set_digital_out(0, True) # Open gripper
sleep(0.5)
movel(p_above_place, a=1.2, v=0.25, r=0.02)
movej(q_above_pick, a=1.4, v=1.05, r=0.05)
When MoveJ Fails
MoveJ can fail when:
- Joint limits: Target is outside joint limits
- Self-collision: Target configuration causes internal collision
- Path through forbidden zone: Start and end are OK, but the joint space path passes through dangerous regions
Solution: Add intermediate waypoints or use a motion planner (RRT/PRM).
When MoveL Fails
MoveL fails more often than MoveJ because:
- Singularity: Cartesian path passes through or near a singularity
- Workspace boundary: Points on the straight line are outside the workspace
- Joint limits: IK solution violates joint limits
- IK configuration flip: Robot "jumps" between two IK configurations
# Example: MoveL failure due to singularity
# Wrist singularity: theta5 ≈ 0
q_near_sing = np.array([0, -np.pi/2, 0, 0, 0.01, 0])
T = ur5e.fkine(q_near_sing)
J = ur5e.jacob0(q_near_sing)
m = np.sqrt(np.linalg.det(J @ J.T))
print(f"Manipulability at near-singularity: {m:.6f}") # Very small!
print(f"Condition number: {np.linalg.cond(J):.1f}") # Very large!
Practical Demo: Pick-and-Place Workflow
Here is a complete pick-and-place cycle using a combination of MoveJ and MoveL:
# URScript — Complete pick-and-place cycle
def pick_and_place():
# === PICK PHASE ===
# 1. MoveJ: Fast move above pick position
movej(q_above_pick, a=1.4, v=1.05)
# 2. MoveL: Straight approach down to pick
movel(p_approach_pick, a=1.2, v=0.1)
# 3. MoveL: Slow, precise final approach
movel(p_pick, a=0.5, v=0.02)
# 4. Grasp object
set_digital_out(0, False) # Close gripper
sleep(0.3)
# 5. MoveL: Straight retreat up (avoid collision)
movel(p_approach_pick, a=1.2, v=0.25)
# === PLACE PHASE ===
# 6. MoveJ: Fast move above place position
movej(q_above_place, a=1.4, v=1.05)
# 7. MoveL: Straight approach down to place
movel(p_approach_place, a=1.2, v=0.1)
# 8. MoveL: Slow, precise placement
movel(p_place, a=0.5, v=0.02)
# 9. Release object
set_digital_out(0, True) # Open gripper
sleep(0.3)
# 10. MoveL: Straight retreat up
movel(p_approach_place, a=1.2, v=0.25)
# 11. MoveJ: Return to home
movej(q_home, a=1.4, v=1.05)
Golden Rules for Combining MoveJ/MoveL
- MoveJ for free movements (TCP path does not matter)
- MoveL for approach and retreat (need straight vertical/horizontal path)
- Slow MoveL for precise operations (grasping, placement, assembly)
- Blend radius for consecutive MoveJ commands (save cycle time)
- Blend radius = 0 for final points (precise stop)
References
- Universal Robots. URScript Programming Language. UR Documentation
- Universal Robots. The URScript Manual — e-Series, SW 5.x. 2023.
- 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
MoveJ and MoveL are the two foundational commands in robot programming:
- MoveJ — fast, safe, joint interpolation, curved TCP path
- MoveL — precise, straight-line TCP, may encounter singularity
- The right combination optimizes both cycle time and accuracy
In the next post, we will explore MoveC — circular/arc motion for welding, polishing, and screw driving applications.
Related Posts
- Joint Space vs Cartesian Space Planning — Theoretical foundation for MoveJ/MoveL
- Inverse Kinematics for 6-DOF Robots — IK is the core of MoveL
- ROS 2 Control Hardware Interface — Sending trajectories to real robots via ros2_control