|
| 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) |
0 commit comments