Skip to content

Commit 5a44201

Browse files
authored
FEAT: Add BWO implementation, example, and tests (#234)
* Add BWO implementation, example, and tests
1 parent 16e57ef commit 5a44201

File tree

4 files changed

+203
-1
lines changed

4 files changed

+203
-1
lines changed

examples/run_bwo_example.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env python
2+
# Created by "Thieu" at 18:37, 27/10/2023 ----------%
3+
# Email: nguyenthieu2102@gmail.com %
4+
# Github: https://github.com/thieu1995 %
5+
# --------------------------------------------------%
6+
7+
import numpy as np
8+
from mealpy import FloatVar, BWO
9+
10+
11+
def objective_function(solution):
12+
return np.sum(solution ** 2)
13+
14+
15+
problem = {
16+
"bounds": FloatVar(lb=(-10.0,) * 30, ub=(10.0,) * 30, name="x"),
17+
"minmax": "min",
18+
"obj_func": objective_function,
19+
"name": "Sphere",
20+
}
21+
22+
model = BWO.OriginalBWO(epoch=100, pop_size=50, pp=0.6, cr=0.44, pm=0.4)
23+
g_best = model.solve(problem, seed=10)
24+
print(f"Best fitness: {g_best.target.fitness}")

mealpy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import sys
3333
import inspect
3434
from .bio_based import (BBO, BBOA, BMO, EOA, IWO, SBO, SMA, SOA, SOS, TPO, TSA, VCS, WHO, BCO, EAO, SFOA)
35-
from .evolutionary_based import (CRO, DE, EP, ES, FPA, GA, MA, SHADE)
35+
from .evolutionary_based import (BWO, CRO, DE, EP, ES, FPA, GA, MA, SHADE)
3636
from .human_based import (BRO, BSO, CA, CHIO, FBIO, GSKA, HBO, HCO, ICA, LCO, QSA, SARO, SPBO, SSDO, TLO, TOA, WarSO,
3737
AFT, CDDO, DOA)
3838
from .math_based import (AOA, CEM, CGO, CircleSA, GBO, HC, INFO, PSS, RUN, SCA, SHIO, TS)

mealpy/evolutionary_based/BWO.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python
2+
# Created by "Thieu" at 11:40, 20/12/2025 ----------%
3+
# Email: nguyenthieu2102@gmail.com %
4+
# Github: https://github.com/thieu1995 %
5+
# --------------------------------------------------%
6+
7+
import numpy as np
8+
from mealpy.optimizer import Optimizer
9+
10+
11+
class OriginalBWO(Optimizer):
12+
"""
13+
The original version of: Black Widow Optimization (BWO)
14+
15+
Links:
16+
1. https://doi.org/10.1016/j.engappai.2019.103249
17+
18+
Hyper-parameters should fine-tune in approximate range to get faster convergence toward the global optimum:
19+
+ pp (float): [0, 1], procreating rate, default = 0.6
20+
+ cr (float): [0, 1], cannibalism rate, default = 0.44
21+
+ pm (float): [0, 1], mutation rate, default = 0.4
22+
23+
Examples
24+
~~~~~~~~
25+
>>> import numpy as np
26+
>>> from mealpy import FloatVar, BWO
27+
>>>
28+
>>> def objective_function(solution):
29+
>>> return np.sum(solution**2)
30+
>>>
31+
>>> problem_dict = {
32+
>>> "bounds": FloatVar(lb=(-10.,) * 30, ub=(10.,) * 30, name="delta"),
33+
>>> "minmax": "min",
34+
>>> "obj_func": objective_function,
35+
>>> }
36+
>>>
37+
>>> model = BWO.OriginalBWO(epoch=1000, pop_size=50, pp=0.6, cr=0.44, pm=0.4)
38+
>>> g_best = model.solve(problem_dict)
39+
>>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}")
40+
>>> print(f"Solution: {model.g_best.solution}, Fitness: {model.g_best.target.fitness}")
41+
42+
References
43+
~~~~~~~~~~
44+
[1] Hayyolalam, V. and Pourhaji Kazem, A.A., 2020. Black widow optimization algorithm: A novel meta-heuristic
45+
approach for solving engineering optimization problems. Engineering Applications of Artificial Intelligence, 87, 103249.
46+
"""
47+
48+
def __init__(self, epoch: int = 10000, pop_size: int = 100, pp: float = 0.6, cr: float = 0.44,
49+
pm: float = 0.4, **kwargs: object) -> None:
50+
"""
51+
Args:
52+
epoch (int): maximum number of iterations, default = 10000
53+
pop_size (int): number of population size, default = 100
54+
pp (float): procreating rate, default = 0.6
55+
cr (float): cannibalism rate, default = 0.44
56+
pm (float): mutation rate, default = 0.4
57+
"""
58+
super().__init__(**kwargs)
59+
self.epoch = self.validator.check_int("epoch", epoch, [1, 100000])
60+
self.pop_size = self.validator.check_int("pop_size", pop_size, [5, 10000])
61+
self.pp = self.validator.check_float("pp", pp, (0.0, 1.0))
62+
self.cr = self.validator.check_float("cr", cr, (0.0, 1.0))
63+
self.pm = self.validator.check_float("pm", pm, (0.0, 1.0))
64+
self.set_parameters(["epoch", "pop_size", "pp", "cr", "pm"])
65+
self.sort_flag = False
66+
67+
def initialize_variables(self):
68+
self.n_parents = max(2, int(self.pp * self.pop_size))
69+
if self.n_parents > self.pop_size:
70+
self.n_parents = self.pop_size
71+
self.n_mutate = max(0, int(self.pm * self.pop_size))
72+
73+
def _procreate(self, parent1: np.ndarray, parent2: np.ndarray) -> tuple:
74+
"""
75+
Create two offspring from a pair of parents using blend crossover on Nvar/2 indices.
76+
"""
77+
n_dims = self.problem.n_dims
78+
n_cross = max(1, n_dims // 2)
79+
idxs = self.generator.choice(n_dims, n_cross, replace=False)
80+
alpha = self.generator.random(len(idxs))
81+
child1 = parent1.copy()
82+
child2 = parent2.copy()
83+
child1[idxs] = alpha * parent1[idxs] + (1 - alpha) * parent2[idxs]
84+
child2[idxs] = alpha * parent2[idxs] + (1 - alpha) * parent1[idxs]
85+
return self.correct_solution(child1), self.correct_solution(child2)
86+
87+
def _mutate(self, position: np.ndarray) -> np.ndarray:
88+
"""
89+
Mutate one randomly selected position in the solution vector.
90+
"""
91+
if self.problem.n_dims < 1:
92+
return position
93+
pos_new = position.copy()
94+
idx = self.generator.integers(0, self.problem.n_dims)
95+
pos_new[idx] = self.generator.uniform(self.problem.lb[idx], self.problem.ub[idx])
96+
return self.correct_solution(pos_new)
97+
98+
def evolve(self, epoch: int) -> None:
99+
"""
100+
The main operations (equations) of algorithm. Inherit from Optimizer class
101+
102+
Args:
103+
epoch (int): The current iteration
104+
"""
105+
pop_sorted = self.get_sorted_population(self.pop, self.problem.minmax)
106+
pop1 = [agent.copy() for agent in pop_sorted[:self.n_parents]]
107+
108+
pop2 = []
109+
for _ in range(self.n_parents):
110+
parent_idx = self.generator.choice(len(pop1), 2, replace=False)
111+
parent1, parent2 = pop1[parent_idx[0]], pop1[parent_idx[1]]
112+
female = self.get_better_agent(parent1, parent2, self.problem.minmax).copy()
113+
child1_pos, child2_pos = self._procreate(parent1.solution, parent2.solution)
114+
child1 = self.generate_empty_agent(child1_pos)
115+
child2 = self.generate_empty_agent(child2_pos)
116+
children = [child1, child2]
117+
if self.mode in self.AVAILABLE_MODES:
118+
self.update_target_for_population(children)
119+
else:
120+
for child in children:
121+
child.target = self.get_target(child.solution)
122+
n_keep = self.generator.binomial(len(children), 1 - self.cr)
123+
if n_keep < 1:
124+
n_keep = 1
125+
children = self.get_sorted_population(children, self.problem.minmax)
126+
pop2.append(female)
127+
pop2.extend(children[:n_keep])
128+
129+
pop3 = []
130+
if self.n_mutate > 0:
131+
for _ in range(self.n_mutate):
132+
parent = pop1[self.generator.integers(0, len(pop1))]
133+
pos_new = self._mutate(parent.solution)
134+
pop3.append(self.generate_empty_agent(pos_new))
135+
if self.mode in self.AVAILABLE_MODES:
136+
self.update_target_for_population(pop3)
137+
else:
138+
for agent in pop3:
139+
agent.target = self.get_target(agent.solution)
140+
141+
pop_new = pop2 + pop3
142+
if len(pop_new) < self.pop_size:
143+
needed = self.pop_size - len(pop_new)
144+
pop_new.extend([agent.copy() for agent in pop_sorted[:needed]])
145+
self.pop = self.get_sorted_and_trimmed_population(pop_new, self.pop_size, self.problem.minmax)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python
2+
# Created by "Thieu" at 11:10, 17/03/2022 ----------%
3+
# Email: nguyenthieu2102@gmail.com %
4+
# Github: https://github.com/thieu1995 %
5+
# --------------------------------------------------%
6+
7+
import numpy as np
8+
import pytest
9+
10+
from mealpy import FloatVar, BWO, Optimizer
11+
12+
13+
@pytest.fixture(scope="module")
14+
def problem():
15+
def objective_function(solution):
16+
return np.sum(solution ** 2)
17+
18+
return {
19+
"obj_func": objective_function,
20+
"bounds": FloatVar(lb=[-10, -15, -4, -2, -8], ub=[10, 15, 12, 8, 20]),
21+
"minmax": "min",
22+
}
23+
24+
25+
def test_BWO_results(problem):
26+
models = [
27+
BWO.OriginalBWO(epoch=10, pop_size=50, pp=0.6, cr=0.44, pm=0.4),
28+
]
29+
for model in models:
30+
g_best = model.solve(problem)
31+
assert isinstance(model, Optimizer)
32+
assert isinstance(g_best.solution, np.ndarray)
33+
assert len(g_best.solution) == len(model.problem.lb)

0 commit comments

Comments
 (0)