#!/usr/bin/env python
# Created by "Thieu" at 11:01, 15/08/2025 ----------%
# Email: nguyenthieu2102@gmail.com %
# Github: https://github.com/thieu1995 %
# --------------------------------------------------%
import numpy as np
from mealpy.optimizer import Optimizer
from mealpy.utils.opt_info import OptInfo, ScientificConcern
[docs]class DevSMO(Optimizer):
"""
Our developed version of: Spider Monkey Optimization (SMO)
Parameters
----------
epoch : int
Maximum number of iterations, in range [1, 100000]. Default is 10000.
pop_size : int
Number of population size, in range [5, 10000]. Default is 100.
max_groups : int
Maximum number of groups for spider monkeys, in range [2, 100]. Default is 5.
perturbation_rate : float
Perturbation rate for spider monkeys, in range [0.0, 1.0]. Default is 0.7.
Danger
------
1. The original paper is truly difficult to read and unclear. The operators are somewhat more
understandable, but the pseudocode they provide is inaccurate. In addition, the design
of the two parameters - `local_leader_limit` and `global_leader_limit`, is essentially meaningless.
After each iteration, the population can be split and separated continuously, making it very
unlikely for the if conditions involving these two values to ever be triggered.
As a result, the operators in the two phases local_leader_decision and global_leader_decision will rarely be applied.
2. In summary, this algorithm has many issues, and the original MATLAB source code is also unavailable.
I cannot guarantee its correctness, so I will refer to it as DevSMO.
References
----------
[1] Bansal, J. C., Sharma, H., Jadon, S. S., & Clerc, M. (2014).
Spider monkey optimization algorithm for numerical optimization. Memetic computing, 6(1), 31-47.
https://doi.org/10.1007/s12293-013-0128-0
Examples
--------
>>> import numpy as np
>>> from mealpy import FloatVar, SMO
>>>
>>> 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 = SMO.DevSMO(epoch=1000, pop_size=50, max_groups = 5, perturbation_rate = 0.7)
>>> 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}")
"""
OPT_INFO = OptInfo(name="Spider Monkey Optimization", year=2014, difficulty="nightmare", kind="original",
scientific_status="questionable",
concerns=(
ScientificConcern.LACK_OF_NOVELTY, ScientificConcern.AMBIGUOUS_METHODOLOGY,
ScientificConcern.POOR_REPRODUCIBILITY, ScientificConcern.FABRICATED_RESULTS,
))
def __init__(self, epoch=10000, pop_size=100, max_groups: int = 5, perturbation_rate: float = 0.7, **kwargs):
"""
Args:
epoch (int): maximum number of iterations, default = 10000
pop_size (int): number of population size, default = 100
max_groups (int): Maximum number of groups for spider monkeys, default = 5
perturbation_rate (float): Perturbation rate for spider monkeys, default = 0.7
"""
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.max_groups = self.validator.check_int("max_groups", max_groups, [2, 100])
self.perturbation_rate = self.validator.check_float("perturbation_rate", perturbation_rate, [0., 1.0])
self.set_parameters(["epoch", "pop_size", "max_groups", "perturbation_rate"])
self.sort_flag = False
self.is_parallelizable = False
[docs] def split_fill_by_group(self, pop, n_groups):
"""
Chia theo kiểu lấp đầy từng group: group 0 trước, rồi group 1, ...
k: số group.
"""
n = len(pop)
gsize = -(-n // n_groups) # ceil(n/k)
# Cắt lát theo chỉ số group i
return [pop[i * gsize:(i + 1) * gsize] for i in range(n_groups) if i * gsize < n]
[docs] def merge_groups(self, groups):
return [x for g in groups for x in g]
[docs] def initialize_variables(self):
# Set default parameters as per paper
max_possible_groups = self.pop_size // 3
self.num_groups = min(self.max_groups, max_possible_groups)
self.group_size = -(-self.pop_size // self.num_groups)
self.LLL = self.epoch // 10 # local_leader_limit
self.GLL = self.epoch // 20 # global_leader_limit
# Counters
self.local_limit_counts = [0] * self.num_groups
self.global_limit_count = 0
[docs] def initialization(self) -> None:
if self.pop is None:
self.pop = self.generate_population(self.pop_size)
# Split groups
self.groups = self.split_fill_by_group(self.pop, self.num_groups)
# Get local leaders
self.local_leaders = [self.get_sorted_population(group, self.problem.minmax)[0][0] for group in self.groups]
[docs] def local_leader_phase(self):
"""Local Leader Phase - all monkeys update based on local leader"""
for group_idx, group in enumerate(self.groups):
n_items = len(group)
for idx in range(n_items):
list_rand = self.generator.choice(list(set(range(n_items)) - {idx}), size=self.problem.n_dims, replace=True)
vector = np.array([group[jdx].solution[kdx] for kdx, jdx in enumerate(list_rand)])
pos_new = group[idx].solution + self.generator.uniform(0, 1, self.problem.n_dims) * \
(self.local_leaders[group_idx].solution - group[idx].solution) + \
self.generator.uniform(-1, 1, self.problem.n_dims) * (vector - group[idx].solution)
pos_new = np.where(self.generator.uniform(0, 1, self.problem.n_dims) >= self.perturbation_rate, pos_new, group[idx].solution)
pos_new = self.correct_solution(pos_new)
agent = self.generate_agent(pos_new)
if self.compare_target(agent.target, group[idx].target, self.problem.minmax):
self.groups[group_idx][idx] = agent
self.pop = self.merge_groups(self.groups)
[docs] def global_leader_phase(self):
"""Global Leader Phase - selected monkeys update based on global leader"""
# Calculate selection probabilities
list_fits = np.array([agent.target.fitness for group in self.groups for agent in group])
# Calculate fitness using formula from paper
fitness = np.where(list_fits >= 0, 1 / (1 + list_fits), 1 + np.abs(list_fits))
max_fitness = np.max(fitness)
prob = 0.9 * (fitness / max_fitness) + 0.1
for group_idx, group in enumerate(self.groups):
attempts = 0
while attempts < self.group_size:
for idx, agent in enumerate(group):
if attempts >= self.group_size:
break
prob_idx = idx + group_idx * self.group_size
if self.generator.uniform(0, 1) < prob[prob_idx]:
attempts += 1
# Randomly select dimension to update
k = self.generator.integers(0, self.problem.n_dims)
# Select random monkey
jdx = self.generator.choice(list(set(range(self.group_size)) - {idx}))
pos_new = agent.solution.copy()
# Update using equation (4)
pos_new[k] = pos_new[k] + self.generator.uniform(0, 1) * (self.g_best.solution[k] - pos_new[k]) +\
self.generator.uniform(-1, 1) * (group[jdx].solution[k] - pos_new[k])
# Apply bounds
pos_new = self.correct_solution(pos_new)
agent = self.generate_agent(pos_new)
# Greedy selection
if self.compare_target(agent.target, self.g_best.target, self.problem.minmax):
self.groups[group_idx][idx] = agent
[docs] def local_leader_decision_phase(self):
"""Local Leader Decision Phase - handle stagnated local leaders"""
local_leaders_new = [self.get_sorted_population(group, self.problem.minmax)[0][0] for group in self.groups]
for group_idx, group in enumerate(self.groups):
# Update local limit count
if self.compare_target(self.local_leaders[group_idx].target, local_leaders_new[group_idx].target, self.problem.minmax):
self.local_limit_counts[group_idx] += 1
else:
self.local_limit_counts[group_idx] = 0
self.local_leaders[group_idx] = local_leaders_new[group_idx]
# If local leader is stagnated
if self.local_limit_counts[group_idx] > self.LLL:
self.local_limit_counts[group_idx] = 0
for idx, agent in enumerate(group):
# Random initialization
pos_new_01 = self.generator.uniform(self.problem.lb, self.problem.ub, self.problem.n_dims)
# Update using equation (5)
pos_new_02 = agent.solution + self.generator.uniform(0, 1, self.problem.n_dims) * (self.g_best.solution - agent.solution) + \
self.generator.uniform(0, 1, self.problem.n_dims) * (agent.solution - self.local_leaders[group_idx].solution)
pos_new = np.where(self.generator.uniform(0, 1, self.problem.n_dims) >= self.perturbation_rate, pos_new_01, pos_new_02)
# Apply bounds
pos_new = self.correct_solution(pos_new)
agent = self.generate_agent(pos_new)
# Always accept new position in this phase
self.groups[group_idx][idx] = agent
[docs] def global_leader_decision_phase(self):
"""Global Leader Decision Phase - handle fission-fusion"""
if self.global_limit_count > self.GLL:
self.global_limit_count = 0
if self.generator.random() < 0.5:
# Fission - divide groups
self.pop = self.merge_groups(self.groups)
self.rng.shuffle(self.pop)
self.groups = self.split_fill_by_group(self.pop, self.num_groups)
else:
# Fusion - combine all groups
self.pop = self.merge_groups(self.groups)
# Update local leaders after fission/fusion
self.local_limit_counts = [0] * self.num_groups
self.local_leaders = [self.get_sorted_population(group, self.problem.minmax)[0][0] for group in self.groups]
[docs] def update_leaders(self):
self.pop = self.merge_groups(self.groups)
# Update global leader
g_best_current = self.get_sorted_population(self.pop, self.problem.minmax)[0][0]
if self.compare_target(g_best_current.target, self.g_best.target, self.problem.minmax):
self.g_best = g_best_current
# Update global limit count
self.global_limit_count = 0
else:
self.global_limit_count += 1
[docs] def evolve(self, epoch):
"""
The main operations (equations) of algorithm. Inherit from Optimizer class
Args:
epoch (int): The current iteration
"""
self.local_leader_phase()
self.global_leader_phase()
self.update_leaders()
self.local_leader_decision_phase()
self.global_leader_decision_phase()