Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mealpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, DandelionO)
from .system_based import AEO, GCO, WCA
from .music_based import HS
from .sota_based import LSHADEcnEpSin, IMODE
Expand Down
172 changes: 172 additions & 0 deletions mealpy/swarm_based/DandelionO.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env python
# Created by "Thieu" at 10:47, 31/12/2025 ----------%
# Email: [email protected] %
# Github: https://github.com/thieu1995 %
# --------------------------------------------------%

import numpy as np
from mealpy.optimizer import Optimizer


class OriginalDandelionO(Optimizer):
"""
Dandelion Optimizer (DandelionO)

Links:
1. https://doi.org/10.1016/j.engappai.2022.105075
2. https://www.mathworks.com/matlabcentral/fileexchange/114680-dandelion-optimizer

Hyper-parameters should fine-tune in approximate range to get faster convergence toward the global optimum:
+ epoch (int): Maximum number of iterations, default = 10000
+ pop_size (int): Population size, default = 100

Notes:
1. The Levy flight step is calculated using the built-in method get_levy_flight_step() from the Optimizer class.
2. 'q' factor calculation strictly follows Eq. (12) using quadratic coefficients a, b, c.
3. Vectorized implementation allows for efficient execution without explicit loops.
4. This code is based on the original MATLAB code and the original paper[1].

Examples
~~~~~~~~
>>> import numpy as np
>>> from mealpy import FloatVar
>>> from mealpy.bio_based.DandelionO import OriginalDandelionO
>>>
>>> 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 = OriginalDO(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] Shijie Zhao, Tianran Zhang, Shilin Ma, Miao Chen,
Dandelion Optimizer: A nature-inspired metaheuristic algorithm for engineering applications,
Engineering Applications of Artificial Intelligence,Volume 114,2022,105075,ISSN 0952-1976,
https://doi.org/10.1016/j.engappai.2022.105075.

"""
def __init__(self, epoch=10000, pop_size=100, **kwargs):
"""
Args:
epoch (int): Maximum number of iterations, default = 10000
pop_size (int): 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, [10, 10000])
self.set_parameters(["epoch", "pop_size"])
self.sort_flag = True

def get_lognormal_distribution(self):
"""
Calculate Log-normal distribution components based on Eq. (7).
Using standard normal distribution μ=0, σ=1 with numpy.
"""
# Eq. (7) formula implementation using numpy
# Generate random variable from standard normal distribution
y = self.generator.standard_normal((self.pop_size, self.problem.n_dims))

# Standard Log-normal PDF formula: (1 / (y * sigma * sqrt(2*pi))) * exp(- (ln(y) - mu)^2 / (2*sigma^2))
# We use standard normal (mu=0, sigma=1)
sigma = 1.0
mu = 0.0
# Avoid negative/zero values for log calculation
y_abs = np.abs(y) + 1e-100
pdf = (1 / (y_abs * sigma * np.sqrt(2 * np.pi))) * \
np.exp(-((np.log(y_abs) - mu) ** 2) / (2 * sigma ** 2))
return pdf

def evolve(self, epoch):
"""
The main evolution step.
"""
# --- Parameters ---
# Eq. (8) Dynamic alpha parameter, factored version of ((1/Maxiteration^2)*t^2-2/Maxiteration*t+1)
alpha = self.generator.random() * ((1 - epoch / self.epoch) ** 2)

# Current population positions
pop_pos = np.array([agent.solution for agent in self.pop])

# --- 1. Rising Stage (Eq. 12) ---
# Eq. (9) Calculate lift components vx, vy
# theta randomly in [-pi, pi]
theta = self.generator.uniform(-np.pi, np.pi, (self.pop_size, self.problem.n_dims))
r = 1 / np.exp(theta)
v_x = r * np.cos(theta)
v_y = r * np.sin(theta)

# Eq. (7) Log-normal distribution factor
ln_y = self.get_lognormal_distribution()

# Eq. (6) Random positions for exploration
X_s = self.generator.uniform(self.problem.lb, self.problem.ub, (self.pop_size, self.problem.n_dims))

# Eq. (11) Local search domain factor k
# Factored version of (c+a*t^2+b*t) from the paper
q_coef = (epoch / self.epoch) ** 2 - 2 * (epoch / self.epoch) + 1
k = 1 - self.generator.random() * q_coef

# Weather conditions: Random check
# Assuming standard normal distribution check < 1.5 as in many implementations
weather_check = self.generator.standard_normal((self.pop_size, 1))

# Calculate steps for both conditions
# Eq. (5) Clear weather (Exploration)
step_clear = pop_pos + alpha * v_x * v_y * ln_y * (X_s - pop_pos)

# Eq. (10) Rainy weather (Exploitation)
step_rainy = pop_pos * k

# Select based on weather condition
# If check < 1.5 -> Clear weather, else -> Rainy weather
pop_rise = np.where(weather_check < 1.5, step_clear, step_rainy)

# --- 2. Descending Stage (Eq. 13) ---
# Eq. (14) Mean position after rising stage
mean_pos = np.mean(pop_rise, axis=0)

# Brownian motion (Standard Normal Distribution)
brownian = self.generator.standard_normal((self.pop_size, self.problem.n_dims))

# Eq. (13) Update positions
pop_descend = pop_rise - alpha * brownian * (mean_pos - alpha * brownian * pop_rise)

# --- 3. Landing Stage (Eq. 15) ---
# Eq. (16) Levy flight step
# Using mealpy's built-in get_levy_flight_step from Optimizer class
# beta=1.5, multiplier=0.01 as per paper Eq. (16) (s=0.01)
# case=-1 returns the step vector directly
levy_step = self.get_levy_flight_step(beta=1.5, multiplier=0.01, case=-1)

# Eq. (18) Linear increasing function delta (approx 2*t/T)
delta = 2 * epoch / self.epoch

# Elite position (Global Best)
elite_pos = self.g_best.solution

# Eq. (15) Final position update
pop_new_pos = elite_pos + levy_step * alpha * (elite_pos - pop_descend * delta)

# --- Final Update ---
# Check boundaries
pop_new_pos = np.clip(pop_new_pos, self.problem.lb, self.problem.ub)
pop_new_pos = self.amend_solution(pop_new_pos)

# Create new population agents
pop_new = []
for i in range(self.pop_size):
# Using generate_agent implicitly calculates the fitness/target
agent = self.generate_agent(pop_new_pos[i])
pop_new.append(agent)

# Update population
self.pop = pop_new
36 changes: 36 additions & 0 deletions tests/swarm_based/test_DandelionO.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python
# Created by "Thieu" at 14:07, 20/03/2022 ----------%
# Email: [email protected] %
# Github: https://github.com/thieu1995 %
# --------------------------------------------------%

import numpy as np
import pytest

from mealpy import FloatVar, Optimizer
from mealpy.swarm_based.DandelionO import OriginalDandelionO


@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_OriginalDandelionO_results(problem):
models = [
OriginalDandelionO(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)