Joint Space vs Cartesian Space

In robotics, we work with two fundamental coordinate spaces: joint space and Cartesian space. Understanding the relationship between these spaces is crucial for robot control, path planning, and manipulation. Today, we’ll explore these concepts and learn how to transform between them using the powerful Jacobian matrix.

Understanding Coordinate Spaces

Joint Space

Joint space represents the robot’s configuration in terms of its joint angles or joint displacements. For a robot with n joints, the joint space is an n-dimensional space where each dimension corresponds to one joint.

Characteristics of Joint Space:

For a 6-DOF robot, joint space is a 6-dimensional space: [θ₁, θ₂, θ₃, θ₄, θ₅, θ₆]

Cartesian Space

Cartesian space (or task space) represents the robot’s end-effector position and orientation in 3D space. It’s the space where we typically specify tasks like “pick up the object” or “move to position (x, y, z).”

Characteristics of Cartesian Space:

For most robots, Cartesian space is 6-dimensional: [x, y, z, roll, pitch, yaw]

The Jacobian Matrix

The Jacobian matrix is the mathematical bridge between joint space and Cartesian space. It represents the relationship between joint velocities and end-effector velocities.

Mathematical Definition

For a robot with n joints and m end-effector variables, the Jacobian J is an m×n matrix:

J = [∂x/∂θ₁  ∂x/∂θ₂  ...  ∂x/∂θₙ]
    [∂y/∂θ₁  ∂y/∂θ₂  ...  ∂y/∂θₙ]
    [∂z/∂θ₁  ∂z/∂θ₂  ...  ∂z/∂θₙ]
    [∂φ/∂θ₁  ∂φ/∂θ₂  ...  ∂φ/∂θₙ]

Where:

Computing the Jacobian

Let’s implement a Jacobian calculator for a 2D robot:

import numpy as np

class Robot2DJacobian:
    def __init__(self, link_lengths):
        """
        Initialize a 2D robot arm with Jacobian capabilities
        
        Args:
            link_lengths: List of link lengths
        """
        self.link_lengths = link_lengths
        self.num_joints = len(link_lengths)
    
    def forward_kinematics(self, joint_angles):
        """
        Calculate joint positions from joint angles
        
        Args:
            joint_angles: List of joint angles in radians
        
        Returns:
            List of joint positions and end-effector position
        """
        positions = [(0, 0)]
        x, y = 0, 0
        angle_sum = 0
        
        for i, (length, angle) in enumerate(zip(self.link_lengths, joint_angles)):
            angle_sum += angle
            x += length * np.cos(angle_sum)
            y += length * np.sin(angle_sum)
            positions.append((x, y))
        
        return positions, (x, y)
    
    def jacobian(self, joint_angles):
        """
        Calculate the Jacobian matrix for the 2D robot
        
        Args:
            joint_angles: List of joint angles in radians
        
        Returns:
            2xN Jacobian matrix (2D position, N joints)
        """
        x_end, y_end = self.forward_kinematics(joint_angles)[1]
        J = np.zeros((2, self.num_joints))
        
        angle_sum = 0
        for i in range(self.num_joints):
            angle_sum += joint_angles[i]
            
            # Partial derivatives for x and y
            for j in range(i, self.num_joints):
                angle_sum += joint_angles[j]
                J[0, i] -= self.link_lengths[j] * np.sin(angle_sum)
                J[1, i] += self.link_lengths[j] * np.cos(angle_sum)
                angle_sum -= joint_angles[j]
        
        return J
    
    def velocity_transform(self, joint_angles, joint_velocities):
        """
        Transform joint velocities to Cartesian velocities
        
        Args:
            joint_angles: Current joint angles
            joint_velocities: Joint velocities [θ̇₁, θ̇₂, ...]
        
        Returns:
            Cartesian velocities [ẋ, ẏ]
        """
        J = self.jacobian(joint_angles)
        return J @ joint_velocities

# Example usage
robot = Robot2DJacobian([2.0, 1.5])
joint_angles = [np.pi/4, np.pi/6]

# Calculate Jacobian
J = robot.jacobian(joint_angles)
print("Jacobian Matrix:")
print(J)

# Test velocity transformation
joint_velocities = [0.1, -0.05]  # rad/s
cartesian_velocities = robot.velocity_transform(joint_angles, joint_velocities)
print(f"\nJoint velocities: {joint_velocities}")
print(f"Cartesian velocities: {cartesian_velocities}")

3D Jacobian Implementation

For 3D robots, the Jacobian is more complex as it includes both linear and angular velocities:

def compute_3d_jacobian(joint_angles, link_lengths):
    """
    Compute 3D Jacobian for a simple robot arm
    
    Args:
        joint_angles: [θ1, θ2, θ3] joint angles
        link_lengths: [L1, L2, L3] link lengths
    
    Returns:
        6x3 Jacobian matrix [linear; angular]
    """
    # Extract joint angles
    theta1, theta2, theta3 = joint_angles
    L1, L2, L3 = link_lengths
    
    # Position of end-effector
    x = L1*np.cos(theta1) + L2*np.cos(theta1)*np.cos(theta2) + L3*np.cos(theta1)*np.cos(theta2+theta3)
    y = L1*np.sin(theta1) + L2*np.sin(theta1)*np.cos(theta2) + L3*np.sin(theta1)*np.cos(theta2+theta3)
    z = L2*np.sin(theta2) + L3*np.sin(theta2+theta3)
    
    # Linear velocity Jacobian (3x3)
    J_linear = np.array([
        [-L1*np.sin(theta1) - L2*np.sin(theta1)*np.cos(theta2) - L3*np.sin(theta1)*np.cos(theta2+theta3),
         -L2*np.cos(theta1)*np.sin(theta2) - L3*np.cos(theta1)*np.sin(theta2+theta3),
         -L3*np.cos(theta1)*np.sin(theta2+theta3)],
        
        [L1*np.cos(theta1) + L2*np.cos(theta1)*np.cos(theta2) + L3*np.cos(theta1)*np.cos(theta2+theta3),
         -L2*np.sin(theta1)*np.sin(theta2) - L3*np.sin(theta1)*np.sin(theta2+theta3),
         -L3*np.sin(theta1)*np.sin(theta2+theta3)],
        
        [0,
         L2*np.cos(theta2) + L3*np.cos(theta2+theta3),
         L3*np.cos(theta2+theta3)]
    ])
    
    # Angular velocity Jacobian (3x3)
    J_angular = np.array([
        [0, 0, 0],
        [0, 0, 0],
        [1, 1, 1]
    ])
    
    # Combine linear and angular parts
    J = np.vstack([J_linear, J_angular])
    
    return J, np.array([x, y, z])

# Test 3D Jacobian
joint_angles_3d = [np.pi/4, np.pi/3, np.pi/6]
link_lengths_3d = [1.0, 0.8, 0.5]

J_3d, position = compute_3d_jacobian(joint_angles_3d, link_lengths_3d)
print("3D Jacobian Matrix:")
print(J_3d)
print(f"\nEnd-effector position: {position}")

Practical Applications

1. Singularities and Jacobian Analysis

Singularities occur when the Jacobian loses rank, meaning the robot loses degrees of freedom:

def check_singularity(J, threshold=1e-6):
    """
    Check if the Jacobian is near a singularity
    
    Args:
        J: Jacobian matrix
        threshold: Singular value threshold
    
    Returns:
        is_singular: Boolean indicating if singular
        condition_number: Condition number of the Jacobian
    """
    try:
        singular_values = np.linalg.svd(J, compute_uv=False)
        condition_number = np.max(singular_values) / np.min(singular_values)
        is_singular = np.min(singular_values) < threshold
        return is_singular, condition_number
    except:
        return True, float('inf')

# Test for singularities
for theta2 in [0, np.pi/2, np.pi]:
    test_angles = [np.pi/4, theta2, np.pi/6]
    J_test, _ = compute_3d_jacobian(test_angles, link_lengths_3d)
    is_singular, cond_num = check_singularity(J_test)
    print(f"θ2 = {theta2:.2f}: Singular = {is_singular}, Condition number = {cond_num:.2f}")

2. Inverse Kinematics using Jacobian

The Jacobian can be used for iterative inverse kinematics:

def jacobian_ik(target_position, initial_angles, link_lengths, max_iterations=100, tolerance=1e-3):
    """
    Solve inverse kinematics using Jacobian pseudo-inverse
    
    Args:
        target_position: Desired end-effector position [x, y, z]
        initial_angles: Initial joint angles guess
        link_lengths: Link lengths
        max_iterations: Maximum number of iterations
        tolerance: Position tolerance
    
    Returns:
        joint_angles: Solution joint angles
        success: Whether solution was found
    """
    current_angles = np.array(initial_angles)
    
    for iteration in range(max_iterations):
        # Calculate current position and Jacobian
        J, current_pos = compute_3d_jacobian(current_angles, link_lengths)
        
        # Check if we're close enough to target
        error = np.linalg.norm(target_position - current_pos)
        if error < tolerance:
            return current_angles, True
        
        # Calculate velocity using pseudo-inverse
        delta_pos = target_position - current_pos
        try:
            J_pinv = np.linalg.pinv(J)
            delta_angles = J_pinv @ delta_pos
            current_angles += 0.1 * delta_angles  # Step size
        except:
            break
    
    return current_angles, False

# Test Jacobian IK
target_pos = [1.5, 1.0, 0.5]
initial_guess = [0.5, 0.5, 0.5]
solution, success = jacobian_ik(target_pos, initial_guess, link_lengths_3d)

print(f"Target position: {target_pos}")
print(f"Initial guess: {initial_guess}")
print(f"Solution: {solution}")
print(f"Success: {success}")

Workspace Analysis

The Jacobian helps us understand the robot’s workspace and manipulability:

def manipulability_index(J):
    """
    Calculate the manipulability index of the robot
    
    Args:
        J: Jacobian matrix
    
    Returns:
        manipulability: Scalar measure of dexterity
    """
    return np.sqrt(np.linalg.det(J @ J.T))

def analyze_workspace(robot, joint_ranges, resolution=20):
    """
    Analyze robot workspace manipulability
    
    Args:
        robot: Robot instance with Jacobian method
        joint_ranges: List of (min, max) angle ranges for each joint
        resolution: Number of points per dimension
    """
    # Generate random joint configurations
    manipulabilities = []
    positions = []
    
    for _ in range(1000):
        # Random joint angles within limits
        joint_angles = [np.random.uniform(low, high) for low, high in joint_ranges]
        
        # Calculate position and manipulability
        J = robot.jacobian(joint_angles)
        manip = manipulability_index(J)
        pos, _ = robot.forward_kinematics(joint_angles)
        
        manipulabilities.append(manip)
        positions.append(pos[-1])  # End-effector position
    
    return positions, manipulabilities

# Analyze workspace
joint_ranges = [(-np.pi, np.pi), (-np.pi/2, np.pi/2)]  # Joint angle limits
positions, manipulabilities = analyze_workspace(robot, joint_ranges)

print(f"Average manipulability: {np.mean(manipulabilities):.4f}")
print(f"Min manipulability: {np.min(manipulabilities):.4f}")
print(f"Max manipulability: {np.max(manipulabilities):.4f}")

Key Takeaways

Next Steps

Tomorrow, we’ll explore robot workspace analysis and understand how to visualize and analyze the reachable workspace of different robot configurations. We’ll also implement workspace boundary calculation and optimization techniques.

Practice Exercise: Implement a complete Jacobian-based inverse kinematics solver for a 3D robot arm. Test it with different target positions and analyze the convergence behavior. Experiment with different initial guesses to understand how they affect the solution.