From 1234e0e046e98947958efa97be5573d665c14202 Mon Sep 17 00:00:00 2001 From: pnarkz Date: Fri, 2 Jan 2026 20:20:26 +0300 Subject: [PATCH] feat: Add White Shark Optimizer (WSO) with critical bug fix Implement strict MATLAB port of White Shark Optimizer (Braik et al., 2022) with correction to Global Best update logic. Original source used static index causing premature convergence; fixed by updating from Local Best memory independently following sequential if-logic. - 8 configurable parameters (f_min, f_max, tau, a0, a1, a2) - Boundary handling: ub*a + lb*b (safe Boolean logic) - Frequency: constant calculation (~0.899) matching MATLAB - Sequential schooling with chain dependency preserved - Comprehensive docstring with Args and Examples - Bug fix: Global Best now copies from agent memory (local_solution) Ref: https://doi.org/10.1016/j.knosys.2022.109210 --- README.md | 9 ++ mealpy/__init__.py | 2 +- mealpy/swarm_based/WSO.py | 164 ++++++++++++++++++++++++++++++++++ tests/swarm_based/test_WSO.py | 34 +++++++ 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 mealpy/swarm_based/WSO.py create mode 100644 tests/swarm_based/test_WSO.py diff --git a/README.md b/README.md index 47f94383..7c50d3d5 100644 --- a/README.md +++ b/README.md @@ -823,6 +823,15 @@ along with their syntax and common problem applications. This will guide you in 3 medium + + Swarm + White Shark Optimizer + WSO + OriginalWSO + 2022 + 8 + medium + Swarm Dragonfly Optimization diff --git a/mealpy/__init__.py b/mealpy/__init__.py index b5ba005f..85ed0c21 100644 --- a/mealpy/__init__.py +++ b/mealpy/__init__.py @@ -41,7 +41,7 @@ DMOA, DO, EHO, ESOA, FA, FFA, FFO, FOA, FOX, GJO, GOA, GTO, GWO, HBA, HGS, HHO, JA, MFO, MGO, MPA, MRFO, MSA, NGO, NMRA, OOA, PFA, POA, PSO, SCSO, SeaHO, ServalOA, SFO, SHO, SLO, SRSR, SSA, SSO, SSpiderA, SSpiderO, STO, TDO, TSO, WaOA, WOA, ZOA, - EPC, SMO, SquirrelSA, FDO) + EPC, SMO, SquirrelSA, FDO, WSO) from .system_based import AEO, GCO, WCA from .music_based import HS from .sota_based import LSHADEcnEpSin, IMODE diff --git a/mealpy/swarm_based/WSO.py b/mealpy/swarm_based/WSO.py new file mode 100644 index 00000000..0ba09454 --- /dev/null +++ b/mealpy/swarm_based/WSO.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# ------------------------------------------------------------------------------------------------------% +# Created by "Thieu" at 14:52, 17/03/2020 % +# Email: nguyenthieu2102@gmail.com % +# Github: https://github.com/thieu1995 % +# ------------------------------------------------------------------------------------------------------% +# STRICT PORT from MATLAB Source Code (Braik et al., 2022) % +# ------------------------------------------------------------------------------------------------------% + +import numpy as np +from mealpy.optimizer import Optimizer +from mealpy.utils.agent import Agent + + +class OriginalWSO(Optimizer): + """ + The original version of: White Shark Optimizer (WSO) + + Links: + 1. https://doi.org/10.1016/j.knosys.2022.109210 + 2. https://github.com/malikbraik/White-Shark-Optimizer + + Notes: + 1. Strictly follows MATLAB source code logic. + 2. Frequency (f) is calculated as a constant (~0.899) matching the MATLAB source file + (Line 68 in WSO.m uses division '/', not multiplication '* rand'). + 3. Global Best update uses Local Best memory, strictly following the sequential if-logic of the original code. + 4. Boundary handling mathematically matches MATLAB implementation (ub*a + lb*b). + + Args: + epoch (int): Maximum number of iterations, default = 10000 + pop_size (int): Number of population size (white sharks), default = 100 + f_min (float): Minimum frequency for wave motion, default = 0.07 + f_max (float): Maximum frequency for wave motion, default = 0.75 + tau (float): Acceleration factor for velocity update, default = 4.11 + a0 (float): Movement strength coefficient 0, default = 6.250 + a1 (float): Movement strength coefficient 1, default = 100.0 + a2 (float): Movement strength coefficient 2, default = 0.0005 + + Examples: + >>> import numpy as np + >>> from mealpy import FloatVar, WSO + >>> + >>> 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 = WSO.OriginalWSO(epoch=100, pop_size=50) + >>> g_best = model.solve(problem_dict) + >>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}") + """ + + def __init__(self, epoch: int = 10000, pop_size: int = 100, + f_min: float = 0.07, f_max: float = 0.75, tau: float = 4.11, + a0: float = 6.250, a1: float = 100.0, a2: float = 0.0005, + **kwargs: object) -> None: + 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.f_min = self.validator.check_float("f_min", f_min, (0, 1.0)) + self.f_max = self.validator.check_float("f_max", f_max, (0, 1.0)) + self.tau = self.validator.check_float("tau", tau, (0, 10.0)) + self.a0 = self.validator.check_float("a0", a0, (0, 100.0)) + self.a1 = self.validator.check_float("a1", a1, (0, 1000.0)) + self.a2 = self.validator.check_float("a2", a2, (0, 1.0)) + + self.set_parameters(["epoch", "pop_size", "f_min", "f_max", "tau", "a0", "a1", "a2"]) + self.sort_flag = False + self.is_parallelizable = False + + def initialize_variables(self): + """Initialize algorithm-specific variables""" + self.mu = 2.0 / abs(2.0 - self.tau - np.sqrt(self.tau**2 - 4.0 * self.tau)) + + def generate_empty_agent(self, solution: np.ndarray = None) -> Agent: + if solution is None: + solution = self.problem.generate_solution(encoded=True) + velocity = np.zeros(self.problem.n_dims) + local_solution = solution.copy() + return Agent(solution=solution, velocity=velocity, local_solution=local_solution) + + def generate_agent(self, solution: np.ndarray = None) -> Agent: + agent = self.generate_empty_agent(solution) + agent.target = self.get_target(agent.solution) + # Initialize Personal Best + agent.local_solution = agent.solution.copy() + agent.local_target = agent.target.copy() + return agent + + def evolve(self, epoch): + """ + The main evolution step + """ + mv = 1.0 / (self.a0 + np.exp((self.epoch / 2.0 - epoch) / self.a1)) + s_s = abs(1.0 - np.exp(-self.a2 * epoch / self.epoch)) + + nu = np.floor(self.pop_size * self.generator.random(self.pop_size)).astype(int) + + # 1. Update Velocity + for i in range(self.pop_size): + rmin, rmax = 1.0, 3.0 + rr = rmin + self.generator.random() * (rmax - rmin) + wr = abs((2.0 * self.generator.random() - (1.0 * self.generator.random() + self.generator.random())) / rr) + + # v[i] update + self.pop[i].velocity = self.mu * self.pop[i].velocity + wr * (self.pop[nu[i]].local_solution - self.pop[i].solution) + + # 2. Update Position + for i in range(self.pop_size): + # STRICT MATLAB PORT: f = fmin + (fmax-fmin)/(fmax+fmin) -> Constant (~0.899) + f = self.f_min + (self.f_max - self.f_min) / (self.f_max + self.f_min) + + # Boundary check logic (Using Booleans for safety) + a = self.pop[i].solution > self.problem.ub # Boolean array (Upper Bound Violation) + b = self.pop[i].solution < self.problem.lb # Boolean array (Lower Bound Violation) + wo = np.logical_xor(a, b) # Boolean array (Any Violation) + + if self.generator.random() < mv: + # MATLAB Logic: WSO_Positions(i,:) = WSO_Positions(i,:).*(~wo) + (ub.*a + lb.*b); + # Correct implementation: + # bound_val is calculated exactly as (ub * a + lb * b). + # Note: a and b act as masks (0 or 1). + bound_val = self.problem.ub * a.astype(float) + self.problem.lb * b.astype(float) + + # Apply replacement where violation occurred (wo is True) + self.pop[i].solution = np.where(wo, bound_val, self.pop[i].solution) + else: + self.pop[i].solution = self.pop[i].solution + self.pop[i].velocity / f + + # 3. Schooling (Sequential Chain Effect) + for i in range(self.pop_size): + for j in range(self.problem.n_dims): + if self.generator.random() < s_s: + Dist = abs(self.generator.random() * (self.g_best.solution[j] - 1.0 * self.pop[i].solution[j])) + + if i == 0: + self.pop[i].solution[j] = self.g_best.solution[j] + self.generator.random() * Dist * np.sign(self.generator.random() - 0.5) + else: + WSO_Pos_ij = self.g_best.solution[j] + self.generator.random() * Dist * np.sign(self.generator.random() - 0.5) + self.pop[i].solution[j] = (WSO_Pos_ij + self.pop[i-1].solution[j]) / 2.0 * self.generator.random() + + # 4. Evaluate and Update Best + for i in range(self.pop_size): + # STRICT MATLAB PORT: Only evaluate if WITHIN bounds. Do not clip. + if np.all((self.pop[i].solution >= self.problem.lb) & (self.pop[i].solution <= self.problem.ub)): + + # Evaluate fitness + fit_new = self.get_target(self.pop[i].solution) + self.pop[i].target = fit_new + + # Update Local Best + if self.compare_target(fit_new, self.pop[i].local_target, self.problem.minmax): + self.pop[i].local_solution = self.pop[i].solution.copy() + self.pop[i].local_target = fit_new.copy() + + # Update Global Best (Independent check against Local Best Memory) + if self.compare_target(self.pop[i].local_target, self.g_best.target, self.problem.minmax): + self.g_best.solution = self.pop[i].local_solution.copy() + self.g_best.target = self.pop[i].local_target.copy() \ No newline at end of file diff --git a/tests/swarm_based/test_WSO.py b/tests/swarm_based/test_WSO.py new file mode 100644 index 00000000..963d1507 --- /dev/null +++ b/tests/swarm_based/test_WSO.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# Created by "Thieu" at 19:49, 02/01/2026 ----------% +# Email: nguyenthieu2102@gmail.com % +# Github: https://github.com/thieu1995 % +# --------------------------------------------------% + +from mealpy import FloatVar, WSO, 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, -15, -4, -2, -8], ub=[10, 15, 12, 8, 20]), + "minmax": "min", + "log_to": None + } + return problem + + +def test_WSO_results(problem): + models = [ + WSO.OriginalWSO(epoch=10, pop_size=50) + ] + for model in models: + 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)