Robot Workspace Analysis

The workspace of a robot is one of the most important concepts in robotics. It defines all the positions and orientations that the robot’s end-effector can reach. Understanding workspace analysis is crucial for robot design, task planning, and operation. Today, we’ll dive deep into workspace analysis techniques and learn how to visualize and optimize robot workspaces.

What is Robot Workspace?

The workspace of a robot is the region of space that the robot’s end-effector can reach. It’s typically divided into several types:

1. Reachable Workspace

The set of all points that can be reached by the end-effector, regardless of orientation.

2. Dexterous Workspace

The subset of the reachable workspace where the end-effector can achieve any desired orientation.

3. Constant Orientation Workspace

The set of points reachable with a fixed end-effector orientation.

Analyzing 2D Robot Workspace

Let’s start with a comprehensive 2D workspace analysis:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import matplotlib.patches as patches

class Robot2DWorkspace:
    def __init__(self, link_lengths):
        """
        Initialize 2D robot for workspace analysis
        
        Args:
            link_lengths: List of link lengths
        """
        self.link_lengths = np.array(link_lengths)
        self.total_length = np.sum(link_lengths)
        self.num_joints = len(link_lengths)
    
    def forward_kinematics(self, joint_angles):
        """
        Calculate end-effector position from joint angles
        
        Args:
            joint_angles: List of joint angles in radians
        
        Returns:
            End-effector position (x, y)
        """
        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)
        
        return x, y
    
    def generate_workspace_boundary(self, num_points=1000):
        """
        Generate the boundary of the reachable workspace
        
        Args:
            num_points: Number of points to generate
        
        Returns:
            Boundary points (x, y)
        """
        # The workspace boundary is a circle with radius = total link length
        theta = np.linspace(0, 2*np.pi, num_points)
        x_boundary = self.total_length * np.cos(theta)
        y_boundary = self.total_length * np.sin(theta)
        
        return x_boundary, y_boundary
    
    def generate_reachable_points(self, num_samples=5000):
        """
        Generate random reachable points within the workspace
        
        Args:
            num_samples: Number of random points to generate
        
        Returns:
            Array of reachable points (x, y)
        """
        reachable_points = []
        
        for _ in range(num_samples):
            # Generate random joint angles
            joint_angles = np.random.uniform(-np.pi, np.pi, self.num_joints)
            
            # Calculate end-effector position
            x, y = self.forward_kinematics(joint_angles)
            
            # Check if position is reachable (within total length)
            if np.sqrt(x**2 + y**2) <= self.total_length:
                reachable_points.append([x, y])
        
        return np.array(reachable_points)
    
    def plot_workspace(self, show_boundary=True, show_samples=True, show_obstacles=False):
        """
        Visualize the robot workspace
        
        Args:
            show_boundary: Show workspace boundary
            show_samples: Show sample reachable points
            show_obstacles: Show obstacle positions
        """
        fig, ax = plt.subplots(1, 1, figsize=(12, 10))
        
        # Plot workspace boundary
        if show_boundary:
            x_boundary, y_boundary = self.generate_workspace_boundary()
            ax.plot(x_boundary, y_boundary, 'b-', linewidth=2, label='Workspace Boundary')
            
            # Fill workspace
            workspace_circle = Circle((0, 0), self.total_length, fill=True, 
                                   alpha=0.1, color='blue', label='Reachable Workspace')
            ax.add_patch(workspace_circle)
        
        # Plot sample reachable points
        if show_samples:
            reachable_points = self.generate_reachable_points()
            ax.scatter(reachable_points[:, 0], reachable_points[:, 1], 
                      c='red', s=1, alpha=0.6, label='Reachable Points')
        
        # Plot obstacles if specified
        if show_obstacles:
            obstacles = self.generate_obstacles()
            for obs in obstacles:
                obstacle_circle = Circle(obs['position'], obs['radius'], 
                                      fill=True, color='gray', alpha=0.7)
                ax.add_patch(obstacle_circle)
        
        # Plot robot arm in multiple configurations
        self.plot_robot_configurations(ax, num_configs=5)
        
        ax.set_xlim(-self.total_length*1.1, self.total_length*1.1)
        ax.set_ylim(-self.total_length*1.1, self.total_length*1.1)
        ax.set_aspect('equal')
        ax.grid(True, alpha=0.3)
        ax.legend()
        ax.set_xlabel('X Position')
        ax.set_ylabel('Y Position')
        ax.set_title('2D Robot Workspace Analysis')
        
        plt.tight_layout()
        plt.show()
    
    def plot_robot_configurations(self, ax, num_configs=5):
        """
        Plot robot arm in multiple configurations
        
        Args:
            ax: Matplotlib axis
            num_configs: Number of configurations to show
        """
        colors = plt.cm.Set3(np.linspace(0, 1, num_configs))
        
        for i, color in enumerate(colors):
            # Random joint angles
            joint_angles = np.random.uniform(-np.pi, np.pi, self.num_joints)
            
            # Calculate joint positions
            positions = [(0, 0)]
            x, y = 0, 0
            angle_sum = 0
            
            for j, (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))
            
            # Plot robot arm
            x_coords = [pos[0] for pos in positions]
            y_coords = [pos[1] for pos in positions]
            ax.plot(x_coords, y_coords, color=color, linewidth=2, alpha=0.7)
            ax.plot(x_coords[-1], y_coords[-1], 'o', color=color, markersize=6)
    
    def calculate_workspace_metrics(self):
        """
        Calculate various workspace metrics
        
        Returns:
            Dictionary of workspace metrics
        """
        metrics = {
            'total_area': np.pi * self.total_length**2,
            'link_lengths': self.link_lengths.tolist(),
            'num_joints': self.num_joints,
            'max_reach': self.total_length,
            'min_reach': abs(self.link_lengths[0] - np.sum(self.link_lengths[1:]))
        }
        
        return metrics
    
    def generate_obstacles(self, num_obstacles=3):
        """
        Generate random obstacles in the workspace
        
        Args:
            num_obstacles: Number of obstacles to generate
        
        Returns:
            List of obstacle dictionaries
        """
        obstacles = []
        
        for _ in range(num_obstacles):
            # Random position within workspace
            angle = np.random.uniform(0, 2*np.pi)
            radius = np.random.uniform(0, self.total_length * 0.8)
            position = [radius * np.cos(angle), radius * np.sin(angle)]
            
            # Random radius
            obstacle_radius = np.random.uniform(0.1, 0.3)
            
            obstacles.append({
                'position': position,
                'radius': obstacle_radius
            })
        
        return obstacles

# Example usage
robot_2d = Robot2DWorkspace([2.0, 1.5])
metrics = robot_2d.calculate_workspace_metrics()

print("Workspace Metrics:")
for key, value in metrics.items():
    print(f"{key}: {value}")

# Visualize workspace
robot_2d.plot_workspace(show_boundary=True, show_samples=True, show_obstacles=True)

3D Workspace Analysis

For 3D robots, workspace analysis becomes more complex but equally important:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

class Robot3DWorkspace:
    def __init__(self, link_lengths):
        """
        Initialize 3D robot for workspace analysis
        
        Args:
            link_lengths: List of link lengths [L1, L2, L3]
        """
        self.link_lengths = np.array(link_lengths)
        self.total_length = np.sum(link_lengths)
    
    def forward_kinematics(self, joint_angles):
        """
        Calculate 3D end-effector position
        
        Args:
            joint_angles: [θ1, θ2, θ3] joint angles in radians
        
        Returns:
            End-effector position (x, y, z)
        """
        theta1, theta2, theta3 = joint_angles
        
        x = (self.link_lengths[0] + self.link_lengths[1]*np.cos(theta2) + 
             self.link_lengths[2]*np.cos(theta2+theta3)) * np.cos(theta1)
        
        y = (self.link_lengths[0] + self.link_lengths[1]*np.cos(theta2) + 
             self.link_lengths[2]*np.cos(theta2+theta3)) * np.sin(theta1)
        
        z = self.link_lengths[1]*np.sin(theta2) + self.link_lengths[2]*np.sin(theta2+theta3)
        
        return x, y, z
    
    def generate_workspace_surface(self, resolution=50):
        """
        Generate workspace surface points
        
        Args:
            resolution: Resolution of the surface grid
        
        Returns:
            X, Y, Z coordinates of workspace surface
        """
        # Generate angles for the outer two joints
        theta2_range = np.linspace(-np.pi/2, np.pi/2, resolution)
        theta3_range = np.linspace(-np.pi, np.pi, resolution)
        
        theta2_grid, theta3_grid = np.meshgrid(theta2_range, theta3_range)
        
        # Calculate reach for each combination
        reach_grid = np.zeros_like(theta2_grid)
        
        for i in range(resolution):
            for j in range(resolution):
                theta2 = theta2_grid[i, j]
                theta3 = theta3_grid[i, j]
                
                # Maximum reach for this configuration
                max_reach = (self.link_lengths[0] + 
                           self.link_lengths[1] * abs(np.cos(theta2)) + 
                           self.link_lengths[2] * abs(np.cos(theta2 + theta3)))
                
                reach_grid[i, j] = max_reach
        
        # Convert to Cartesian coordinates
        X = reach_grid * np.cos(0)  # Fixed θ1 = 0 for simplicity
        Y = reach_grid * np.sin(0)  # Fixed θ1 = 0 for simplicity
        Z = reach_grid
        
        return X, Y, Z
    
    def plot_3d_workspace(self):
        """
        Visualize 3D workspace
        """
        fig = plt.figure(figsize=(15, 5))
        
        # 3D workspace visualization
        ax1 = fig.add_subplot(131, projection='3d')
        
        # Generate and plot workspace surface
        X, Y, Z = self.generate_workspace_surface()
        ax1.plot_surface(X, Y, Z, alpha=0.6, cmap='viridis')
        
        # Plot sample robot configurations
        for _ in range(10):
            joint_angles = np.random.uniform(-np.pi, np.pi, 3)
            x, y, z = self.forward_kinematics(joint_angles)
            ax1.scatter([x], [y], [z], color='red', s=50)
        
        ax1.set_xlabel('X')
        ax1.set_ylabel('Y')
        ax1.set_zlabel('Z')
        ax1.set_title('3D Robot Workspace')
        
        # XY projection
        ax2 = fig.add_subplot(132)
        theta_range = np.linspace(0, 2*np.pi, 100)
        x_circle = self.total_length * np.cos(theta_range)
        y_circle = self.total_length * np.sin(theta_range)
        ax2.plot(x_circle, y_circle, 'b-', linewidth=2)
        ax2.set_aspect('equal')
        ax2.set_xlabel('X')
        ax2.set_ylabel('Y')
        ax2.set_title('XY Projection')
        ax2.grid(True, alpha=0.3)
        
        # XZ projection
        ax3 = fig.add_subplot(133)
        z_range = np.linspace(-self.total_length, self.total_length, 100)
        x_circle = self.total_length * np.ones_like(z_range)
        ax3.plot(x_circle, z_range, 'b-', linewidth=2)
        ax3.plot(-x_circle, z_range, 'b-', linewidth=2)
        ax3.set_xlabel('X')
        ax3.set_ylabel('Z')
        ax3.set_title('XZ Projection')
        ax3.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.show()

# Example 3D workspace analysis
robot_3d = Robot3DWorkspace([1.0, 0.8, 0.5])
robot_3d.plot_3d_workspace()

Workspace Optimization

Optimizing robot workspace is crucial for better performance:

def optimize_workspace(link_lengths, target_task):
    """
    Optimize robot link lengths for a specific task
    
    Args:
        link_lengths: Initial link lengths
        target_task: Dictionary with task requirements
    
    Returns:
        Optimized link lengths
    """
    # Task requirements
    required_reach = target_task.get('required_reach', 2.0)
    dexterity_requirement = target_task.get('dexterity_requirement', 0.8)
    
    def objective_function(lengths):
        """
        Objective function to minimize
        """
        # Create robot with current lengths
        robot = Robot2DWorkspace(lengths)
        
        # Calculate workspace area
        workspace_area = np.pi * np.sum(lengths)**2
        
        # Calculate manipulability (simplified)
        manipulability = np.prod(lengths) / np.sum(lengths)**2
        
        # Penalty for not meeting reach requirement
        reach_penalty = max(0, required_reach - np.sum(lengths))**2
        
        # Penalty for poor dexterity
        dexterity_penalty = max(0, dexterity_requirement - manipulability)**2
        
        # Objective: maximize workspace and manipulability, minimize penalties
        objective = -(workspace_area + 10*manipulability) - 100*(reach_penalty + dexterity_penalty)
        
        return objective
    
    # Optimize using scipy
    from scipy.optimize import minimize
    
    # Constraints: positive lengths
    bounds = [(0.1, 3.0) for _ in link_lengths]
    
    result = minimize(objective_function, link_lengths, bounds=bounds, method='L-BFGS-B')
    
    return result.x

# Example workspace optimization
target_task = {
    'required_reach': 2.5,
    'dexterity_requirement': 0.7
}

initial_lengths = [1.0, 1.0, 1.0]
optimized_lengths = optimize_workspace(initial_lengths, target_task)

print(f"Initial lengths: {initial_lengths}")
print(f"Optimized lengths: {optimized_lengths}")
print(f"Total reach: {sum(optimized_lengths):.2f}")

Workspace with Obstacles

Real-world robots must work around obstacles:

def plan_path_with_obstacles(start_pos, end_pos, obstacles, robot_workspace):
    """
    Plan a path avoiding obstacles
    
    Args:
        start_pos: Starting position (x, y)
        end_pos: Ending position (x, y)
        obstacles: List of obstacle dictionaries
        robot_workspace: Robot workspace object
    
    Returns:
        Path waypoints and collision information
    """
    # Simple straight-line path planning
    path = np.linspace(start_pos, end_pos, 20)
    
    # Check for collisions
    collision_points = []
    for i, point in enumerate(path):
        for obstacle in obstacles:
            dist = np.linalg.norm(np.array(point) - np.array(obstacle['position']))
            if dist < obstacle['radius']:
                collision_points.append(i)
    
    # If collisions found, plan alternative path
    if collision_points:
        # Simple obstacle avoidance: go around
        avoid_path = []
        for i, point in enumerate(path):
            if i in collision_points:
                # Move perpendicular to path direction
                direction = path[i+1] - path[i-1] if i > 0 and i < len(path)-1 else np.array([0, 1])
                perpendicular = np.array([-direction[1], direction[0]])
                perpendicular = perpendicular / np.linalg.norm(perpendicular)
                avoid_point = point + perpendicular * obstacle['radius'] * 1.5
                avoid_path.append(avoid_point)
            else:
                avoid_path.append(point)
        
        path = np.array(avoid_path)
    
    return path, len(collision_points) > 0

# Example path planning with obstacles
robot_ws = Robot2DWorkspace([2.0, 1.5])
obstacles = robot_ws.generate_obstacles(num_obstacles=3)

start_pos = [-1.5, -1.0]
end_pos = [1.5, 1.0]

path, has_collisions = plan_path_with_obstacles(start_pos, end_pos, obstacles, robot_ws)

print(f"Path planning completed. Collisions detected: {has_collisions}")

# Visualize path planning
fig, ax = plt.subplots(1, 1, figsize=(10, 8))

# Plot workspace
x_boundary, y_boundary = robot_ws.generate_workspace_boundary()
ax.plot(x_boundary, y_boundary, 'b-', linewidth=2, label='Workspace Boundary')

# Plot obstacles
for obs in obstacles:
    obstacle_circle = Circle(obs['position'], obs['radius'], 
                          fill=True, color='gray', alpha=0.7, label='Obstacle' if obs == obstacles[0] else "")
    ax.add_patch(obstacle_circle)

# Plot path
ax.plot(path[:, 0], path[:, 1], 'r-', linewidth=2, label='Planned Path')
ax.plot(start_pos[0], start_pos[1], 'go', markersize=10, label='Start')
ax.plot(end_pos[0], end_pos[1], 'ro', markersize=10, label='End')

ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
ax.legend()
ax.set_xlabel('X Position')
ax.set_ylabel('Y Position')
ax.set_title('Path Planning with Obstacles')

plt.tight_layout()
plt.show()

Key Takeaways

Next Steps

Tomorrow, we’ll explore robot path planning algorithms, including A*, RRT, and other popular methods. We’ll implement these algorithms and test them on complex workspace scenarios with multiple obstacles.

Practice Exercise: Implement a comprehensive workspace analysis tool for your own robot configuration. Include workspace boundary calculation, manipulability analysis, and obstacle avoidance planning. Create visualizations for both 2D and 3D workspaces.