diff --git a/README.md b/README.md index 47f94383..f7ac7c68 100644 --- a/README.md +++ b/README.md @@ -1858,6 +1858,15 @@ along with their syntax and common problem applications. This will guide you in 5 easy + + Bio + Secretary Bird Optimization Algorithm + SBOA + OriginalSBOA + 2024 + 2 + easy + Bio Earthworm Optimisation Algorithm @@ -3243,6 +3252,8 @@ All visualization examples: [Link](https://mealpy.readthedocs.io/en/latest/pages * **SBO - Satin Bowerbird Optimizer** * **OriginalSBO**: Moosavi, S. H. S., & Bardsiri, V. K. (2017). Satin bowerbird optimizer: a new optimization algorithm to optimize ANFIS for software development effort estimation. Engineering Applications of Artificial Intelligence, 60, 1-15. * **BaseSBO**: The developed version +* **SBOA - Secretary Bird Optimization Algorithm** + * **OriginalSBOA**: Fu, Y., Liu, D., Chen, J. et al. (2024). Secretary bird optimization algorithm: a new metaheuristic for solving global optimization problems. Artificial Intelligence Review, 57, 123. https://doi.org/10.1007/s10462-024-10729-y * **SHO - Spotted Hyena Optimizer** * **OriginalSHO**: Dhiman, G., & Kumar, V. (2017). Spotted hyena optimizer: a novel bio-inspired based metaheuristic technique for engineering applications. Advances in Engineering Software, 114, 48-70. @@ -3405,4 +3416,4 @@ All visualization examples: [Link](https://mealpy.readthedocs.io/en/latest/pages --- -Developed by: [Thieu](mailto:nguyenthieu2102@gmail.com?Subject=MEALPY_QUESTIONS) @ 2022 \ No newline at end of file +Developed by: [Thieu](mailto:nguyenthieu2102@gmail.com?Subject=MEALPY_QUESTIONS) @ 2022 diff --git a/mealpy/bio_based/SBOA.py b/mealpy/bio_based/SBOA.py new file mode 100644 index 00000000..94ff1e77 --- /dev/null +++ b/mealpy/bio_based/SBOA.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# Created by "Thieu" at 13:00, 05/01/2026 ----------% +# Email: nguyenthieu2102@gmail.com % +# Github: https://github.com/thieu1995 % +# --------------------------------------------------% + +import numpy as np +from mealpy.optimizer import Optimizer + + +class OriginalSBOA(Optimizer): + """ + The original version of: Secretary Bird Optimization Algorithm (SBOA) + + Links: + 1. https://doi.org/10.1007/s10462-024-10729-y + 2. https://www.mathworks.com/matlabcentral/fileexchange/164456-secretary-bird-optimization-algorithm-sboa + + Examples + ~~~~~~~~ + >>> import numpy as np + >>> from mealpy import FloatVar, SBOA + >>> + >>> def objective_function(solution): + >>> return np.sum(solution**2) + >>> + >>> problem_dict = { + >>> "bounds": FloatVar(lb=(-10.,) * 30, ub=(10.,) * 30, name="delta"), + >>> "obj_func": objective_function, + >>> "minmax": "min", + >>> } + >>> + >>> model = SBOA.OriginalSBOA(epoch=1000, pop_size=50) + >>> 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}") + + References + ~~~~~~~~~~ + [1] Fu, Y., Liu, D., Chen, J. et al. Secretary bird optimization algorithm: a new metaheuristic + for solving global optimization problems. Artif Intell Rev 57, 123 (2024). + https://doi.org/10.1007/s10462-024-10729-y + """ + + def __init__(self, epoch: int = 10000, pop_size: int = 100, **kwargs: object) -> None: + """ + Args: + epoch (int): maximum number of iterations, default = 10000 + pop_size (int): number of population size, default = 100 + """ + 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.set_parameters(["epoch", "pop_size"]) + self.sort_flag = False + + def get_levy_flight_step(self, beta=1.5, multiplier=1.0, size=None): + """ + Get Levy flight step + + Args: + beta (float): Levy exponent, default = 1.5 + multiplier (float): Multiplier factor, default = 1.0 + size (int): Size of levy step, default = None + + Returns: + numpy.ndarray: Levy flight step + """ + if size is None: + size = self.problem.n_dims + sigma = (np.math.gamma(1 + beta) * np.sin(np.pi * beta / 2) / + (np.math.gamma((1 + beta) / 2) * beta * 2 ** ((beta - 1) / 2))) ** (1 / beta) + u = self.generator.normal(0, sigma, size) + v = self.generator.normal(0, 1, size) + step = multiplier * u / (np.abs(v) ** (1 / beta)) + return step + + def evolve(self, epoch): + """ + The main operations (equations) of algorithm. Inherit from Optimizer class + + Args: + epoch (int): The current iteration + """ + t = epoch + T = self.epoch + + # Calculate Convergence Factor (Eq. 9) + CF = (1 - t/T) ** (2 * t/T) + + pop_new = [] + + # Hunting Strategies (Exploration Phase) + for idx in range(0, self.pop_size): + if t < T / 3: + # Stage 1: Secretary bird search prey (Eq. 4-5) + idxs = self.generator.choice(list(range(0, self.pop_size)), 2, replace=True) + x_rand1 = self.pop[idxs[0]].solution + x_rand2 = self.pop[idxs[1]].solution + R1 = self.generator.random(self.problem.n_dims) + pos_new = self.pop[idx].solution + (x_rand1 - x_rand2) * R1 + + elif t < 2 * T / 3: + # Stage 2: Secretary bird approaching prey (Eq. 7-8) + RB = self.generator.normal(0, 1, self.problem.n_dims) + term = np.exp((t/T)**4) + pos_new = self.g_best.solution + term * (RB - 0.5) * (self.g_best.solution - self.pop[idx].solution) + + else: + # Stage 3: Secretary bird attacks prey (Eq. 9-10) + levy_step = self.get_levy_flight_step(beta=1.5, multiplier=1.0) + RL = 0.5 * levy_step + pos_new = self.g_best.solution + CF * self.pop[idx].solution * RL + + pos_new = self.correct_solution(pos_new) + agent = self.generate_empty_agent(pos_new) + pop_new.append(agent) + if self.mode not in self.AVAILABLE_MODES: + agent.target = self.get_target(pos_new) + self.pop[idx] = self.get_better_agent(agent, self.pop[idx], self.problem.minmax) + + # Update population after exploration + if self.mode in self.AVAILABLE_MODES: + pop_new = self.update_target_for_population(pop_new) + self.pop = self.greedy_selection_population(self.pop, pop_new, self.problem.minmax) + + # Escaping Strategies (Exploitation Phase) + r = self.generator.random() + k = self.generator.integers(0, self.pop_size) + x_random_global = self.pop[k].solution + + pop_new_escape = [] + for idx in range(0, self.pop_size): + if r < 0.5: + # C1: Secretary birds use their environment to hide (Eq. 14) + RB = self.generator.random(self.problem.n_dims) + factor = (1 - t/T) ** 2 + pos_new = self.g_best.solution + factor * (2 * RB - 1) * self.pop[idx].solution + else: + # C2: Secretary birds fly or run away (Eq. 14, 16) + K = int(round(1 + self.generator.random())) + R2 = self.generator.random(self.problem.n_dims) + pos_new = self.pop[idx].solution + R2 * (x_random_global - K * self.pop[idx].solution) + + pos_new = self.correct_solution(pos_new) + agent = self.generate_empty_agent(pos_new) + pop_new_escape.append(agent) + if self.mode not in self.AVAILABLE_MODES: + agent.target = self.get_target(pos_new) + self.pop[idx] = self.get_better_agent(agent, self.pop[idx], self.problem.minmax) + + # Update population after exploitation + if self.mode in self.AVAILABLE_MODES: + pop_new_escape = self.update_target_for_population(pop_new_escape) + self.pop = self.greedy_selection_population(self.pop, pop_new_escape, self.problem.minmax) diff --git a/tests/bio_based/test_SBOA.py b/tests/bio_based/test_SBOA.py new file mode 100644 index 00000000..adfeaf3d --- /dev/null +++ b/tests/bio_based/test_SBOA.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# Created by "Thieu" at 13:00, 05/01/2026 ----------% +# Email: nguyenthieu2102@gmail.com % +# Github: https://github.com/thieu1995 % +# --------------------------------------------------% + +from mealpy import FloatVar, SBOA, Optimizer +import numpy as np +import pytest + + +@pytest.fixture(scope="module") # scope: Call only 1 time at the beginning +def problem(): + def objective_function(solution): + return np.sum(solution ** 2) + + problem = { + "obj_func": objective_function, + "bounds": FloatVar(lb=[-10, -10, -10, -10, -10], ub=[10, 10, 10, 10, 10]), + "minmax": "min", + } + return problem + + +def test_OriginalSBOA_results(problem): + epoch = 10 + pop_size = 50 + model = SBOA.OriginalSBOA(epoch, pop_size) + g_best = model.solve(problem) + assert isinstance(model, Optimizer) + assert isinstance(g_best.solution, np.ndarray) + assert len(g_best.solution) == len(model.problem.lb) + + +@pytest.mark.parametrize("problem, epoch, system_code", + [ + (problem, None, 0), + (problem, "hello", 0), + (problem, -10, 0), + (problem, [10], 0), + (problem, (0, 9), 0), + (problem, 0, 0), + (problem, float("inf"), 0), + ]) +def test_epoch_SBOA(problem, epoch, system_code): + pop_size = 50 + with pytest.raises(ValueError) as e: + SBOA.OriginalSBOA(epoch, pop_size) + assert e.type == ValueError + + +@pytest.mark.parametrize("problem, pop_size, system_code", + [ + (problem, None, 0), + (problem, "hello", 0), + (problem, -10, 0), + (problem, [10], 0), + (problem, (0, 9), 0), + (problem, 0, 0), + (problem, float("inf"), 0), + ]) +def test_pop_size_SBOA(problem, pop_size, system_code): + epoch = 10 + with pytest.raises(ValueError) as e: + SBOA.OriginalSBOA(epoch, pop_size) + assert e.type == ValueError