Source code for porcupy.utils.visualization_manager

"""
Visualization manager for the Crested Porcupine Optimizer (CPO).

This module provides a unified interface for all visualization tools in the Porcupy library.
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from typing import List, Tuple, Optional, Union, Dict, Any, Callable

# Import visualization modules
from porcupy.utils.enhanced_visualization import (
    plot_defense_mechanisms,
    plot_population_cycles,
    plot_diversity_history,
    plot_2d_porcupines,
    animate_porcupines_2d,
    plot_3d_porcupines,
    calculate_diversity,
    track_defense_mechanisms
)

from porcupy.utils.interactive_visualization import (
    OptimizationDashboard,
    ParameterTuningDashboard,
    create_interactive_optimization_plot
)

from porcupy.utils.defense_visualization import (
    visualize_defense_territories,
    visualize_defense_mechanisms,
    animate_defense_mechanisms,
    plot_defense_effectiveness,
    visualize_quill_directions
)

from porcupy.utils.population_visualization import (
    plot_population_reduction_strategies,
    plot_population_diversity_map,
    animate_population_cycle,
    plot_exploration_exploitation_balance,
    plot_diversity_vs_convergence
)


[docs]class CPOVisualizer: """ Unified interface for all CPO visualization tools. This class provides access to all visualization capabilities for the Crested Porcupine Optimizer algorithm. """ def __init__( self, objective_func: Optional[Callable] = None, bounds: Optional[Tuple[np.ndarray, np.ndarray]] = None ): """ Initialize the CPO visualizer. Parameters ---------- objective_func : callable, optional The objective function being optimized. bounds : tuple, optional A tuple (lb, ub) containing the lower and upper bounds. """
[docs] self.objective_func = objective_func
[docs] self.bounds = bounds
# Data storage
[docs] self.position_history = []
[docs] self.best_position_history = []
[docs] self.fitness_history = []
[docs] self.pop_size_history = []
[docs] self.defense_history = {}
[docs] self.defense_types_history = []
[docs] self.diversity_history = []
[docs] def record_iteration( self, positions: np.ndarray, best_position: np.ndarray, fitness: np.ndarray, pop_size: int, defense_types: Optional[List[str]] = None ): """ Record data from a single iteration for visualization. Parameters ---------- positions : numpy.ndarray Positions of all porcupines in the current iteration. best_position : numpy.ndarray Best position found so far. fitness : numpy.ndarray Fitness values of all porcupines in the current iteration. pop_size : int Current population size. defense_types : list of str, optional Types of defense mechanisms used by each porcupine in the current iteration. """ # Store position and fitness data self.position_history.append(positions.copy()) self.best_position_history.append(best_position.copy()) self.fitness_history.append(fitness.copy()) # Store defense types if provided if defense_types is not None: # Initialize defense_types_history if it doesn't exist if not hasattr(self, 'defense_types_history'): self.defense_types_history = [] self.defense_types_history.append(defense_types) # Also store in defense_history for backward compatibility if 'defense_types' not in self.defense_history: self.defense_history['defense_types'] = [] self.defense_history['defense_types'].append(defense_types) self.pop_size_history.append(pop_size) # Calculate and store diversity diversity = calculate_diversity(positions) self.diversity_history.append(diversity) # Store defense mechanism data if defense_types is not None: for defense in ['sight', 'sound', 'odor', 'physical']: if defense not in self.defense_history: self.defense_history[defense] = [] self.defense_history[defense].append(defense_types.count(defense))
[docs] def create_dashboard( self, update_interval: float = 0.5, figsize: Tuple[int, int] = (15, 10) ) -> OptimizationDashboard: """ Create an interactive dashboard for monitoring optimization. Parameters ---------- update_interval : float, optional Time interval between dashboard updates in seconds (default: 0.5). figsize : tuple, optional Figure size as (width, height) in inches (default: (15, 10)). Returns ------- OptimizationDashboard The created dashboard. """ if self.objective_func is None or self.bounds is None: raise ValueError("objective_func and bounds must be provided to create a dashboard") dimensions = self.position_history[0].shape[1] if self.position_history else 2 dashboard = OptimizationDashboard( objective_func=self.objective_func, bounds=self.bounds, dimensions=dimensions, update_interval=update_interval, figsize=figsize ) return dashboard
[docs] def visualize_defense_mechanisms( self, title: str = "Defense Mechanism Activation", figsize: Tuple[int, int] = (12, 6), save_path: Optional[str] = None ): """ Visualize the activation of different defense mechanisms over iterations. Parameters ---------- title : str, optional Title of the plot (default: "Defense Mechanism Activation"). figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 6)). 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 not self.position_history: raise ValueError("No optimization data recorded") # Check if we have defense mechanism data # First check if we have defense_types_history directly (from CPO class) if hasattr(self, 'defense_types_history') and self.defense_types_history: defense_data = self.defense_types_history has_direct_data = True # Then check if we have it in the defense_history dictionary elif self.defense_history and 'defense_types' in self.defense_history and self.defense_history['defense_types']: defense_data = self.defense_history['defense_types'] has_direct_data = True else: has_direct_data = False raise ValueError("No defense mechanism data recorded. Make sure to record defense types during optimization.") # Create figure with subplots fig = plt.figure(figsize=figsize) gs = fig.add_gridspec(2, 2, hspace=0.3, wspace=0.3) # 1. Defense Mechanism Usage Over Time ax1 = fig.add_subplot(gs[0, 0]) # Count defense mechanisms at each iteration iterations = len(defense_data) sight_counts = [] sound_counts = [] odor_counts = [] physical_counts = [] for i in range(iterations): defenses = defense_data[i] sight_counts.append(defenses.count('sight')) sound_counts.append(defenses.count('sound')) odor_counts.append(defenses.count('odor')) physical_counts.append(defenses.count('physical')) # Plot usage over time x = range(iterations) ax1.plot(x, sight_counts, 'b-', label='Sight', linewidth=2) ax1.plot(x, sound_counts, 'g-', label='Sound', linewidth=2) ax1.plot(x, odor_counts, 'orange', label='Odor', linewidth=2) ax1.plot(x, physical_counts, 'r-', label='Physical', linewidth=2) ax1.set_title('Defense Mechanism Usage') ax1.set_xlabel('Iteration') ax1.set_ylabel('Count') ax1.legend() ax1.grid(True, alpha=0.3) # 2. Exploration vs Exploitation Balance ax2 = fig.add_subplot(gs[0, 1]) exploration = [sight + sound for sight, sound in zip(sight_counts, sound_counts)] exploitation = [odor + physical for odor, physical in zip(odor_counts, physical_counts)] ax2.stackplot(x, exploration, exploitation, labels=['Exploration (Sight/Sound)', 'Exploitation (Odor/Physical)'], colors=['#3498db', '#e74c3c'], alpha=0.7) ax2.set_title('Exploration-Exploitation Balance') ax2.set_xlabel('Iteration') ax2.set_ylabel('Count') ax2.legend() ax2.grid(True, alpha=0.3) # 3. Fitness Improvement by Defense Mechanism ax3 = fig.add_subplot(gs[1, 0]) # Calculate fitness improvement for each defense mechanism defense_improvement = { 'sight': [], 'sound': [], 'odor': [], 'physical': [] } # Use best fitness from each iteration fitness_values = self.fitness_history # Skip first iteration as we can't calculate improvement for i in range(1, iterations): prev_fitness = fitness_values[i-1] if i > 0 else fitness_values[0] current_fitness = fitness_values[i] # Extract scalar values for comparison if isinstance(prev_fitness, np.ndarray) and isinstance(current_fitness, np.ndarray): # Take the minimum (best) fitness from each array prev_best = np.min(prev_fitness) current_best = np.min(current_fitness) improvement = prev_best - current_best else: # Already scalar values improvement = prev_fitness - current_fitness # Count improvements by defense mechanism defenses = defense_data[i-1] for defense in ['sight', 'sound', 'odor', 'physical']: count = defenses.count(defense) if count > 0: defense_improvement[defense].append(improvement / count if improvement > 0 else 0) else: defense_improvement[defense].append(0) # Calculate cumulative improvement for defense in defense_improvement: defense_improvement[defense] = np.cumsum(defense_improvement[defense]) # Plot cumulative improvement ax3.plot(range(1, iterations), defense_improvement['sight'], 'b-', label='Sight', linewidth=2) ax3.plot(range(1, iterations), defense_improvement['sound'], 'g-', label='Sound', linewidth=2) ax3.plot(range(1, iterations), defense_improvement['odor'], 'orange', label='Odor', linewidth=2) ax3.plot(range(1, iterations), defense_improvement['physical'], 'r-', label='Physical', linewidth=2) ax3.set_title('Cumulative Fitness Improvement') ax3.set_xlabel('Iteration') ax3.set_ylabel('Cumulative Improvement') ax3.legend() ax3.grid(True, alpha=0.3) # 4. Defense Mechanism Effectiveness Pie Chart ax4 = fig.add_subplot(gs[1, 1]) # Calculate total improvement by each defense mechanism total_improvement = {} for defense in defense_improvement: if len(defense_improvement[defense]) > 0: total_improvement[defense] = defense_improvement[defense][-1] else: total_improvement[defense] = 0 # Create pie chart labels = ['Sight', 'Sound', 'Odor', 'Physical'] sizes = [total_improvement['sight'], total_improvement['sound'], total_improvement['odor'], total_improvement['physical']] colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c'] # Ensure we don't have negative values for the pie chart sizes = [max(0, size) for size in sizes] # If all sizes are 0, set equal values if sum(sizes) == 0: sizes = [1, 1, 1, 1] ax4.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', shadow=True, startangle=90) ax4.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle ax4.set_title('Overall Defense Mechanism Effectiveness') # Set the main title fig.suptitle(title, fontsize=16) # Save figure if path provided if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig
[docs] def visualize_population_cycles( self, cycles: int, max_iter: int, title: str = "Population Size Cycles", figsize: Tuple[int, int] = (12, 6), save_path: Optional[str] = None ): """ Visualize the population size changes over cycles. Parameters ---------- cycles : int Number of cycles used in the optimization. max_iter : int Maximum number of iterations. title : str, optional Title of the plot (default: "Population Size Cycles"). figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 6)). 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 not self.pop_size_history: raise ValueError("No population size data recorded") return plot_population_cycles( pop_size_history=self.pop_size_history, cycles=cycles, max_iter=max_iter, title=title, figsize=figsize, save_path=save_path )
[docs] def visualize_diversity_history( self, title: str = "Population Diversity History", figsize: Tuple[int, int] = (10, 6), save_path: Optional[str] = None ): """ Visualize the diversity history of the population. Parameters ---------- title : str, optional Title of the plot (default: "Population Diversity History"). figsize : tuple, optional Figure size as (width, height) in inches (default: (10, 6)). 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 not self.diversity_history: raise ValueError("No diversity data recorded") return plot_diversity_history( diversity_history=self.diversity_history, title=title, figsize=figsize, save_path=save_path )
[docs] def visualize_porcupines_2d( self, iteration: int = -1, title: str = "Porcupine Positions", figsize: Tuple[int, int] = (10, 8), cmap: str = 'viridis', contour_levels: int = 20, quill_length: float = 0.5, save_path: Optional[str] = None ): """ Visualize porcupines in 2D search space at a specific iteration. Parameters ---------- iteration : int, optional Iteration to visualize. Default is -1 (last iteration). title : str, optional Title of the plot (default: "Porcupine Positions"). figsize : tuple, optional Figure size as (width, height) in inches (default: (10, 8)). cmap : str, optional Colormap for the contour plot (default: 'viridis'). contour_levels : int, optional Number of contour levels (default: 20). quill_length : float, optional Length of the directional quills (default: 0.5). 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 not self.position_history: raise ValueError("No position data recorded") if self.objective_func is None or self.bounds is None: raise ValueError("objective_func and bounds must be provided for this visualization") # Get positions for the specified iteration positions = self.position_history[iteration] # Get defense types if available defense_types = None if self.defense_history: defense_types = [] for defense in ['sight', 'sound', 'odor', 'physical']: count = self.defense_history[defense][iteration] defense_types.extend([defense] * count) # Get best position if available best_pos = self.best_position_history[iteration] if self.best_position_history else None return plot_2d_porcupines( positions=positions, func=self.objective_func, bounds=self.bounds, best_pos=best_pos, defense_types=defense_types, title=title, figsize=figsize, cmap=cmap, contour_levels=contour_levels, quill_length=quill_length, save_path=save_path )
[docs] def animate_optimization( self, interval: int = 200, figsize: Tuple[int, int] = (10, 8), cmap: str = 'viridis', contour_levels: int = 20, quill_length: float = 0.5, save_path: Optional[str] = None, dpi: int = 100 ): """ Create an animation of the optimization process in 2D. Parameters ---------- interval : int, optional Interval between frames in milliseconds (default: 200). figsize : tuple, optional Figure size as (width, height) in inches (default: (10, 8)). cmap : str, optional Colormap for the contour plot (default: 'viridis'). contour_levels : int, optional Number of contour levels (default: 20). quill_length : float, optional Length of the directional quills (default: 0.5). 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 self.position_history: raise ValueError("No position data recorded") if self.objective_func is None or self.bounds is None: raise ValueError("objective_func and bounds must be provided for this animation") # Create defense history in the required format defense_history = None if self.defense_history: defense_history = [] for i in range(len(self.position_history)): defenses = [] for defense in ['sight', 'sound', 'odor', 'physical']: count = self.defense_history[defense][i] defenses.extend([defense] * count) defense_history.append(defenses) return animate_porcupines_2d( position_history=self.position_history, func=self.objective_func, bounds=self.bounds, defense_history=defense_history, best_pos_history=self.best_position_history, interval=interval, figsize=figsize, cmap=cmap, contour_levels=contour_levels, quill_length=quill_length, save_path=save_path, dpi=dpi )
[docs] def create_animation( self, positions_history: List[np.ndarray], best_position_history: List[np.ndarray], title: str = "CPO Optimization Process", save_path: Optional[str] = None, fps: int = 5, defense_types_history: Optional[List[List[str]]] = None, figsize: Tuple[int, int] = (12, 10), dpi: int = 100, show_exploration_exploitation: bool = True ): """ Create an enhanced animation of the optimization process with defense mechanisms. Parameters ---------- positions_history : List[np.ndarray] List of position arrays for each iteration. best_position_history : List[np.ndarray] List of best positions for each iteration. title : str, optional Title of the animation (default: "CPO Optimization Process"). save_path : str, optional Path to save the animation. If None, the animation is not saved (default: None). fps : int, optional Frames per second for the animation (default: 5). defense_types_history : List[List[str]], optional List of defense types used by each porcupine at each iteration. figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 10)). dpi : int, optional DPI for the saved animation (default: 100). show_exploration_exploitation : bool, optional Whether to show exploration-exploitation balance subplot (default: True). Returns ------- matplotlib.animation.FuncAnimation The created animation. """ if self.objective_func is None or self.bounds is None: raise ValueError("objective_func and bounds must be provided for this visualization") # Create figure with subplots if show_exploration_exploitation: fig = plt.figure(figsize=figsize) gs = fig.add_gridspec(2, 2, height_ratios=[3, 1], width_ratios=[3, 1], hspace=0.3, wspace=0.3) ax_main = fig.add_subplot(gs[0, :]) ax_exp_vs_expl = fig.add_subplot(gs[1, 0]) ax_defense_pie = fig.add_subplot(gs[1, 1]) else: fig, ax_main = plt.subplots(figsize=figsize) # Generate meshgrid for contour plot lb, ub = self.bounds x = np.linspace(lb[0], ub[0], 100) y = np.linspace(lb[1], ub[1], 100) X, Y = np.meshgrid(x, y) Z = np.zeros_like(X) for i in range(X.shape[0]): for j in range(X.shape[1]): Z[i, j] = self.objective_func(np.array([X[i, j], Y[i, j]])) # Create contour plot on main axis contour = ax_main.contourf(X, Y, Z, 50, cmap='viridis', alpha=0.6) fig.colorbar(contour, ax=ax_main, label='Objective Function Value') # Initialize scatter plots for different defense mechanisms sight_scatter = ax_main.scatter([], [], c='blue', s=80, label='Sight', alpha=0.7) sound_scatter = ax_main.scatter([], [], c='green', s=80, label='Sound', alpha=0.7) odor_scatter = ax_main.scatter([], [], c='orange', s=80, label='Odor', alpha=0.7) physical_scatter = ax_main.scatter([], [], c='red', s=80, label='Physical', alpha=0.7) best_scatter = ax_main.scatter([], [], c='yellow', s=150, marker='*', edgecolors='black', label='Best Position', zorder=10) # Add trajectory line for best position trajectory_line, = ax_main.plot([], [], 'r-', linewidth=1.5, alpha=0.5, label='Best Trajectory') # Set title and labels for main plot ax_main.set_title(title) ax_main.set_xlabel('x1') ax_main.set_ylabel('x2') ax_main.legend(loc='upper right') ax_main.grid(True, alpha=0.3) # Set axis limits ax_main.set_xlim(lb[0], ub[0]) ax_main.set_ylim(lb[1], ub[1]) # Text for iteration counter and population size iteration_text = ax_main.text(0.02, 0.98, '', transform=ax_main.transAxes, verticalalignment='top', fontsize=10) # Initialize exploration-exploitation subplot if needed if show_exploration_exploitation: # Set up exploration vs exploitation subplot ax_exp_vs_expl.set_title('Exploration-Exploitation Balance') ax_exp_vs_expl.set_xlabel('Iteration') ax_exp_vs_expl.set_ylabel('Count') ax_exp_vs_expl.grid(True, alpha=0.3) # Set up defense pie chart ax_defense_pie.set_title('Defense Mechanisms') ax_defense_pie.axis('equal') # Initialize exploration-exploitation plot data exp_line, = ax_exp_vs_expl.plot([], [], 'b-', label='Exploration (Sight/Sound)') expl_line, = ax_exp_vs_expl.plot([], [], 'r-', label='Exploitation (Odor/Physical)') ax_exp_vs_expl.legend(loc='upper left', fontsize=8) # Set initial limits for exploration-exploitation plot ax_exp_vs_expl.set_xlim(0, len(positions_history)) ax_exp_vs_expl.set_ylim(0, max([len(pos) for pos in positions_history]) + 2) # Update function for animation def update(frame): # Clear previous points sight_scatter.set_offsets(np.empty((0, 2))) sound_scatter.set_offsets(np.empty((0, 2))) odor_scatter.set_offsets(np.empty((0, 2))) physical_scatter.set_offsets(np.empty((0, 2))) # Get positions for current frame positions = positions_history[frame] # Update best position and trajectory best_pos = best_position_history[frame] best_scatter.set_offsets([best_pos[0], best_pos[1]]) # Update trajectory line if frame > 0: traj_x = [pos[0] for pos in best_position_history[:frame+1]] traj_y = [pos[1] for pos in best_position_history[:frame+1]] trajectory_line.set_data(traj_x, traj_y) # Update iteration text iteration_text.set_text(f'Iteration: {frame+1}/{len(positions_history)}\n' f'Population Size: {len(positions)}') # If defense types are provided, color points accordingly if defense_types_history is not None: defenses = defense_types_history[frame] sight_pos = [] sound_pos = [] odor_pos = [] physical_pos = [] # Count defenses for exploration-exploitation balance sight_count = 0 sound_count = 0 odor_count = 0 physical_count = 0 for i, defense in enumerate(defenses): if i < len(positions): # Ensure we don't go out of bounds if defense == 'sight': sight_pos.append(positions[i]) sight_count += 1 elif defense == 'sound': sound_pos.append(positions[i]) sound_count += 1 elif defense == 'odor': odor_pos.append(positions[i]) odor_count += 1 elif defense == 'physical': physical_pos.append(positions[i]) physical_count += 1 # Update exploration-exploitation plots if enabled if show_exploration_exploitation: # Update exploration-exploitation balance plot exploration = [0] * (frame + 1) exploitation = [0] * (frame + 1) # Calculate exploration-exploitation for all frames up to current for i in range(frame + 1): curr_defenses = defense_types_history[i] exp_count = sum(1 for d in curr_defenses if d in ['sight', 'sound']) expl_count = sum(1 for d in curr_defenses if d in ['odor', 'physical']) exploration[i] = exp_count exploitation[i] = expl_count # Update lines exp_line.set_data(range(frame + 1), exploration) expl_line.set_data(range(frame + 1), exploitation) # Update pie chart ax_defense_pie.clear() ax_defense_pie.set_title('Defense Mechanisms') labels = ['Sight', 'Sound', 'Odor', 'Physical'] sizes = [sight_count, sound_count, odor_count, physical_count] colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c'] # Only create pie if we have non-zero values if sum(sizes) > 0: ax_defense_pie.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, textprops={'fontsize': 8}) ax_defense_pie.axis('equal') if sight_pos: sight_scatter.set_offsets(sight_pos) if sound_pos: sound_scatter.set_offsets(sound_pos) if odor_pos: odor_scatter.set_offsets(odor_pos) if physical_pos: physical_scatter.set_offsets(physical_pos) else: # If no defense types, show all points in one color sight_scatter.set_offsets(positions) return sight_scatter, sound_scatter, odor_scatter, physical_scatter, best_scatter, trajectory_line, iteration_text # Create animation anim = FuncAnimation(fig, update, frames=len(positions_history), interval=1000/fps, blit=True) # Save animation if path is provided if save_path: print(f"Generating animation (this may take a moment)...") anim.save(save_path, writer='pillow', fps=fps, dpi=dpi) plt.close(fig) return anim
[docs] def visualize_defense_territories( self, iteration: int = -1, title: str = "Defense Territories", figsize: Tuple[int, int] = (10, 8), save_path: Optional[str] = None ): """ Visualize the defense territories of porcupines. Parameters ---------- iteration : int, optional Iteration to visualize. Default is -1 (last iteration). 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 not self.position_history: raise ValueError("No position data recorded") if self.bounds is None: raise ValueError("bounds must be provided for this visualization") # Get positions for the specified iteration positions = self.position_history[iteration] # Get defense types if available defense_types = None if self.defense_history: defense_types = [] for defense in ['sight', 'sound', 'odor', 'physical']: count = self.defense_history[defense][iteration] defense_types.extend([defense] * count) else: # If no defense data, create random defense types defense_options = ['sight', 'sound', 'odor', 'physical'] defense_types = [defense_options[i % 4] for i in range(len(positions))] return visualize_defense_territories( positions=positions, defense_types=defense_types, bounds=self.bounds, title=title, figsize=figsize, save_path=save_path )
[docs] def visualize_exploration_exploitation( self, sample_iterations: Optional[List[int]] = None, figsize: Tuple[int, int] = (15, 10), save_path: Optional[str] = None ): """ Visualize the balance between exploration and exploitation. Parameters ---------- sample_iterations : list, optional List of iteration indices to visualize. If None, evenly spaced iterations are selected. figsize : tuple, optional Figure size as (width, height) in inches (default: (15, 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 not self.position_history or not self.best_position_history: raise ValueError("No position data recorded") if self.bounds is None: raise ValueError("bounds must be provided for this visualization") # If no sample iterations provided, select evenly spaced iterations if sample_iterations is None: n_samples = min(6, len(self.position_history)) sample_iterations = np.linspace(0, len(self.position_history) - 1, n_samples, dtype=int).tolist() return plot_exploration_exploitation_balance( positions_history=self.position_history, best_positions_history=self.best_position_history, bounds=self.bounds, sample_iterations=sample_iterations, figsize=figsize, save_path=save_path )
[docs] def visualize_diversity_vs_convergence( self, cycles: int, max_iter: int, title: str = "Diversity vs Convergence", figsize: Tuple[int, int] = (12, 6), save_path: Optional[str] = None ): """ Visualize the relationship between population diversity and convergence. Parameters ---------- cycles : int Number of cycles used in the optimization. max_iter : int Maximum number of iterations. title : str, optional Title of the plot (default: "Diversity vs Convergence"). figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 6)). 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 not self.diversity_history or not self.fitness_history: raise ValueError("No diversity or fitness data recorded") return plot_diversity_vs_convergence( diversity_history=self.diversity_history, fitness_history=self.fitness_history, cycles=cycles, max_iter=max_iter, title=title, figsize=figsize, save_path=save_path )
[docs] def visualize_defense_effectiveness( self, title: str = "Defense Mechanism Effectiveness", figsize: Tuple[int, int] = (12, 8), save_path: Optional[str] = None ): """ Visualize the effectiveness of each defense mechanism. Parameters ---------- 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. """ if not self.position_history or not self.fitness_history: raise ValueError("No optimization data recorded") # Check if we have defense mechanism data in the expected format if not self.defense_history: raise ValueError("No defense mechanism data recorded") # Ensure we have defense_types available if 'defense_types' not in self.defense_history: # If we have individual defense counts but not defense_types, we can't proceed # as we need the specific defense type for each porcupine raise ValueError("Defense mechanism types not recorded. This visualization requires defense_types data.") # Ensure defense_types has data if not self.defense_history['defense_types'] or len(self.defense_history['defense_types']) == 0: raise ValueError("Defense mechanism types list is empty") # Create figure with subplots fig = plt.figure(figsize=figsize) gs = fig.add_gridspec(2, 2, hspace=0.3, wspace=0.3) # 1. Defense Mechanism Usage Over Time ax1 = fig.add_subplot(gs[0, 0]) # Count defense mechanisms at each iteration iterations = len(self.defense_history['defense_types']) sight_counts = [] sound_counts = [] odor_counts = [] physical_counts = [] for i in range(iterations): defenses = self.defense_history['defense_types'][i] sight_counts.append(defenses.count('sight')) sound_counts.append(defenses.count('sound')) odor_counts.append(defenses.count('odor')) physical_counts.append(defenses.count('physical')) # Plot usage over time x = range(iterations) ax1.plot(x, sight_counts, 'b-', label='Sight', linewidth=2) ax1.plot(x, sound_counts, 'g-', label='Sound', linewidth=2) ax1.plot(x, odor_counts, 'orange', label='Odor', linewidth=2) ax1.plot(x, physical_counts, 'r-', label='Physical', linewidth=2) ax1.set_title('Defense Mechanism Usage') ax1.set_xlabel('Iteration') ax1.set_ylabel('Count') ax1.legend() ax1.grid(True, alpha=0.3) # 2. Exploration vs Exploitation Balance ax2 = fig.add_subplot(gs[0, 1]) exploration = [sight + sound for sight, sound in zip(sight_counts, sound_counts)] exploitation = [odor + physical for odor, physical in zip(odor_counts, physical_counts)] ax2.stackplot(x, exploration, exploitation, labels=['Exploration (Sight/Sound)', 'Exploitation (Odor/Physical)'], colors=['#3498db', '#e74c3c'], alpha=0.7) ax2.set_title('Exploration-Exploitation Balance') ax2.set_xlabel('Iteration') ax2.set_ylabel('Count') ax2.legend() ax2.grid(True, alpha=0.3) # 3. Fitness Improvement by Defense Mechanism ax3 = fig.add_subplot(gs[1, 0]) # Calculate fitness improvement for each defense mechanism defense_improvement = { 'sight': [], 'sound': [], 'odor': [], 'physical': [] } # Use best fitness from each iteration - extract scalar values fitness_values = [] for fitness_array in self.fitness_history: # If fitness is an array, take the minimum value (best fitness) if isinstance(fitness_array, np.ndarray) and fitness_array.size > 0: fitness_values.append(float(np.min(fitness_array))) # If it's already a scalar, use it directly elif np.isscalar(fitness_array): fitness_values.append(float(fitness_array)) # If it's a list, take the minimum value elif isinstance(fitness_array, list) and len(fitness_array) > 0: fitness_values.append(float(min(fitness_array))) # Default case else: fitness_values.append(0.0) # Skip first iteration as we can't calculate improvement if len(fitness_values) <= 1: # Not enough data for improvement calculation return fig for i in range(1, min(iterations, len(fitness_values))): prev_fitness = fitness_values[i-1] if i > 0 else fitness_values[0] current_fitness = fitness_values[i] # Extract scalar values for comparison if isinstance(prev_fitness, np.ndarray) and isinstance(current_fitness, np.ndarray): # Take the minimum (best) fitness from each array prev_best = np.min(prev_fitness) current_best = np.min(current_fitness) improvement = prev_best - current_best else: # Already scalar values improvement = prev_fitness - current_fitness # Count improvements by defense mechanism defenses = self.defense_history['defense_types'][i-1] for defense in ['sight', 'sound', 'odor', 'physical']: count = defenses.count(defense) if count > 0: defense_improvement[defense].append(improvement / count if improvement > 0 else 0) else: defense_improvement[defense].append(0) # Calculate cumulative improvement for defense in defense_improvement: defense_improvement[defense] = np.cumsum(defense_improvement[defense]) # Plot cumulative improvement ax3.plot(range(1, iterations), defense_improvement['sight'], 'b-', label='Sight', linewidth=2) ax3.plot(range(1, iterations), defense_improvement['sound'], 'g-', label='Sound', linewidth=2) ax3.plot(range(1, iterations), defense_improvement['odor'], 'orange', label='Odor', linewidth=2) ax3.plot(range(1, iterations), defense_improvement['physical'], 'r-', label='Physical', linewidth=2) ax3.set_title('Cumulative Fitness Improvement') ax3.set_xlabel('Iteration') ax3.set_ylabel('Cumulative Improvement') ax3.legend() ax3.grid(True, alpha=0.3) # 4. Defense Mechanism Effectiveness Pie Chart ax4 = fig.add_subplot(gs[1, 1]) # Calculate total improvement by each defense mechanism total_improvement = {} for defense in defense_improvement: # Check if the array is not empty if len(defense_improvement[defense]) > 0: # Get the last value from the array total_improvement[defense] = defense_improvement[defense][-1] else: total_improvement[defense] = 0 # Create pie chart labels = ['Sight', 'Sound', 'Odor', 'Physical'] sizes = [total_improvement['sight'], total_improvement['sound'], total_improvement['odor'], total_improvement['physical']] colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c'] # Ensure we don't have negative values for the pie chart sizes = [max(0, size) for size in sizes] # If all sizes are 0, set equal values if sum(sizes) == 0: sizes = [1, 1, 1, 1] ax4.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', shadow=True, startangle=90) ax4.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle ax4.set_title('Overall Defense Mechanism Effectiveness') # Set the main title fig.suptitle(title, fontsize=16) # Save figure if path provided if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig
[docs] def compare_reduction_strategies( self, max_iter: int, pop_size: int, cycles: int, strategies: List[str] = ['linear', 'cosine', 'exponential'], figsize: Tuple[int, int] = (12, 6), save_path: Optional[str] = None ): """ Compare different population reduction strategies. Parameters ---------- max_iter : int Maximum number of iterations. pop_size : int Initial population size. cycles : int Number of cycles. strategies : list, optional List of reduction strategies to compare (default: ['linear', 'cosine', 'exponential']). figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 6)). 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. """ return plot_population_reduction_strategies( max_iter=max_iter, pop_size=pop_size, cycles=cycles, strategies=strategies, figsize=figsize, save_path=save_path )
[docs] def record_from_optimizer(self, optimizer): """ Record data from a CPO optimizer instance. Parameters ---------- optimizer : CPO The CPO optimizer instance to record data from. """ # Record position history if hasattr(optimizer, 'positions_history') and optimizer.positions_history: self.position_history = optimizer.positions_history # Record best position history if hasattr(optimizer, 'best_positions_history') and optimizer.best_positions_history: self.best_position_history = optimizer.best_positions_history # Record fitness history if hasattr(optimizer, 'fitness_history') and optimizer.fitness_history: self.fitness_history = optimizer.fitness_history # Record population size history if hasattr(optimizer, 'pop_size_history') and optimizer.pop_size_history: self.pop_size_history = optimizer.pop_size_history # Record defense mechanism history if hasattr(optimizer, 'defense_types_history') and optimizer.defense_types_history: # Store in both places for compatibility self.defense_types_history = optimizer.defense_types_history if not self.defense_history: self.defense_history = {} self.defense_history['defense_types'] = optimizer.defense_types_history
[docs] def create_parameter_tuning_dashboard( self, parameter_name: str, parameter_range: List[float], result_metric: str = "Best Cost", figsize: Tuple[int, int] = (12, 8) ) -> ParameterTuningDashboard: """ Create a dashboard for parameter tuning and sensitivity analysis. Parameters ---------- parameter_name : str Name of the parameter being tuned. parameter_range : list List of parameter values to test. result_metric : str, optional Name of the result metric (default: "Best Cost"). figsize : tuple, optional Figure size as (width, height) in inches (default: (12, 8)). Returns ------- ParameterTuningDashboard The created dashboard. """ return ParameterTuningDashboard( parameter_name=parameter_name, parameter_range=parameter_range, result_metric=result_metric, figsize=figsize )