Kinematics User Guide#
This guide covers the SerialManipulator class in ManipulaPy, which provides forward kinematics, inverse kinematics, and Jacobian computations for serial robot manipulators using modern screw theory.
What is Robot Kinematics?#
Robot kinematics studies the geometry of manipulator motion without considering forces. The key problems are:
Forward Kinematics (FK): Given joint angles â find end-effector pose
Inverse Kinematics (IK): Given desired pose â find joint angles
Jacobian Analysis: Relationship between joint velocities and end-effector motion
ManipulaPy uses the Product of Exponentials (PoE) formulation with screw theory for numerically stable computations.
Mathematical Foundation#
Product of Exponentials Formula#
The forward kinematics is computed using:
Space Frame:
Body Frame:
where
with \([\boldsymbol\omega]_\times\) the usual 3Ă3 skewâsymmetric matrix of \(\boldsymbol\omega\).
Screw Axes#
Each jointâs motion is encoded as a 6-vector
with three canonical cases:
Revolute joint about axis \(\boldsymbol\omega\) through point \(\mathbf r\)
\[\begin{split}\mathbf v = \mathbf r \times \boldsymbol\omega,\qquad \mathcal S = \begin{bmatrix} \boldsymbol\omega \\ \mathbf r \times \boldsymbol\omega \end{bmatrix}\end{split}\]Prismatic joint translating along unit vector \(\mathbf v\)
\[\begin{split}\mathcal S = \begin{bmatrix} \mathbf0 \\ \mathbf v \end{bmatrix},\quad \|\mathbf v\|=1\end{split}\]General (helical) screw with pitch \(h\)
\[\begin{split}\mathcal S = \begin{bmatrix} \boldsymbol\omega \\ \mathbf r \times \boldsymbol\omega + h\,\boldsymbol\omega \end{bmatrix}\end{split}\]
Jacobian Computation#
Space Jacobian for an n-DOF manipulator:
where
and
Body Jacobian follows by
Inverse Kinematics (NewtonâRaphson / Damped Least Squares)#
Error twist:
Update step:
with damping \(\lambda\) for numerical stability.
SerialManipulator Class#
Constructor#
from ManipulaPy.kinematics import SerialManipulator
robot = SerialManipulator(
M_list, # Home configuration (4Ă4 matrix)
omega_list, # Joint axes (3Ăn matrix)
r_list=None, # Points on joint axes (optional)
b_list=None, # Body frame points (optional)
S_list=None, # Space frame screw axes (6Ăn matrix)
B_list=None, # Body frame screw axes (6Ăn matrix)
G_list=None, # Inertia matrices (for dynamics)
joint_limits=None # Joint limits [(min, max), ...]
)
Key Parameters:
M_list: 4Ă4 transformation matrix representing the home pose
omega_list: 3Ăn matrix of joint rotation axes
S_list: 6Ăn matrix of space frame screw axes (auto-computed if not provided)
joint_limits: List of (min, max) tuples for each joint
Creating a Robot Model#
From URDF (Recommended)#
from ManipulaPy.urdf_processor import URDFToSerialManipulator
from ManipulaPy.ManipulaPy_data.xarm import urdf_file as xarm_urdf
# Load built-in xArm robot
processor = URDFToSerialManipulator(xarm_urdf)
robot = processor.serial_manipulator
print(f"Robot has {len(robot.joint_limits)} joints")
print(f"Home position: {robot.M_list[:3, 3]}")
Manual Definition#
import numpy as np
# Example: 2-DOF planar robot
def create_2dof_planar_robot():
"""Create a simple 2-DOF planar RR robot."""
# Link lengths
L1, L2 = 0.5, 0.3
# Home configuration (fully extended)
M = np.array([
[1, 0, 0, L1 + L2],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
])
# Space frame screw axes (both Z-axis rotations)
S_list = np.array([
# Joint 1 at origin
[0, 0, 1, 0, 0, 0],
# Joint 2 at (L1, 0, 0)
[0, 0, 1, 0, -L1, 0]
]).T # Shape: (6, 2)
# Extract omega_list for constructor
omega_list = S_list[:3, :]
# Joint limits
joint_limits = [(-np.pi, np.pi), (-np.pi, np.pi)]
robot = SerialManipulator(
M_list=M,
omega_list=omega_list,
S_list=S_list,
joint_limits=joint_limits
)
return robot
# Create the robot
planar_robot = create_2dof_planar_robot()
Forward Kinematics#
Basic Forward Kinematics#
import numpy as np
# Define joint angles
theta = np.array([0.5, -0.3]) # radians
# Compute forward kinematics
T = robot.forward_kinematics(theta, frame="space")
# Extract position and orientation
position = T[:3, 3]
rotation_matrix = T[:3, :3]
print(f"End-effector position: {position}")
print(f"End-effector orientation:\n{rotation_matrix}")
Space vs Body Frames#
import numpy as np
theta = np.array([0.2, 0.3])
# Both methods give the same result
T_space = robot.forward_kinematics(theta, frame="space")
T_body = robot.forward_kinematics(theta, frame="body")
# Verify they're identical
error = np.linalg.norm(T_space - T_body)
print(f"Space vs Body frame error: {error:.2e}") # Should be ~0
End-Effector Pose as Vector#
import numpy as np
# Get pose as [x, y, z, roll, pitch, yaw]
pose_vector = robot.end_effector_pose(theta)
position = pose_vector[:3]
euler_angles = pose_vector[3:] # in radians
print(f"Position: {position}")
print(f"Orientation (degrees): {np.degrees(euler_angles)}")
Multiple Configurations#
import numpy as np
def test_multiple_configurations():
"""Test FK for multiple joint configurations."""
# Test configurations
test_configs = [
np.array([0.0, 0.0]), # Home position
np.array([np.pi/4, -np.pi/4]), # 45° configuration
np.array([np.pi/2, 0.0]), # Elbow up
np.array([0.0, np.pi/2]) # Forearm up
]
config_names = ["Home", "45° config", "Elbow up", "Forearm up"]
for config, name in zip(test_configs, config_names):
T = robot.forward_kinematics(config)
pos = T[:3, 3]
print(f"{name:12}: position = [{pos[0]:.3f}, {pos[1]:.3f}, {pos[2]:.3f}]")
test_multiple_configurations()
Inverse Kinematics#
Basic Inverse Kinematics#
import numpy as np
# Define target pose
T_target = np.eye(4)
T_target[:3, 3] = [0.6, 0.2, 0.0] # desired position
# Initial guess
theta_initial = np.array([0.0, 0.0])
# Solve inverse kinematics
solution, success, iterations = robot.iterative_inverse_kinematics(
T_desired=T_target,
thetalist0=theta_initial,
eomg=1e-6, # rotation error tolerance
ev=1e-6, # translation error tolerance
max_iterations=1000
)
if success:
print(f"â
IK converged in {iterations} iterations")
print(f"Solution: {np.degrees(solution)} degrees")
# Verify solution
T_achieved = robot.forward_kinematics(solution)
error = np.linalg.norm(T_achieved[:3, 3] - T_target[:3, 3])
print(f"Position error: {error:.6f} m")
else:
print("â IK failed to converge")
Advanced IK Options#
# More robust IK with damping
solution, success, iterations = robot.iterative_inverse_kinematics(
T_desired=T_target,
thetalist0=theta_initial,
eomg=1e-6,
ev=1e-6,
max_iterations=1000,
plot_residuals=True, # Save convergence plot
# New solver knobs (optional; off by default on iterative_inverse_kinematics)
weight_position=1.5, # emphasis on translational error in the solve step
weight_orientation=1.0, # emphasis on rotational error
adaptive_tuning=True, # adapt damping/step size when error stalls
backtracking=True, # simple line search over scaled steps
)
Key IK tuning parameters:
damping: Damped least-squares regularization (stability near singularities).
step_cap: Maximum norm of \(\Delta\theta\) per iteration.
weight_position / weight_orientation: Scale translational vs rotational error in the solve.
adaptive_tuning: Adjusts damping/step_cap on the fly when progress is slow.
backtracking: Tries scaled steps (1.0, 0.5, 0.25) to pick the best error reduction.
smart_inverse_kinematics enables adaptive/backtracking by default and accepts the same weighting knobs. You can also supply a cache to reuse good solutions:
import numpy as np
from ManipulaPy.ik_helpers import IKInitialGuessCache
cache = IKInitialGuessCache(max_size=200)
T_target = np.eye(4)
T_target[:3, 3] = [0.6, 0.1, 0.0]
theta, success, iters = robot.smart_inverse_kinematics(
T_target,
strategy="cached", # try cache first, then fall back to workspace guess
cache=cache,
weight_position=1.5,
weight_orientation=1.0,
)
if success:
cache.add(T_target, theta) # store solution with residuals for future queries
Multiple IK Solutions#
import numpy as np
def find_multiple_solutions(robot, target_pos, n_attempts=5):
"""Find multiple IK solutions for the same target."""
T_target = np.eye(4)
T_target[:3, 3] = target_pos
solutions = []
for attempt in range(n_attempts):
# Random initial guess
joint_limits = np.array(robot.joint_limits)
theta_init = np.random.uniform(joint_limits[:, 0], joint_limits[:, 1])
solution, success, _ = robot.iterative_inverse_kinematics(
T_desired=T_target,
thetalist0=theta_init,
max_iterations=500
)
if success:
# Check if this is a new solution
is_new = True
for existing_sol in solutions:
if np.linalg.norm(solution - existing_sol) < 0.1:
is_new = False
break
if is_new:
solutions.append(solution)
print(f"Solution {len(solutions)}: {np.degrees(solution)}")
return solutions
# Test multiple solutions
target = [0.5, 0.3, 0.0]
multiple_solutions = find_multiple_solutions(robot, target)
print(f"Found {len(multiple_solutions)} distinct solutions")
Jacobian Matrix#
Computing the Jacobian#
import numpy as np
theta = np.array([0.3, -0.2])
# Compute Jacobian in space frame
J_space = robot.jacobian(theta, frame="space")
# Compute Jacobian in body frame
J_body = robot.jacobian(theta, frame="body")
print(f"Space Jacobian shape: {J_space.shape}") # (6, n_joints)
print(f"Body Jacobian shape: {J_body.shape}") # (6, n_joints)
Jacobian Analysis#
import numpy as np
def analyze_jacobian(robot, theta):
"""Analyze Jacobian properties at a configuration."""
J = robot.jacobian(theta)
# Basic properties
rank = np.linalg.matrix_rank(J)
condition_number = np.linalg.cond(J)
print(f"Jacobian Analysis:")
print(f" Shape: {J.shape}")
print(f" Rank: {rank} (full rank: {rank == min(J.shape)})")
print(f" Condition number: {condition_number:.2e}")
# Singularity check
if condition_number > 1e6:
print(" â ď¸ Configuration is near singular!")
else:
print(" â
Configuration is well-conditioned")
# Manipulability (for square Jacobians)
if J.shape[0] == J.shape[1]:
manipulability = abs(np.linalg.det(J))
print(f" Manipulability: {manipulability:.6f}")
else:
# For non-square Jacobians
manipulability = np.sqrt(np.linalg.det(J @ J.T))
print(f" Manipulability: {manipulability:.6f}")
return J, condition_number
# Analyze current configuration
J, cond_num = analyze_jacobian(robot, theta)
Velocity Kinematics#
End-Effector Velocity#
import numpy as np
# Current configuration and joint velocities
theta = np.array([0.2, 0.3])
theta_dot = np.array([0.1, -0.2]) # rad/s
# Compute end-effector velocity
V_ee = robot.end_effector_velocity(theta, theta_dot, frame="space")
print(f"Joint velocities: {theta_dot} rad/s")
print(f"End-effector velocity: {V_ee}")
# Decompose spatial velocity
linear_velocity = V_ee[:3] # [vx, vy, vz]
angular_velocity = V_ee[3:] # [Ďx, Ďy, Ďz]
print(f"Linear velocity: {linear_velocity} m/s")
print(f"Angular velocity: {angular_velocity} rad/s")
Joint Velocities from Desired Motion#
import numpy as np
# Desired end-effector motion
V_desired = np.array([0.1, 0.0, 0.0, 0.0, 0.0, 0.2]) # Move +X, rotate +Z
# Compute required joint velocities
theta_dot_required = robot.joint_velocity(theta, V_desired, frame="space")
print(f"Desired EE velocity: {V_desired}")
print(f"Required joint velocities: {theta_dot_required} rad/s")
# Verify by forward computation
V_achieved = robot.end_effector_velocity(theta, theta_dot_required, frame="space")
error = np.linalg.norm(V_achieved - V_desired)
print(f"Velocity error: {error:.2e}")
State Management#
Robot State Tracking#
# Update robot state
robot.update_state(
joint_positions=theta,
joint_velocities=theta_dot
)
# Access current state
print(f"Current positions: {robot.joint_positions}")
print(f"Current velocities: {robot.joint_velocities}")
# Compute current end-effector state
current_pose = robot.end_effector_pose(robot.joint_positions)
current_velocity = robot.end_effector_velocity(
robot.joint_positions,
robot.joint_velocities
)
print(f"Current EE pose: {current_pose}")
print(f"Current EE velocity: {current_velocity}")
Working Examples#
Complete Workflow Example#
import numpy as np
def complete_kinematics_example():
"""Complete example showing all kinematic functions."""
# Create a simple 3-DOF robot
def create_3dof_robot():
L1, L2, L3 = 0.3, 0.25, 0.15
M = np.array([
[1, 0, 0, L1 + L2 + L3],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
])
S_list = np.array([
[0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, -L1, 0],
[0, 0, 1, 0, -(L1+L2), 0]
]).T
omega_list = S_list[:3, :]
joint_limits = [(-np.pi, np.pi)] * 3
return SerialManipulator(
M_list=M,
omega_list=omega_list,
S_list=S_list,
joint_limits=joint_limits
)
robot = create_3dof_robot()
print("=== Complete Kinematics Example ===")
# 1. Forward Kinematics
theta = np.array([0.5, -0.3, 0.8])
T = robot.forward_kinematics(theta)
print(f"1. Forward Kinematics:")
print(f" Joint angles: {np.degrees(theta)} deg")
print(f" End-effector position: {T[:3, 3]}")
# 2. Inverse Kinematics
T_target = np.eye(4)
T_target[:3, 3] = [0.4, 0.2, 0.0]
solution, success, iterations = robot.iterative_inverse_kinematics(
T_desired=T_target,
thetalist0=np.array([0.0, 0.0, 0.0])
)
print(f"\n2. Inverse Kinematics:")
print(f" Target position: {T_target[:3, 3]}")
print(f" Success: {success}")
if success:
print(f" Solution: {np.degrees(solution)} deg")
print(f" Iterations: {iterations}")
# 3. Jacobian Analysis
J = robot.jacobian(theta)
cond_num = np.linalg.cond(J)
print(f"\n3. Jacobian Analysis:")
print(f" Shape: {J.shape}")
print(f" Condition number: {cond_num:.2e}")
# 4. Velocity Kinematics
theta_dot = np.array([0.1, -0.2, 0.3])
V_ee = robot.end_effector_velocity(theta, theta_dot)
print(f"\n4. Velocity Kinematics:")
print(f" Joint velocities: {theta_dot} rad/s")
print(f" EE linear velocity: {V_ee[:3]} m/s")
print(f" EE angular velocity: {V_ee[3:]} rad/s")
return robot
# Run the complete example
example_robot = complete_kinematics_example()
Common Issues and Solutions#
Troubleshooting IK Convergence#
import numpy as np
def troubleshoot_ik(robot, T_target):
"""Helper function to troubleshoot IK issues."""
print("đ IK Troubleshooting:")
# Check if target is reasonable
target_pos = T_target[:3, 3]
target_distance = np.linalg.norm(target_pos)
print(f"Target position: {target_pos}")
print(f"Target distance from origin: {target_distance:.3f} m")
# Try different initial guesses
joint_limits = np.array(robot.joint_limits)
attempts = [
np.zeros(len(joint_limits)), # Zero guess
np.mean(joint_limits, axis=1), # Middle of ranges
np.random.uniform(joint_limits[:, 0], joint_limits[:, 1]) # Random
]
attempt_names = ["Zero", "Middle", "Random"]
for i, (theta_init, name) in enumerate(zip(attempts, attempt_names)):
solution, success, iterations = robot.iterative_inverse_kinematics(
T_desired=T_target,
thetalist0=theta_init,
max_iterations=500
)
if success:
T_achieved = robot.forward_kinematics(solution)
error = np.linalg.norm(T_achieved[:3, 3] - target_pos)
print(f" {name} init: â
Success (error: {error:.2e}, iter: {iterations})")
return solution
else:
print(f" {name} init: â Failed after {iterations} iterations")
print(" All attempts failed. Target may be unreachable.")
return None
Best Practices#
Robot Definition
Use URDF files when possible for real robots
Validate screw axes are unit vectors for revolute joints
Set realistic joint limits
Forward Kinematics
Both space and body frames give identical results
Choose the frame that makes your calculations easier
Inverse Kinematics
Provide good initial guesses (avoid singularities)
Use damping for stability near singularities
Try multiple initial guesses for difficult targets
Jacobian Analysis
Monitor condition number to detect singularities
Use velocity kinematics for real-time control
Performance
Cache Jacobian computations when configuration doesnât change
Use appropriate tolerances (donât over-specify)
Next Steps#
Dynamics: Add forces and inertias â Dynamics User Guide
Trajectory Planning: Plan smooth motions â Trajectory Planning User Guide
Control: Implement feedback controllers â Control Module User Guide
Simulation: Test in PyBullet â Simulation Module User Guide
API Reference#
For complete function documentation: Kinematics API Reference