#!/usr/bin/env python
# Created by "Thieu" at 11:40, 20/12/2025 ----------%
# Email: nguyenthieu2102@gmail.com %
# Github: https://github.com/thieu1995 %
# --------------------------------------------------%
import numpy as np
from mealpy.optimizer import Optimizer
from mealpy.utils.opt_info import OptInfo, ScientificConcern
[docs]class OriginalBWOA(Optimizer):
"""
The original version of: Black Widow Optimization Algorithm (BWOA)
Parameters
----------
epoch : int
Maximum number of iterations, in range [1, 100000]. Default is 10000.
pop_size : int
Number of population size, in range [5, 10000]. Default is 100.
pp : float
Procreating rate, in range (0.0, 1.0). Default is 0.6.
cr : float
Cannibalism rate, in range (0.0, 1.0). Default is 0.44.
pm : float
Mutation rate, in range (0.0, 1.0). Default is 0.4.
References
~~~~~~~~~~
1. Hayyolalam, V. and Pourhaji Kazem, A.A., 2020.
Black widow optimization algorithm: A novel meta-heuristic approach for solving engineering optimization problems.
Engineering Applications of Artificial Intelligence, 87, 103249. https://doi.org/10.1016/j.engappai.2019.103249
Examples
~~~~~~~~
>>> import numpy as np
>>> from mealpy import FloatVar, BWOA
>>>
>>> def objective_function(solution):
>>> return np.sum(solution**2)
>>>
>>> problem_dict = {
>>> "bounds": FloatVar(lb=(-10.,) * 30, ub=(10.,) * 30, name="delta"),
>>> "minmax": "min",
>>> "obj_func": objective_function,
>>> }
>>>
>>> model = BWOA.OriginalBWOA(epoch=1000, pop_size=50, pp=0.6, cr=0.44, pm=0.4)
>>> g_best = model.solve(problem_dict)
>>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}")
>>> print(f"Solution: {model.g_best.solution}, Fitness: {model.g_best.target.fitness}")
"""
OPT_INFO = OptInfo(name="Black Widow Optimization Algorithm", year=2020, difficulty="medium", kind="original",
scientific_status="questionable",
concerns=(
ScientificConcern.LACK_OF_NOVELTY, ScientificConcern.POOR_REPRODUCIBILITY
))
def __init__(self, epoch: int = 10000, pop_size: int = 100, pp: float = 0.6, cr: float = 0.44,
pm: float = 0.4, **kwargs: object) -> None:
"""
Args:
epoch (int): maximum number of iterations, default = 10000
pop_size (int): number of population size, default = 100
pp (float): procreating rate, default = 0.6
cr (float): cannibalism rate, default = 0.44
pm (float): mutation rate, default = 0.4
"""
super().__init__(**kwargs)
self.epoch = self.validator.check_int("epoch", epoch, [1, 100000])
self.pop_size = self.validator.check_int("pop_size", pop_size, [5, 10000])
self.pp = self.validator.check_float("pp", pp, (0.0, 1.0))
self.cr = self.validator.check_float("cr", cr, (0.0, 1.0))
self.pm = self.validator.check_float("pm", pm, (0.0, 1.0))
self.set_parameters(["epoch", "pop_size", "pp", "cr", "pm"])
self.sort_flag = False
[docs] def initialize_variables(self):
self.n_parents = max(2, int(self.pp * self.pop_size))
if self.n_parents > self.pop_size:
self.n_parents = self.pop_size
self.n_mutate = max(0, int(self.pm * self.pop_size))
def _procreate(self, parent1: np.ndarray, parent2: np.ndarray) -> tuple:
"""
Create two offspring from a pair of parents using blend crossover on Nvar/2 indices.
"""
n_dims = self.problem.n_dims
n_cross = max(1, n_dims // 2)
idxs = self.generator.choice(n_dims, n_cross, replace=False)
alpha = self.generator.random(len(idxs))
child1 = parent1.copy()
child2 = parent2.copy()
child1[idxs] = alpha * parent1[idxs] + (1 - alpha) * parent2[idxs]
child2[idxs] = alpha * parent2[idxs] + (1 - alpha) * parent1[idxs]
return self.correct_solution(child1), self.correct_solution(child2)
def _mutate(self, position: np.ndarray) -> np.ndarray:
"""
Mutate one randomly selected position in the solution vector.
"""
if self.problem.n_dims < 1:
return position
pos_new = position.copy()
idx = self.generator.integers(0, self.problem.n_dims)
pos_new[idx] = self.generator.uniform(self.problem.lb[idx], self.problem.ub[idx])
return self.correct_solution(pos_new)
[docs] def evolve(self, epoch: int) -> None:
"""
The main operations (equations) of algorithm. Inherit from Optimizer class
Args:
epoch (int): The current iteration
"""
pop_sorted, _ = self.get_sorted_population(self.pop, self.problem.minmax)
pop1 = [agent.copy() for agent in pop_sorted[:self.n_parents]]
pop2 = []
for _ in range(self.n_parents):
parent_idx = self.generator.choice(len(pop1), 2, replace=False)
parent1, parent2 = pop1[parent_idx[0]], pop1[parent_idx[1]]
female = self.get_better_agent(parent1, parent2, self.problem.minmax).copy()
child1_pos, child2_pos = self._procreate(parent1.solution, parent2.solution)
child1 = self.generate_empty_agent(child1_pos)
child2 = self.generate_empty_agent(child2_pos)
children = [child1, child2]
if self.mode in self.AVAILABLE_MODES:
self.update_target_for_population(children)
else:
for child in children:
child.target = self.get_target(child.solution)
n_keep = self.generator.binomial(len(children), 1 - self.cr)
if n_keep < 1:
n_keep = 1
children, _ = self.get_sorted_population(children, self.problem.minmax)
pop2.append(female)
pop2.extend(children[:n_keep])
pop3 = []
if self.n_mutate > 0:
for _ in range(self.n_mutate):
parent = pop1[self.generator.integers(0, len(pop1))]
pos_new = self._mutate(parent.solution)
pop3.append(self.generate_empty_agent(pos_new))
if self.mode in self.AVAILABLE_MODES:
self.update_target_for_population(pop3)
else:
for agent in pop3:
agent.target = self.get_target(agent.solution)
pop_new = pop2 + pop3
if len(pop_new) < self.pop_size:
needed = self.pop_size - len(pop_new)
pop_new.extend([agent.copy() for agent in pop_sorted[:needed]])
self.pop = self.get_sorted_and_trimmed_population(pop_new, self.pop_size, self.problem.minmax)