evobench-lib 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evobench/__init__.py +80 -0
- evobench/algorithms/__init__.py +19 -0
- evobench/algorithms/bee.py +155 -0
- evobench/algorithms/eda.py +122 -0
- evobench/algorithms/pso.py +115 -0
- evobench/base.py +87 -0
- evobench/benchmarks/__init__.py +56 -0
- evobench/benchmarks/multimodal.py +38 -0
- evobench/benchmarks/unimodal.py +113 -0
- evobench/stats/__init__.py +13 -0
- evobench/stats/analyzer.py +102 -0
- evobench/stats/core_tests.py +128 -0
- evobench/stats/post_hoc.py +151 -0
- evobench/stats/reporter.py +108 -0
- evobench/tools/__init__.py +8 -0
- evobench/tools/experiment_engine.py +189 -0
- evobench/tools/operators.py +339 -0
- evobench/tools/plotter.py +157 -0
- evobench_lib-1.0.0.dist-info/METADATA +526 -0
- evobench_lib-1.0.0.dist-info/RECORD +23 -0
- evobench_lib-1.0.0.dist-info/WHEEL +5 -0
- evobench_lib-1.0.0.dist-info/licenses/LICENSE +674 -0
- evobench_lib-1.0.0.dist-info/top_level.txt +1 -0
evobench/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
evobench: Comprehensive Benchmarking Suite for Evolutionary Algorithms
|
|
3
|
+
|
|
4
|
+
A Python framework for testing, comparing, and analyzing evolutionary algorithms
|
|
5
|
+
and metaheuristics. Includes three baseline implementations (PSO, EDA, ABC),
|
|
6
|
+
benchmark functions (Sphere, Rosenbrock, Ackley, Schwefel, Trid), and built-in
|
|
7
|
+
statistical analysis tools.
|
|
8
|
+
|
|
9
|
+
Basic Usage:
|
|
10
|
+
>>> from evobench import PSO, sphere
|
|
11
|
+
>>> bounds = [(-5, 5)] * 10
|
|
12
|
+
>>> optimizer = PSO(sphere, bounds, max_iterations=100)
|
|
13
|
+
>>> best_solution, best_fitness = optimizer.run()
|
|
14
|
+
>>> print(f"Best fitness: {best_fitness:.6e}")
|
|
15
|
+
|
|
16
|
+
For advanced usage, reproducibility, and statistical comparison, see:
|
|
17
|
+
- docs/getting-started/SETUP_CONFIG.md
|
|
18
|
+
- docs/guide/PERFORMANCE_AND_REPRODUCIBILITY.md
|
|
19
|
+
- docs/reference/index.md
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "1.0.0"
|
|
23
|
+
__author__ = "Enrique Gómez Linares, Victoria Galván Delgadillo"
|
|
24
|
+
__license__ = "GPL-3"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ALGORITHM EXPORTS
|
|
28
|
+
|
|
29
|
+
from evobench.algorithms import PSO, EDA, ABC
|
|
30
|
+
|
|
31
|
+
from evobench.base import EvolutionaryAlgorithm
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# BENCHMARK FUNCTION EXPORTS
|
|
35
|
+
|
|
36
|
+
from evobench.benchmarks import (
|
|
37
|
+
sphere,
|
|
38
|
+
rosenbrock,
|
|
39
|
+
ackley,
|
|
40
|
+
schwefel,
|
|
41
|
+
trid,
|
|
42
|
+
get_benchmark,
|
|
43
|
+
BENCHMARK_REGISTRY,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# STATISTICAL ANALYSIS EXPORTS
|
|
48
|
+
|
|
49
|
+
from .stats import analyze, stat_report
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# UTILITIES EXPORTS
|
|
53
|
+
|
|
54
|
+
from .tools import run as run_automated_experiment
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# PUBLIC API DEFINITION
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
# Core algorithms
|
|
61
|
+
"PSO",
|
|
62
|
+
"EDA",
|
|
63
|
+
"ABC",
|
|
64
|
+
"EvolutionaryAlgorithm",
|
|
65
|
+
# Benchmark functions (individual)
|
|
66
|
+
"sphere",
|
|
67
|
+
"rosenbrock",
|
|
68
|
+
"ackley",
|
|
69
|
+
"schwefel",
|
|
70
|
+
"trid",
|
|
71
|
+
# Benchmark utilities
|
|
72
|
+
"get_benchmark",
|
|
73
|
+
"BENCHMARK_REGISTRY",
|
|
74
|
+
# Statistical analysis
|
|
75
|
+
"analyze",
|
|
76
|
+
"stat_report",
|
|
77
|
+
# Experiment tools
|
|
78
|
+
"run_automated_experiment",
|
|
79
|
+
|
|
80
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optimization Algorithms Module.
|
|
3
|
+
|
|
4
|
+
This module provides implementations of various evolutionary and swarm-based
|
|
5
|
+
metaheuristics. The algorithms are exported with their standard acronyms
|
|
6
|
+
for ease of use.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Importamos las clases con sus nombres completos y les asignamos el alias oficial
|
|
10
|
+
from .eda import EstimationOfDistributionAlgorithm as EDA
|
|
11
|
+
from .pso import ParticleSwarmOptimization as PSO
|
|
12
|
+
from .bee import ArtificialBeeColony as ABC
|
|
13
|
+
|
|
14
|
+
# Definimos exactamente qué se expone cuando alguien importa este módulo
|
|
15
|
+
__all__ = [
|
|
16
|
+
"EDA",
|
|
17
|
+
"PSO",
|
|
18
|
+
"ABC"
|
|
19
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Callable, Tuple
|
|
3
|
+
from evobench.base import EvolutionaryAlgorithm
|
|
4
|
+
import evobench.tools.operators as ops
|
|
5
|
+
|
|
6
|
+
class ArtificialBeeColony(EvolutionaryAlgorithm):
|
|
7
|
+
"""
|
|
8
|
+
Artificial Bee Colony (ABC) metaheuristic for continuous optimization.
|
|
9
|
+
|
|
10
|
+
This algorithm mimics the intelligent foraging behavior of a honey bee swarm.
|
|
11
|
+
The population is divided into three groups: Employed Bees, Onlooker Bees,
|
|
12
|
+
and Scout Bees. It utilizes a trial-counter mechanism to manage food source
|
|
13
|
+
exhaustion, ensuring the swarm escapes local optima through a structured
|
|
14
|
+
stagnation-replacement strategy.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
objective_function: Callable[[np.ndarray], float],
|
|
20
|
+
bounds: np.ndarray,
|
|
21
|
+
population_size: int = 50,
|
|
22
|
+
max_iterations: int = 100,
|
|
23
|
+
limit: int = 20
|
|
24
|
+
) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Initializes the ABC hyperparameters and structural attributes.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
objective_function (Callable): The mathematical function to minimize.
|
|
30
|
+
bounds (np.ndarray): Spatial boundaries [min, max] for each dimension.
|
|
31
|
+
population_size (int): Total number of bees. Half will be employed.
|
|
32
|
+
max_iterations (int): Maximum number of search cycles.
|
|
33
|
+
limit (int): Trials allowed before a food source is abandoned.
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(objective_function, bounds, population_size, max_iterations)
|
|
36
|
+
|
|
37
|
+
# In ABC, the number of food sources is equal to half of the population
|
|
38
|
+
self.food_sources_count = population_size // 2
|
|
39
|
+
|
|
40
|
+
# Maximum trials allowed for a food source to improve before being discarded
|
|
41
|
+
self.limit = limit
|
|
42
|
+
|
|
43
|
+
# Array to track the number of failed attempts at improving each food source
|
|
44
|
+
self.trial_counters = np.zeros(self.food_sources_count)
|
|
45
|
+
|
|
46
|
+
def _apply_boundary_constraints(self, individual: np.ndarray) -> np.ndarray:
|
|
47
|
+
"""
|
|
48
|
+
Clips the coordinates of a candidate solution to the defined search space.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
individual (np.ndarray): The continuous vector to be constrained.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
np.ndarray: The bounded candidate solution.
|
|
55
|
+
"""
|
|
56
|
+
lower_bounds = self.bounds[:, 0]
|
|
57
|
+
upper_bounds = self.bounds[:, 1]
|
|
58
|
+
|
|
59
|
+
return np.clip(individual, lower_bounds, upper_bounds)
|
|
60
|
+
|
|
61
|
+
def run(self) -> Tuple[np.ndarray, float]:
|
|
62
|
+
"""
|
|
63
|
+
Executes the three-phase artificial foraging cycle.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple[np.ndarray, float]: The global best solution and its fitness.
|
|
67
|
+
"""
|
|
68
|
+
# Initial food sources are sampled uniformly from the search domain
|
|
69
|
+
# In this algorithm, population size reflects the number of active food sources
|
|
70
|
+
food_sources = self._initialize_population()[:self.food_sources_count]
|
|
71
|
+
|
|
72
|
+
# Initial evaluation of the food source fitness
|
|
73
|
+
fitness_values = np.apply_along_axis(self.objective_function, 1, food_sources)
|
|
74
|
+
|
|
75
|
+
for iteration in range(self.max_iterations):
|
|
76
|
+
|
|
77
|
+
# --- EMPLOYED BEES PHASE ---
|
|
78
|
+
# Each employed bee explores a neighbor of its assigned food source
|
|
79
|
+
for i in range(self.food_sources_count):
|
|
80
|
+
|
|
81
|
+
# Select a random neighbor index different from current source
|
|
82
|
+
neighbor_idx = np.random.choice([idx for idx in range(self.food_sources_count) if idx != i])
|
|
83
|
+
|
|
84
|
+
# Select a random dimension to perturb
|
|
85
|
+
dimension_idx = np.random.randint(self.dimension)
|
|
86
|
+
|
|
87
|
+
# Calculate the neighbor solution using the ABC search formula
|
|
88
|
+
# Phi is a random scaling factor in the range [-1, 1]
|
|
89
|
+
phi = np.random.uniform(-1, 1)
|
|
90
|
+
candidate_solution = np.copy(food_sources[i])
|
|
91
|
+
candidate_solution[dimension_idx] += phi * (food_sources[i][dimension_idx] - food_sources[neighbor_idx][dimension_idx])
|
|
92
|
+
|
|
93
|
+
# Enforce space boundaries and evaluate
|
|
94
|
+
candidate_solution = self._apply_boundary_constraints(candidate_solution)
|
|
95
|
+
candidate_fitness = self.objective_function(candidate_solution)
|
|
96
|
+
|
|
97
|
+
# Greedy selection: update source if the new candidate is better
|
|
98
|
+
if candidate_fitness < fitness_values[i]:
|
|
99
|
+
food_sources[i] = candidate_solution
|
|
100
|
+
fitness_values[i] = candidate_fitness
|
|
101
|
+
self.trial_counters[i] = 0
|
|
102
|
+
else:
|
|
103
|
+
self.trial_counters[i] += 1
|
|
104
|
+
|
|
105
|
+
# --- ONLOOKER BEES PHASE ---
|
|
106
|
+
# Bees in the hive choose food sources to exploit based on probability (Roulette)
|
|
107
|
+
for _ in range(self.food_sources_count):
|
|
108
|
+
|
|
109
|
+
# Use the roulette selection from operators.py to favor better sources
|
|
110
|
+
# We pass the full matrix and fitness values to pick a winning source index
|
|
111
|
+
# Note: We simulate the selection of a source for additional local search
|
|
112
|
+
selected_idx = ops.roulette_wheel_selection_index(food_sources, fitness_values)
|
|
113
|
+
|
|
114
|
+
# Repeat the local search logic for the chosen source
|
|
115
|
+
neighbor_idx = np.random.choice([idx for idx in range(self.food_sources_count) if idx != selected_idx])
|
|
116
|
+
dimension_idx = np.random.randint(self.dimension)
|
|
117
|
+
phi = np.random.uniform(-1, 1)
|
|
118
|
+
|
|
119
|
+
candidate_solution = np.copy(food_sources[selected_idx])
|
|
120
|
+
candidate_solution[dimension_idx] += phi * (food_sources[selected_idx][dimension_idx] - food_sources[neighbor_idx][dimension_idx])
|
|
121
|
+
|
|
122
|
+
candidate_solution = self._apply_boundary_constraints(candidate_solution)
|
|
123
|
+
candidate_fitness = self.objective_function(candidate_solution)
|
|
124
|
+
|
|
125
|
+
# Greedy selection for the onlooker bee's target
|
|
126
|
+
if candidate_fitness < fitness_values[selected_idx]:
|
|
127
|
+
food_sources[selected_idx] = candidate_solution
|
|
128
|
+
fitness_values[selected_idx] = candidate_fitness
|
|
129
|
+
self.trial_counters[selected_idx] = 0
|
|
130
|
+
else:
|
|
131
|
+
self.trial_counters[selected_idx] += 1
|
|
132
|
+
|
|
133
|
+
# --- SCOUT BEES PHASE ---
|
|
134
|
+
# Identify food sources that have exceeded the trial limit without improvement
|
|
135
|
+
for i in range(self.food_sources_count):
|
|
136
|
+
if self.trial_counters[i] > self.limit:
|
|
137
|
+
|
|
138
|
+
# Abandon the exhausted source and re-initialize it randomly
|
|
139
|
+
# This step prevents premature convergence to local optima
|
|
140
|
+
lower_bounds = self.bounds[:, 0]
|
|
141
|
+
upper_bounds = self.bounds[:, 1]
|
|
142
|
+
food_sources[i] = np.random.uniform(lower_bounds, upper_bounds, self.dimension)
|
|
143
|
+
fitness_values[i] = self.objective_function(food_sources[i])
|
|
144
|
+
self.trial_counters[i] = 0
|
|
145
|
+
|
|
146
|
+
# Update the global optimum record
|
|
147
|
+
best_idx = np.argmin(fitness_values)
|
|
148
|
+
if fitness_values[best_idx] < self.best_fitness:
|
|
149
|
+
self.best_fitness = fitness_values[best_idx]
|
|
150
|
+
self.best_individual = np.copy(food_sources[best_idx])
|
|
151
|
+
|
|
152
|
+
# Store best fitness for convergence history tracking
|
|
153
|
+
self.fitness_history.append(self.best_fitness)
|
|
154
|
+
|
|
155
|
+
return self.best_individual, self.best_fitness
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Callable, Tuple
|
|
3
|
+
from evobench.base import EvolutionaryAlgorithm
|
|
4
|
+
import evobench.tools.operators as ops
|
|
5
|
+
|
|
6
|
+
class EstimationOfDistributionAlgorithm(EvolutionaryAlgorithm):
|
|
7
|
+
"""
|
|
8
|
+
Continuous Estimation of Distribution Algorithm (EDA) with static typing.
|
|
9
|
+
|
|
10
|
+
This metaheuristic models the search space using a probabilistic approach.
|
|
11
|
+
Instead of applying classical genetic operators like crossover or mutation,
|
|
12
|
+
it selects a subset of the best-performing individuals to estimate the
|
|
13
|
+
parameters of a Gaussian distribution (mean and standard deviation).
|
|
14
|
+
The subsequent generation is then entirely sampled from this updated
|
|
15
|
+
probability model, effectively guiding the search towards promising regions.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
objective_function: Callable[[np.ndarray], float],
|
|
21
|
+
bounds: np.ndarray,
|
|
22
|
+
population_size: int = 50,
|
|
23
|
+
max_iterations: int = 100,
|
|
24
|
+
selection_ratio: float = 0.5
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Initializes the EDA-specific hyperparameters alongside the standard
|
|
28
|
+
evolutionary algorithm attributes.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
objective_function (Callable[[np.ndarray], float]): The mathematical
|
|
32
|
+
function to minimize.
|
|
33
|
+
bounds (np.ndarray): The spatial boundaries for the search domain.
|
|
34
|
+
population_size (int): Total number of candidate solutions per generation.
|
|
35
|
+
max_iterations (int): The termination criterion.
|
|
36
|
+
selection_ratio (float): The fraction of the population selected to
|
|
37
|
+
estimate the probability distribution.
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(objective_function, bounds, population_size, max_iterations)
|
|
40
|
+
|
|
41
|
+
# Determine the exact number of individuals that will form the elite pool
|
|
42
|
+
self.selection_size = int(self.population_size * selection_ratio)
|
|
43
|
+
|
|
44
|
+
def _apply_boundary_constraints(self, population: np.ndarray) -> np.ndarray:
|
|
45
|
+
"""
|
|
46
|
+
Enforces spatial boundaries on a newly sampled population.
|
|
47
|
+
|
|
48
|
+
Since the Gaussian distribution spans infinitely, sampled individuals
|
|
49
|
+
might fall outside the mathematical domain of the objective function.
|
|
50
|
+
This method clips the values strictly within the permitted limits.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
population (np.ndarray): The unbounded matrix of candidate solutions.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
np.ndarray: The spatially bounded population matrix.
|
|
57
|
+
"""
|
|
58
|
+
lower_bounds = self.bounds[:, 0]
|
|
59
|
+
upper_bounds = self.bounds[:, 1]
|
|
60
|
+
|
|
61
|
+
# Apply strict clipping across all dimensions simultaneously
|
|
62
|
+
bounded_population: np.ndarray = np.clip(population, lower_bounds, upper_bounds)
|
|
63
|
+
return bounded_population
|
|
64
|
+
|
|
65
|
+
def run(self) -> Tuple[np.ndarray, float]:
|
|
66
|
+
"""
|
|
67
|
+
Executes the main probabilistic evolutionary loop.
|
|
68
|
+
|
|
69
|
+
The process iteratively evaluates the population, selects the elite
|
|
70
|
+
subset using tournament selection, calculates the mean and standard
|
|
71
|
+
deviation vectors, and samples the new candidate solutions.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Tuple[np.ndarray, float]: The global best candidate vector and
|
|
75
|
+
its corresponding fitness.
|
|
76
|
+
"""
|
|
77
|
+
# Generate the initial uniform random population
|
|
78
|
+
population = self._initialize_population()
|
|
79
|
+
|
|
80
|
+
for _ in range(self.max_iterations):
|
|
81
|
+
# Evaluate the objective function for every individual in the matrix
|
|
82
|
+
fitness_values = np.apply_along_axis(self.objective_function, 1, population)
|
|
83
|
+
|
|
84
|
+
# Track the best individual found in the current generation
|
|
85
|
+
current_best_index = np.argmin(fitness_values)
|
|
86
|
+
current_best_fitness = float(fitness_values[current_best_index])
|
|
87
|
+
|
|
88
|
+
if current_best_fitness < self.best_fitness:
|
|
89
|
+
self.best_fitness = current_best_fitness
|
|
90
|
+
self.best_individual = np.copy(population[current_best_index])
|
|
91
|
+
|
|
92
|
+
# Record the convergence history for later statistical analysis
|
|
93
|
+
self.fitness_history.append(self.best_fitness)
|
|
94
|
+
|
|
95
|
+
# Construct the elite pool using the previously defined selection operator
|
|
96
|
+
# The tournament selection provides selection pressure while maintaining diversity
|
|
97
|
+
elite_pool = np.empty((self.selection_size, self.dimension))
|
|
98
|
+
for i in range(self.selection_size):
|
|
99
|
+
elite_pool[i] = ops.tournament_selection(population, fitness_values, tournament_size=3)
|
|
100
|
+
|
|
101
|
+
# Estimate the probability distribution parameters from the elite pool
|
|
102
|
+
# The mean acts as the center of mass for the promising region
|
|
103
|
+
mean_vector = np.mean(elite_pool, axis=0)
|
|
104
|
+
|
|
105
|
+
# The standard deviation acts as the search radius or exploration threshold
|
|
106
|
+
# A tiny constant is added to prevent mathematical errors if variance collapses to zero
|
|
107
|
+
standard_deviation_vector = np.std(elite_pool, axis=0) + 1e-8
|
|
108
|
+
|
|
109
|
+
# Sample the completely new population from the estimated Gaussian model
|
|
110
|
+
# Elitism: Preserve the global best individual to guarantee monotonic improvement
|
|
111
|
+
population = np.random.normal(
|
|
112
|
+
loc=mean_vector,
|
|
113
|
+
scale=standard_deviation_vector,
|
|
114
|
+
size=(self.population_size, self.dimension)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
population[0] = self.best_individual
|
|
118
|
+
|
|
119
|
+
# Ensure the newly sampled coordinates do not violate the problem domain
|
|
120
|
+
population = self._apply_boundary_constraints(population)
|
|
121
|
+
|
|
122
|
+
return self.best_individual, self.best_fitness
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Callable, Tuple
|
|
3
|
+
from evobench.base import EvolutionaryAlgorithm
|
|
4
|
+
|
|
5
|
+
class ParticleSwarmOptimization(EvolutionaryAlgorithm):
|
|
6
|
+
"""
|
|
7
|
+
Standard Particle Swarm Optimization (PSO) for continuous spaces.
|
|
8
|
+
|
|
9
|
+
This algorithm simulates a swarm of particles moving through a multi-dimensional
|
|
10
|
+
search space. Each particle maintains a velocity and a memory of its personal
|
|
11
|
+
best position. The movement is guided by an inertia weight, a cognitive
|
|
12
|
+
component (personal experience), and a social component (global best),
|
|
13
|
+
effectively balancing exploration and exploitation.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
objective_function: Callable[[np.ndarray], float],
|
|
19
|
+
bounds: np.ndarray,
|
|
20
|
+
population_size: int = 50,
|
|
21
|
+
max_iterations: int = 100,
|
|
22
|
+
inertia_weight: float = 0.7,
|
|
23
|
+
cognitive_constant: float = 1.5,
|
|
24
|
+
social_constant: float = 1.5
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Initializes the PSO hyperparameters and the base evolutionary attributes.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
objective_function (Callable): The mathematical function to minimize.
|
|
31
|
+
bounds (np.ndarray): Spatial boundaries [min, max] for each dimension.
|
|
32
|
+
population_size (int): Number of particles in the swarm.
|
|
33
|
+
max_iterations (int): Maximum number of iterations for the loop.
|
|
34
|
+
inertia_weight (float): Factor that controls the impact of previous velocity.
|
|
35
|
+
cognitive_constant (float): Acceleration coefficient for personal best.
|
|
36
|
+
social_constant (float): Acceleration coefficient for global best.
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(objective_function, bounds, population_size, max_iterations)
|
|
39
|
+
|
|
40
|
+
# Hyperparameters for the velocity update equation
|
|
41
|
+
self.w = inertia_weight
|
|
42
|
+
self.c1 = cognitive_constant
|
|
43
|
+
self.c2 = social_constant
|
|
44
|
+
|
|
45
|
+
def _apply_boundary_constraints(self, population: np.ndarray) -> np.ndarray:
|
|
46
|
+
"""
|
|
47
|
+
Keeps particles within the search space and handles velocity reflections.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
population (np.ndarray): The current positions of the swarm.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
np.ndarray: The bounded population matrix.
|
|
54
|
+
"""
|
|
55
|
+
lower_bounds = self.bounds[:, 0]
|
|
56
|
+
upper_bounds = self.bounds[:, 1]
|
|
57
|
+
|
|
58
|
+
# Clip positions that exceed the established search domain
|
|
59
|
+
return np.clip(population, lower_bounds, upper_bounds)
|
|
60
|
+
|
|
61
|
+
def run(self) -> Tuple[np.ndarray, float]:
|
|
62
|
+
"""
|
|
63
|
+
Executes the main PSO swarm intelligence loop.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple[np.ndarray, float]: The global best position and its fitness.
|
|
67
|
+
"""
|
|
68
|
+
# Initializing swarm positions using the base uniform sampling method
|
|
69
|
+
current_positions = self._initialize_population()
|
|
70
|
+
|
|
71
|
+
# Velocities are initialized to zero for all particles and dimensions
|
|
72
|
+
velocities = np.zeros((self.population_size, self.dimension))
|
|
73
|
+
|
|
74
|
+
# Personal best positions (p_best) are initialized to the starting positions
|
|
75
|
+
personal_best_positions = np.copy(current_positions)
|
|
76
|
+
|
|
77
|
+
# Personal best fitness values initialized to infinity for minimization
|
|
78
|
+
personal_best_fitness = np.full(self.population_size, float('inf'))
|
|
79
|
+
|
|
80
|
+
for iteration in range(self.max_iterations):
|
|
81
|
+
# Evaluate objective function for every particle in the swarm
|
|
82
|
+
fitness_values = np.apply_along_axis(self.objective_function, 1, current_positions)
|
|
83
|
+
|
|
84
|
+
# Update personal and global bests based on current evaluations
|
|
85
|
+
for i in range(self.population_size):
|
|
86
|
+
if fitness_values[i] < personal_best_fitness[i]:
|
|
87
|
+
personal_best_fitness[i] = fitness_values[i]
|
|
88
|
+
personal_best_positions[i] = np.copy(current_positions[i])
|
|
89
|
+
|
|
90
|
+
# Update the global optimum if a better solution is found
|
|
91
|
+
if fitness_values[i] < self.best_fitness:
|
|
92
|
+
self.best_fitness = fitness_values[i]
|
|
93
|
+
self.best_individual = np.copy(current_positions[i])
|
|
94
|
+
|
|
95
|
+
# Store the best fitness of the iteration for convergence tracking
|
|
96
|
+
self.fitness_history.append(self.best_fitness)
|
|
97
|
+
|
|
98
|
+
# Generate random coefficients for the stochastic components of the velocity
|
|
99
|
+
r1 = np.random.rand(self.population_size, self.dimension)
|
|
100
|
+
r2 = np.random.rand(self.population_size, self.dimension)
|
|
101
|
+
|
|
102
|
+
# Calculate the cognitive and social attraction vectors
|
|
103
|
+
cognitive_acceleration = self.c1 * r1 * (personal_best_positions - current_positions)
|
|
104
|
+
social_acceleration = self.c2 * r2 * (self.best_individual - current_positions)
|
|
105
|
+
|
|
106
|
+
# Update velocity applying inertia and acceleration components
|
|
107
|
+
velocities = (self.w * velocities) + cognitive_acceleration + social_acceleration
|
|
108
|
+
|
|
109
|
+
# Update particle positions based on the new velocity vectors
|
|
110
|
+
current_positions += velocities
|
|
111
|
+
|
|
112
|
+
# Enforce boundary constraints to keep the swarm within the domain
|
|
113
|
+
current_positions = self._apply_boundary_constraints(current_positions)
|
|
114
|
+
|
|
115
|
+
return self.best_individual, self.best_fitness
|
evobench/base.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Callable, List, Tuple
|
|
2
|
+
import numpy as np
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
class EvolutionaryAlgorithm(ABC):
|
|
6
|
+
"""
|
|
7
|
+
Abstract base class defining the standard architecture for population-based
|
|
8
|
+
evolutionary algorithms and metaheuristics.
|
|
9
|
+
|
|
10
|
+
This class enforces a uniform interface across different optimization strategies,
|
|
11
|
+
ensuring that all derived algorithms share common initialization protocols,
|
|
12
|
+
state tracking, and execution signatures. This standardization is critical
|
|
13
|
+
for executing rigorous statistical benchmarking over continuous search spaces.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, objective_function: Callable, bounds: List, population_size: int = 50, max_iterations: int = 100) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initializes the fundamental hyperparameters and state variables required
|
|
19
|
+
for the optimization process.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
objective_function (callable): The mathematical function to be minimized.
|
|
23
|
+
bounds (list of tuples or numpy.ndarray): The continuous search domain
|
|
24
|
+
defined as [lower_bound, upper_bound]
|
|
25
|
+
for each dimension.
|
|
26
|
+
population_size (int): The number of candidate solutions evaluated per generation.
|
|
27
|
+
max_iterations (int): The termination criterion defining the maximum
|
|
28
|
+
number of evolutionary cycles.
|
|
29
|
+
"""
|
|
30
|
+
self.objective_function = objective_function
|
|
31
|
+
|
|
32
|
+
# Array conversion ensures vectorized operations can be applied efficiently
|
|
33
|
+
# during mutation or boundary handling
|
|
34
|
+
self.bounds = np.array(bounds)
|
|
35
|
+
self.population_size = population_size
|
|
36
|
+
self.max_iterations = max_iterations
|
|
37
|
+
|
|
38
|
+
# The dimensionality of the problem is implicitly derived from the bounds matrix
|
|
39
|
+
self.dimension = len(self.bounds)
|
|
40
|
+
|
|
41
|
+
# State trackers required to store the global optimum throughout the execution
|
|
42
|
+
self.best_individual = None
|
|
43
|
+
self.best_fitness = float('inf')
|
|
44
|
+
|
|
45
|
+
# Convergence tracker used to store the best fitness value of each generation,
|
|
46
|
+
# which is later utilized to generate convergence plots and statistical analysis
|
|
47
|
+
self.fitness_history = []
|
|
48
|
+
|
|
49
|
+
def _initialize_population(self) -> np.ndarray:
|
|
50
|
+
"""
|
|
51
|
+
Generates the initial set of candidate solutions.
|
|
52
|
+
|
|
53
|
+
The initial population is generated by sampling from a uniform probability
|
|
54
|
+
distribution bounded by the defined search space. This ensures an unbiased
|
|
55
|
+
exploration of the domain during the first generation.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
numpy.ndarray: A matrix of shape (population_size, dimension) containing
|
|
59
|
+
the initial candidate vectors.
|
|
60
|
+
"""
|
|
61
|
+
# Vectorized extraction of the lower and upper limits for the search space
|
|
62
|
+
lower_bounds = self.bounds[:, 0]
|
|
63
|
+
upper_bounds = self.bounds[:, 1]
|
|
64
|
+
|
|
65
|
+
# Matrix generation using uniform sampling across all dimensions simultaneously
|
|
66
|
+
initial_population = np.random.uniform(
|
|
67
|
+
low=lower_bounds,
|
|
68
|
+
high=upper_bounds,
|
|
69
|
+
size=(self.population_size, self.dimension)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return initial_population
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def run(self) -> Tuple[np.ndarray, float]:
|
|
76
|
+
"""
|
|
77
|
+
Executes the core evolutionary loop.
|
|
78
|
+
|
|
79
|
+
This method acts as the main algorithmic contract. Every subclass must implement
|
|
80
|
+
its own stochastic search mechanisms, position updates, or probabilistic models
|
|
81
|
+
within this function.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
tuple: A pair containing the best candidate vector (numpy.ndarray) and
|
|
85
|
+
its corresponding fitness value (float).
|
|
86
|
+
"""
|
|
87
|
+
pass
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evolutionary Benchmarking Registry Module
|
|
3
|
+
|
|
4
|
+
This module acts as a central hub for all optimization test functions.
|
|
5
|
+
It utilizes a registry pattern to map string identifiers directly to
|
|
6
|
+
their corresponding mathematical implementations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# We use relative imports (with the dot) to tell Python:
|
|
10
|
+
# "Look inside the current folder"
|
|
11
|
+
from .unimodal import (
|
|
12
|
+
rosenbrock_function as rosenbrock,
|
|
13
|
+
sphere_function as sphere,
|
|
14
|
+
schwefel_1_2_function as schwefel,
|
|
15
|
+
trid_function as trid
|
|
16
|
+
)
|
|
17
|
+
from .multimodal import ackley_function as ackley
|
|
18
|
+
|
|
19
|
+
# Central dictionary mapping string names to function references
|
|
20
|
+
# Note: Using lowercase keys is a good practice for robustness
|
|
21
|
+
BENCHMARK_REGISTRY = {
|
|
22
|
+
"sphere": sphere,
|
|
23
|
+
"rosenbrock": rosenbrock,
|
|
24
|
+
"ackley": ackley,
|
|
25
|
+
"schwefel 1.2": schwefel,
|
|
26
|
+
"trid": trid
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def get_benchmark(name: str):
|
|
30
|
+
"""
|
|
31
|
+
Retrieves the callable mathematical function based on its registry name.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: The string identifier of the benchmark function.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
callable: The requested benchmarking function.
|
|
38
|
+
"""
|
|
39
|
+
# Normalize the input name to lowercase to match the registry keys
|
|
40
|
+
search_name = name.lower()
|
|
41
|
+
|
|
42
|
+
if search_name not in BENCHMARK_REGISTRY:
|
|
43
|
+
raise ValueError(f"Benchmark '{name}' is not implemented in the registry.")
|
|
44
|
+
|
|
45
|
+
return BENCHMARK_REGISTRY[search_name]
|
|
46
|
+
|
|
47
|
+
# List of publicly available objects when using 'from evobench.benchmarks import *'
|
|
48
|
+
# Fixed the typos and ensured names match the aliases defined above
|
|
49
|
+
__all__ = [
|
|
50
|
+
"rosenbrock",
|
|
51
|
+
"sphere",
|
|
52
|
+
"schwefel",
|
|
53
|
+
"trid",
|
|
54
|
+
"ackley",
|
|
55
|
+
"get_benchmark"
|
|
56
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def ackley_function(x: np.ndarray) -> float:
|
|
4
|
+
"""
|
|
5
|
+
Evaluates the Ackley function for a given continuous vector.
|
|
6
|
+
|
|
7
|
+
The Ackley function is characterized by a nearly flat outer region and
|
|
8
|
+
a large hole at the center. It poses a risk for optimization algorithms
|
|
9
|
+
to be trapped in one of its many local minima.
|
|
10
|
+
|
|
11
|
+
Search space bounds: [-10, 10]
|
|
12
|
+
Global optimum: F(0) = 0
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
x: The candidate solution vector representing coordinates.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The evaluated fitness value (objective to minimize).
|
|
19
|
+
"""
|
|
20
|
+
# Extract the dimensionality of the search space directly from the vector length
|
|
21
|
+
dimension = len(x)
|
|
22
|
+
|
|
23
|
+
# Calculate the sum of squared elements for the first exponential term
|
|
24
|
+
sum_of_squares = float(np.sum(x**2))
|
|
25
|
+
|
|
26
|
+
# Calculate the sum of cosine evaluations for the second exponential term
|
|
27
|
+
sum_of_cosines = float(np.sum(np.cos(2.0 * np.pi * x)))
|
|
28
|
+
|
|
29
|
+
# Compute the first component of the Ackley mathematical equation
|
|
30
|
+
term_one = -20.0 * np.exp(-0.2 * np.sqrt(sum_of_squares / dimension))
|
|
31
|
+
|
|
32
|
+
# Compute the second component involving the trigonometric summation
|
|
33
|
+
term_two = -np.exp(sum_of_cosines / dimension)
|
|
34
|
+
|
|
35
|
+
# Aggregate all terms alongside the mathematical constants
|
|
36
|
+
final_fitness = term_one + term_two + 20.0 + np.exp(1.0)
|
|
37
|
+
|
|
38
|
+
return final_fitness
|