Source code for porcupy.utils.defense_visualization

"""
Defense mechanism visualization module for the Crested Porcupine Optimizer (CPO).

This module provides specialized visualization tools for the unique defense mechanisms
of the CPO algorithm, including sight, sound, odor, and physical defense visualizations.
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Wedge, Arrow
from matplotlib.collections import PatchCollection
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.animation as animation
from typing import List, Tuple, Optional, Union, Dict, Any, Callable


[docs]def visualize_defense_territories( positions: np.ndarray, defense_types: List[str], bounds: Tuple[np.ndarray, np.ndarray], territory_sizes: Optional[np.ndarray] = None, title: str = "Defense Territories", figsize: Tuple[int, int] = (10, 8), save_path: Optional[str] = None ): """ Visualize the defense territories of porcupines based on their defense mechanisms. Parameters ---------- positions : ndarray Positions of the porcupines, shape (pop_size, 2). defense_types : list List of defense mechanisms used by each porcupine. bounds : tuple A tuple (lb, ub) containing the lower and upper bounds. territory_sizes : ndarray, optional Sizes of the territories for each porcupine, shape (pop_size,). If None, territories are sized based on defense mechanism. title : str, optional Title of the plot (default: "Defense Territories"). figsize : tuple, optional Figure size as (width, height) in inches (default: (10, 8)). save_path : str, optional Path to save the figure. If None, the figure is not saved (default: None). Returns ------- matplotlib.figure.Figure The created figure. """ if positions.shape[1] != 2: raise ValueError("This function only works for 2D search spaces") # Create the figure and axis fig, ax = plt.subplots(figsize=figsize) # Define colors and sizes for different defense mechanisms defense_colors = { 'sight': 'blue', 'sound': 'green', 'odor': 'orange', 'physical': 'red' } defense_alphas = { 'sight': 0.2, 'sound': 0.3, 'odor': 0.4, 'physical': 0.5 } if territory_sizes is None: defense_sizes = { 'sight': 1.5, # Sight has the largest territory 'sound': 1.2, # Sound has a medium-large territory 'odor': 0.8, # Odor has a medium territory 'physical': 0.5 # Physical has the smallest territory } territory_sizes = np.array([defense_sizes[d] for d in defense_types]) # Create territory patches patches = [] for i, (pos, defense, size) in enumerate(zip(positions, defense_types, territory_sizes)): color = defense_colors.get(defense, 'gray') alpha = defense_alphas.get(defense, 0.3) # Create a circle patch for the territory circle = Circle(pos, size, alpha=alpha, edgecolor=color, facecolor=color, linewidth=1.5) patches.append(circle) # Add a marker for the porcupine itself ax.scatter(pos[0], pos[1], c=color, edgecolors='black', s=80, zorder=10) # Add the territory patches to the plot collection = PatchCollection(patches, match_original=True) ax.add_collection(collection) # Set the plot limits based on the bounds lb, ub = bounds ax.set_xlim(lb[0], ub[0]) ax.set_ylim(lb[1], ub[1]) # Add a legend legend_elements = [] for defense, color in defense_colors.items(): if defense in defense_types: legend_elements.append(plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=color, markersize=10, label=defense.capitalize())) ax.legend(handles=legend_elements) # Set labels and title ax.set_title(title) ax.set_xlabel('x1') ax.set_ylabel('x2') ax.grid(True, linestyle='--', alpha=0.3) if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig
[docs]def visualize_defense_mechanisms( positions: np.ndarray, prev_positions: np.ndarray, defense_types: List[str], bounds: Tuple[np.ndarray, np.ndarray], best_position: Optional[np.ndarray] = None, title: str = "Defense Mechanisms Visualization", figsize: Tuple[int, int] = (12, 10), save_path: Optional[str] = None ): """ Visualize the specific defense mechanisms of porcupines with directional indicators. Parameters ---------- positions : ndarray Current positions of the porcupines, shape (pop_size, 2). prev_positions : ndarray Previous positions of the porcupines, shape (pop_size, 2). defense_types : list List of defense mechanisms used by each porcupine. bounds : tuple A tuple (lb, ub) containing the lower and upper bounds. best_position : ndarray, optional Global best position, shape (2,). title : str, optional Title of the plot (default: "Defense Mechanisms Visualization"). figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 10)). save_path : str, optional Path to save the figure. If None, the figure is not saved (default: None). Returns ------- matplotlib.figure.Figure The created figure. """ if positions.shape[1] != 2: raise ValueError("This function only works for 2D search spaces") # Create the figure and axis fig, ax = plt.subplots(figsize=figsize) # Define colors for different defense mechanisms defense_colors = { 'sight': 'blue', 'sound': 'green', 'odor': 'orange', 'physical': 'red' } # Set the plot limits based on the bounds lb, ub = bounds ax.set_xlim(lb[0], ub[0]) ax.set_ylim(lb[1], ub[1]) # Plot the best position if provided if best_position is not None: ax.scatter(best_position[0], best_position[1], c='red', s=150, marker='*', label='Best Position', zorder=15) # Visualize each porcupine and its defense mechanism for i, (pos, prev_pos, defense) in enumerate(zip(positions, prev_positions, defense_types)): color = defense_colors.get(defense, 'gray') # Plot the porcupine ax.scatter(pos[0], pos[1], c=color, edgecolors='black', s=80, zorder=10) # Plot the movement vector movement = pos - prev_pos if np.linalg.norm(movement) > 0: ax.arrow(prev_pos[0], prev_pos[1], movement[0], movement[1], head_width=0.1, head_length=0.1, fc=color, ec=color, alpha=0.7) # Visualize the specific defense mechanism if defense == 'sight': # Sight: Draw a cone of vision angle = np.arctan2(movement[1], movement[0]) if np.linalg.norm(movement) > 0 else 0 wedge = Wedge(pos, 1.0, angle * 180 / np.pi - 30, angle * 180 / np.pi + 30, alpha=0.2, color=color, width=0.8) ax.add_patch(wedge) elif defense == 'sound': # Sound: Draw concentric circles representing sound waves for radius in [0.3, 0.6, 0.9]: circle = Circle(pos, radius, fill=False, alpha=0.5, color=color, linewidth=1.5) ax.add_patch(circle) elif defense == 'odor': # Odor: Draw a diffuse cloud around the porcupine circle = Circle(pos, 0.7, alpha=0.3, color=color, linewidth=0) ax.add_patch(circle) elif defense == 'physical': # Physical: Draw quills (spikes) around the porcupine for angle in np.linspace(0, 2*np.pi, 8, endpoint=False): dx = 0.3 * np.cos(angle) dy = 0.3 * np.sin(angle) ax.arrow(pos[0], pos[1], dx, dy, head_width=0.08, head_length=0.08, fc=color, ec=color, alpha=0.9) # Add a legend legend_elements = [] for defense, color in defense_colors.items(): if defense in defense_types: legend_elements.append(plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=color, markersize=10, label=defense.capitalize())) if best_position is not None: legend_elements.append(plt.Line2D([0], [0], marker='*', color='w', markerfacecolor='red', markersize=15, label='Best Position')) ax.legend(handles=legend_elements) # Set labels and title ax.set_title(title) ax.set_xlabel('x1') ax.set_ylabel('x2') ax.grid(True, linestyle='--', alpha=0.3) if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig
[docs]def animate_defense_mechanisms( position_history: List[np.ndarray], defense_history: List[List[str]], bounds: Tuple[np.ndarray, np.ndarray], best_position_history: Optional[List[np.ndarray]] = None, interval: int = 200, figsize: Tuple[int, int] = (12, 10), save_path: Optional[str] = None, dpi: int = 100 ): """ Create an animation of porcupines using different defense mechanisms over time. Parameters ---------- position_history : list List of position arrays at each iteration, each with shape (pop_size, 2). defense_history : list List of lists containing defense mechanisms used by each porcupine at each iteration. bounds : tuple A tuple (lb, ub) containing the lower and upper bounds. best_position_history : list, optional List of best positions at each iteration, each with shape (2,). interval : int, optional Interval between frames in milliseconds (default: 200). figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 10)). save_path : str, optional Path to save the animation. If None, the animation is not saved (default: None). dpi : int, optional DPI for the saved animation (default: 100). Returns ------- matplotlib.animation.FuncAnimation The created animation. """ if not position_history: raise ValueError("Position history is empty") if position_history[0].shape[1] != 2: raise ValueError("This function only works for 2D search spaces") # Create the figure and axis fig, ax = plt.subplots(figsize=figsize) # Define colors for different defense mechanisms defense_colors = { 'sight': 'blue', 'sound': 'green', 'odor': 'orange', 'physical': 'red' } # Set the plot limits based on the bounds lb, ub = bounds ax.set_xlim(lb[0], ub[0]) ax.set_ylim(lb[1], ub[1]) # Initialize scatter plot for porcupines scatter_porcupines = ax.scatter([], [], c=[], edgecolors='black', s=80, zorder=10) # Initialize scatter plot for best position scatter_best = ax.scatter([], [], c='red', s=150, marker='*', zorder=15) # Initialize patches for defense mechanisms sight_patches = [] sound_patches = [] odor_patches = [] physical_patches = [] # Create initial patches for each porcupine n_porcupines = position_history[0].shape[0] for i in range(n_porcupines): # Sight: Wedge for cone of vision sight_patch = Wedge((0, 0), 1.0, 0, 60, alpha=0.2, color='blue', width=0.8) sight_patches.append(sight_patch) ax.add_patch(sight_patch) # Sound: Concentric circles sound_circles = [] for radius in [0.3, 0.6, 0.9]: circle = Circle((0, 0), radius, fill=False, alpha=0.5, color='green', linewidth=1.5) sound_circles.append(circle) ax.add_patch(circle) sound_patches.append(sound_circles) # Odor: Diffuse cloud odor_patch = Circle((0, 0), 0.7, alpha=0.3, color='orange', linewidth=0) odor_patches.append(odor_patch) ax.add_patch(odor_patch) # Physical: Quills (will be drawn as arrows in the update function) physical_arrows = [] for angle in np.linspace(0, 2*np.pi, 8, endpoint=False): arrow = ax.arrow(0, 0, 0, 0, head_width=0.08, head_length=0.08, fc='red', ec='red', alpha=0.9) physical_arrows.append(arrow) physical_patches.append(physical_arrows) # Set labels and title ax.set_title('Defense Mechanisms Animation') ax.set_xlabel('x1') ax.set_ylabel('x2') ax.grid(True, linestyle='--', alpha=0.3) # Add a legend legend_elements = [] for defense, color in defense_colors.items(): legend_elements.append(plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=color, markersize=10, label=defense.capitalize())) legend_elements.append(plt.Line2D([0], [0], marker='*', color='w', markerfacecolor='red', markersize=15, label='Best Position')) ax.legend(handles=legend_elements) # Text for iteration number iteration_text = ax.text(0.02, 0.98, '', transform=ax.transAxes, verticalalignment='top', fontsize=10) # Animation update function def update(frame): positions = position_history[frame] defenses = defense_history[frame] # Update porcupine positions scatter_porcupines.set_offsets(positions) # Update porcupine colors based on defense mechanisms colors = [defense_colors.get(defense, 'gray') for defense in defenses] scatter_porcupines.set_color(colors) # Update best position if best_position_history is not None: best_pos = best_position_history[frame] scatter_best.set_offsets([best_pos]) # Calculate movement vectors if not the first frame if frame > 0: prev_positions = position_history[frame - 1] else: prev_positions = positions # No movement for the first frame # Update defense mechanism visualizations for i, (pos, defense) in enumerate(zip(positions, defenses)): # Hide all defense mechanisms first sight_patches[i].set_visible(False) for circle in sound_patches[i]: circle.set_visible(False) odor_patches[i].set_visible(False) for arrow in physical_patches[i]: arrow.set_visible(False) # Show only the active defense mechanism if defense == 'sight': movement = pos - prev_positions[i] angle = np.arctan2(movement[1], movement[0]) if np.linalg.norm(movement) > 0 else 0 sight_patches[i].set_center(pos) sight_patches[i].set_theta1(angle * 180 / np.pi - 30) sight_patches[i].set_theta2(angle * 180 / np.pi + 30) sight_patches[i].set_visible(True) elif defense == 'sound': for circle in sound_patches[i]: circle.set_center(pos) circle.set_visible(True) elif defense == 'odor': odor_patches[i].set_center(pos) odor_patches[i].set_visible(True) elif defense == 'physical': for j, angle in enumerate(np.linspace(0, 2*np.pi, 8, endpoint=False)): dx = 0.3 * np.cos(angle) dy = 0.3 * np.sin(angle) # Remove old arrow and create a new one physical_patches[i][j].remove() physical_patches[i][j] = ax.arrow(pos[0], pos[1], dx, dy, head_width=0.08, head_length=0.08, fc='red', ec='red', alpha=0.9) # Update iteration text iteration_text.set_text(f'Iteration: {frame+1}/{len(position_history)}') # Return all artists that need to be redrawn return [scatter_porcupines, scatter_best, iteration_text] # Create the animation anim = animation.FuncAnimation(fig, update, frames=len(position_history), interval=interval, blit=True) # Save the animation if a path is provided if save_path: anim.save(save_path, dpi=dpi, writer='pillow') return anim
[docs]def plot_defense_effectiveness( defense_history: Dict[str, List[int]], fitness_history: List[float], title: str = "Defense Mechanism Effectiveness", figsize: Tuple[int, int] = (12, 8), save_path: Optional[str] = None ): """ Plot the effectiveness of each defense mechanism in relation to fitness improvement. Parameters ---------- defense_history : dict Dictionary with keys as defense mechanisms and values as lists of activation counts. fitness_history : list List of best fitness values at each iteration. title : str, optional Title of the plot (default: "Defense Mechanism Effectiveness"). figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 8)). save_path : str, optional Path to save the figure. If None, the figure is not saved (default: None). Returns ------- matplotlib.figure.Figure The created figure. """ # Create the figure and axes fig, (ax1, ax2) = plt.subplots(2, 1, figsize=figsize, sharex=True, gridspec_kw={'height_ratios': [2, 1]}) # Define colors for different defense mechanisms defense_colors = { 'sight': 'blue', 'sound': 'green', 'odor': 'orange', 'physical': 'red' } # Plot defense mechanism activation counts iterations = np.arange(1, len(list(defense_history.values())[0]) + 1) for defense, counts in defense_history.items(): ax1.plot(iterations, counts, color=defense_colors.get(defense, 'gray'), linewidth=2, label=defense.capitalize()) ax1.set_title(title) ax1.set_ylabel('Activation Count') ax1.grid(True, linestyle='--', alpha=0.7) ax1.legend() # Plot fitness history ax2.plot(iterations, fitness_history, 'k-', linewidth=2) ax2.set_xlabel('Iterations') ax2.set_ylabel('Best Fitness') ax2.grid(True, linestyle='--', alpha=0.7) # Calculate correlation between defense mechanism usage and fitness improvement correlations = {} for defense, counts in defense_history.items(): # Calculate fitness improvements fitness_improvements = np.zeros_like(fitness_history) for i in range(1, len(fitness_history)): fitness_improvements[i] = fitness_history[i-1] - fitness_history[i] # Calculate correlation correlation = np.corrcoef(counts, fitness_improvements)[0, 1] correlations[defense] = correlation # Add correlation information as text correlation_text = "Correlation with Fitness Improvement:\n" for defense, corr in correlations.items(): correlation_text += f"{defense.capitalize()}: {corr:.4f}\n" ax1.text(0.02, 0.02, correlation_text, transform=ax1.transAxes, verticalalignment='bottom', horizontalalignment='left', bbox=dict(facecolor='white', alpha=0.7)) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig
[docs]def visualize_quill_directions( positions: np.ndarray, velocities: np.ndarray, bounds: Tuple[np.ndarray, np.ndarray], title: str = "Porcupine Quill Directions", figsize: Tuple[int, int] = (10, 8), save_path: Optional[str] = None ): """ Visualize porcupines with quills pointing in the direction of their movement. Parameters ---------- positions : ndarray Positions of the porcupines, shape (pop_size, 2). velocities : ndarray Velocities of the porcupines, shape (pop_size, 2). bounds : tuple A tuple (lb, ub) containing the lower and upper bounds. title : str, optional Title of the plot (default: "Porcupine Quill Directions"). figsize : tuple, optional Figure size as (width, height) in inches (default: (10, 8)). save_path : str, optional Path to save the figure. If None, the figure is not saved (default: None). Returns ------- matplotlib.figure.Figure The created figure. """ if positions.shape[1] != 2: raise ValueError("This function only works for 2D search spaces") # Create the figure and axis fig, ax = plt.subplots(figsize=figsize) # Set the plot limits based on the bounds lb, ub = bounds ax.set_xlim(lb[0], ub[0]) ax.set_ylim(lb[1], ub[1]) # Plot each porcupine with directional quills for i, (pos, vel) in enumerate(zip(positions, velocities)): # Plot the porcupine ax.scatter(pos[0], pos[1], c='brown', edgecolors='black', s=100, zorder=10) # Normalize velocity for quill direction vel_norm = vel / (np.linalg.norm(vel) + 1e-10) # Avoid division by zero # Draw quills in the direction of movement and surrounding directions main_angle = np.arctan2(vel_norm[1], vel_norm[0]) # Draw main quill (longer) in the direction of movement main_length = 0.4 ax.arrow(pos[0], pos[1], main_length * vel_norm[0], main_length * vel_norm[1], head_width=0.1, head_length=0.1, fc='black', ec='black', zorder=11) # Draw surrounding quills (shorter) for angle_offset in np.linspace(-np.pi/2, np.pi/2, 5): if angle_offset != 0: # Skip the main direction angle = main_angle + angle_offset dx = 0.2 * np.cos(angle) dy = 0.2 * np.sin(angle) ax.arrow(pos[0], pos[1], dx, dy, head_width=0.05, head_length=0.05, fc='black', ec='black', alpha=0.7, zorder=11) # Set labels and title ax.set_title(title) ax.set_xlabel('x1') ax.set_ylabel('x2') ax.grid(True, linestyle='--', alpha=0.3) if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig