Source code for mealpy.swarm_based.DandelionO

#!/usr/bin/env python
# Created by "Halil" at 9:35, 03/01/2026 ----------%
#       Email: halilakbas11@outlook.com            %
#       Github: https://github.com/halilakbas11    %
# -------------------------------------------------%

import numpy as np
from mealpy.optimizer import Optimizer
from mealpy.utils.opt_info import OptInfo


[docs]class OriginalDandelionO(Optimizer): """ The original version: Dandelion Optimizer (DandelionO) Hyperparameters --------------- + epoch (int): Maximum number of iterations, default = 10000 + pop_size (int): Population size, default = 100 Warnings -------- 1. This version is implemented exactly as described in the paper and the author's original MATLAB code. 2. However, in the MATLAB code, the author omitted the 0.01 multiplier in the Levy function, despite it being explicitly mentioned in the paper. Links ----- 1. https://www.mathworks.com/matlabcentral/fileexchange/114680-dandelion-optimizer 2. https://doi.org/10.1016/j.engappai.2022.105075 References ---------- 1. Zhao, S., Zhang, T., Ma, S., & Chen, M. (2022). Dandelion Optimizer: A nature-inspired metaheuristic algorithm for engineering applications. Engineering Applications of Artificial Intelligence, 114, 105075. Examples ~~~~~~~~ >>> import numpy as np >>> from mealpy import FloatVar, DandelionO >>> >>> 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 = DandelionO.OriginalDandelionO(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}") """ OPT_INFO = OptInfo(name="Dandelion Optimizer", year=2022, difficulty="medium", kind="original") def __init__(self, epoch=10000, pop_size=100, **kwargs): 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, 100000]) self.set_parameters(["epoch", "pop_size"]) self.sort_flag = True
[docs] def evolve(self, epoch): """ The main evolution step. """ # --- Parameters --- # Eq. (8) Dynamic alpha parameter alpha = self.generator.random() * ((epoch / self.epoch)**2 - 2.0 * epoch / self.epoch + 1) # Eq. (11) Local search domain factor k aa = 1.0 / (self.epoch ** 2 - 2 *self.epoch + 1) bb = -2 * aa cc = 1 - aa - bb kk = 1 - self.generator.random() * (cc + aa*epoch**2 + bb * epoch) # Current population positions pop_pos = np.array([agent.solution for agent in self.pop]) if self.generator.standard_normal() < 1.5: lamd = np.abs(self.generator.standard_normal(size=(self.pop_size, self.problem.n_dims))) theta = (2 * self.generator.random(self.pop_size) - 1) * np.pi row = 1 / np.exp(theta) vx = row * np.cos(theta) vy = row * np.sin(theta) pop_pos_new = self.generator.random(size=(self.pop_size, self.problem.n_dims)) * (self.problem.ub - self.problem.lb) + self.problem.lb # lognpdf equivalent (mu=0, sigma=1) lognpdf_vals = np.exp(-0.5 * (np.log(lamd)) ** 2) / (lamd * np.sqrt(2 * np.pi)) # Eq. 5 pop_pos_new = pop_pos + np.dot(alpha * vx * vy, lognpdf_vals * (pop_pos_new - pop_pos)) else: pop_pos_new = pop_pos * kk ## Check bound pop_pos = np.clip(pop_pos_new, self.problem.lb, self.problem.ub) ## Decline stage pop_mean = np.mean(pop_pos, axis=0) # Shape: (Dim,) beta_randn = self.generator.standard_normal(size=(self.pop_size, self.problem.n_dims)) dd2 = pop_pos - beta_randn * alpha * (pop_mean - beta_randn * alpha * pop_mean) # Eq.(13) # Check boundaries pop_pos = np.clip(dd2, self.problem.lb, self.problem.ub) ## Landing stage levy_step = self.get_levy_flight_step(beta=1.5, multiplier=0.01, size=(self.pop_size, self.problem.n_dims), case=-1) pop_elite = np.tile(self.g_best.solution, (self.pop_size, 1)) pop_pos = pop_elite + levy_step * alpha * (pop_elite - pop_pos * (2 * epoch / self.epoch)) # Create new population agents for idx in range(self.pop_size): pos_new = self.correct_solution(pop_pos[idx]) self.pop[idx] = self.generate_empty_agent(pos_new) # Update fitness in single mode if self.mode not in self.AVAILABLE_MODES: self.pop[idx].target = self.get_target(pos_new) # Update fitness in parallel modes if self.mode in self.AVAILABLE_MODES: self.pop = self.update_target_for_population(self.pop)
[docs]class DevDandelionO(Optimizer): """ The developed version: Dandelion Optimizer (DandelionO) Hyperparameters --------------- + epoch (int): Maximum number of iterations, default = 10000 + pop_size (int): Population size, default = 100 Danger ------ 1. This dev version was contributed by the user "Halil". Several parameters—such as alpha, a, b, and k, differ from the original paper. 2. Furthermore, the Levy function is applied to the entire population simultaneously, whereas the paper specifies generating a Levy step for each individual. If you choose to use this version, it must be clearly stated that it is not the original implementation. References ---------- 1. Zhao, S., Zhang, T., Ma, S., & Chen, M. (2022). Dandelion Optimizer: A nature-inspired metaheuristic algorithm for engineering applications. Engineering Applications of Artificial Intelligence, 114, 105075. https://doi.org/10.1016/j.engappai.2022.105075 Examples ~~~~~~~~ >>> import numpy as np >>> from mealpy import FloatVar, DandelionO >>> >>> 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 = DandelionO.DevDandelionO(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}") """ OPT_INFO = OptInfo(name="Dandelion Optimizer (Dev)", difficulty="medium", kind="developed") def __init__(self, epoch=10000, pop_size=100, **kwargs): 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, 100000]) self.set_parameters(["epoch", "pop_size"]) self.sort_flag = True
[docs] def get_lognormal_distribution(self): """ Calculate Log-normal distribution components based on Eq. (7). Using standard normal distribution mu=0, sigma=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
[docs] 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 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 --- for idx in range(self.pop_size): pos_new = self.correct_solution(pop_new_pos[idx]) self.pop[idx] = self.generate_empty_agent(pos_new) # Update fitness in single mode if self.mode not in self.AVAILABLE_MODES: self.pop[idx].target = self.get_target(pos_new) # Update fitness in parallel modes if self.mode in self.AVAILABLE_MODES: self.pop = self.update_target_for_population(self.pop)