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)