|
| 1 | +#!/usr/bin/env python |
| 2 | +# Created by "Ulaş Görkem Kazan" on 05/01/2026 |
| 3 | +# Github: https://github.com/gorkemulas2005 |
| 4 | +# --------------------------------------------------% |
| 5 | +# Updated by "Thieu" on 16/07/2026 |
| 6 | +# Github: https://github.com/thieu1995 |
| 7 | +# --------------------------------------------------% |
| 8 | + |
| 9 | +import numpy as np |
| 10 | +from mealpy.optimizer import Optimizer |
| 11 | + |
| 12 | + |
| 13 | +class OriginalChameleonSA(Optimizer): |
| 14 | + """ |
| 15 | + The original version of: Chameleon Swarm Algorithm (ChameleonSA) |
| 16 | +
|
| 17 | + Hyperparameters |
| 18 | + --------------- |
| 19 | + + epoch (int): Maximum number of iterations, default = 10000 |
| 20 | + + pop_size (int): Population size, default = 100 |
| 21 | + + pp (float): [0, 1] Probability of the chameleon perceiving prey, default=0.1 |
| 22 | + + p1 (float): [0, 5.0] Exploration control parameter 1 (From PSO), default=0.25 |
| 23 | + + p2 (float): [0, 5.0] Exploration control parameter 2 (From PSO), (default=1.50) |
| 24 | + + c1 (float): [0, 5.0] Personal best influence (From PSO), default=1.75 |
| 25 | + + c2 (float): [0, 5.0] Global best influence (From PSO), default=1.75 |
| 26 | + + gama (float): [0, 2] Constant controlling the exploration rate decay over iterations, default=1.0 |
| 27 | + + alpha (float): [0, 10] Constant defining the steepness of the exploration decay curve, default=3.5 |
| 28 | + + rho (float): [0, 2] Positive number, default=1.0. |
| 29 | +
|
| 30 | + Warnings |
| 31 | + -------- |
| 32 | + 1. This algorithm essentially relies on the update operators of the PSO algorithm. It has too many |
| 33 | + parameters, and the results are nowhere near as good as those presented in the paper |
| 34 | + 2. Please note that the official MATLAB code deviates from the paper, using undocumented |
| 35 | + modifications to artificially boost performance. |
| 36 | + 3. This pure implementation is provided specifically so users can independently evaluate |
| 37 | + the algorithm's true performance based solely on the published mathematical model, |
| 38 | + allowing you to verify whether the paper's claims and results are legitimate. |
| 39 | +
|
| 40 | + Links |
| 41 | + ----- |
| 42 | + 1. https://www.mathworks.com/matlabcentral/fileexchange/98014-chameleon-swarm-algorithm |
| 43 | + 2. https://doi.org/10.1016/j.eswa.2021.114685 |
| 44 | +
|
| 45 | + References |
| 46 | + ---------- |
| 47 | + .. [1] Braik, M. S. (2021). Chameleon Swarm Algorithm: A bio-inspired optimizer for solving |
| 48 | + engineering design problems. Expert Systems with Applications, 174, 114685. |
| 49 | +
|
| 50 | + Examples |
| 51 | + ~~~~~~~~ |
| 52 | + >>> import numpy as np |
| 53 | + >>> from mealpy import FloatVar, ChameleonSA |
| 54 | + >>> |
| 55 | + >>> def objective_function(solution): |
| 56 | + >>> return np.sum(solution**2) |
| 57 | + >>> |
| 58 | + >>> problem_dict = { |
| 59 | + >>> "bounds": FloatVar(lb=(-10.,) * 30, ub=(10.,) * 30, name="delta"), |
| 60 | + >>> "minmax": "min", |
| 61 | + >>> "obj_func": objective_function |
| 62 | + >>> } |
| 63 | + >>> |
| 64 | + >>> model = ChameleonSA.OriginalChameleonSA(epoch=1000, pop_size=50, pp=0.2, p1=0.3, p2=2.0, c1=2.0, c2=2.0, gama=1.0, alpha=5.0, rho=1.5) |
| 65 | + >>> g_best = model.solve(problem_dict) |
| 66 | + >>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}") |
| 67 | + >>> print(f"Solution: {model.g_best.solution}, Fitness: {model.g_best.target.fitness}") |
| 68 | + """ |
| 69 | + |
| 70 | + def __init__(self, epoch=10000, pop_size=100, pp: float=0.1, p1: float=0.25, p2: float=1.50, |
| 71 | + c1: float=1.75, c2: float=1.75, gama: float=1.0, alpha: float=3.5, rho: float=1.0, **kwargs): |
| 72 | + super().__init__(**kwargs) |
| 73 | + self.epoch = self.validator.check_int("epoch", epoch, [1, 100000]) |
| 74 | + self.pop_size = self.validator.check_int("pop_size", pop_size, [5, 100000]) |
| 75 | + self.pp = self.validator.check_float("pp", pp, [0, 1]) |
| 76 | + self.p1 = self.validator.check_float("p1", p1, [0, 10.]) |
| 77 | + self.p2 = self.validator.check_float("p2", p2, [0, 10.]) |
| 78 | + self.c1 = self.validator.check_float("c1", c1, [0, 10.]) |
| 79 | + self.c2 = self.validator.check_float("c2", c2, [0, 10.]) |
| 80 | + self.gama = self.validator.check_float("gama", gama, [0, 10.]) |
| 81 | + self.alpha = self.validator.check_float("alpha", alpha, [0, 10.]) |
| 82 | + self.rho = self.validator.check_float("rho", rho, [0, 10.]) |
| 83 | + |
| 84 | + self.set_parameters(["epoch", "pop_size", "pp", "p1", "p2", "c1", "c2", "gama", "alpha", "rho"]) |
| 85 | + self.sort_flag = True |
| 86 | + self.pop_personal = None |
| 87 | + self.velocities = None |
| 88 | + self.prev_velocities = None |
| 89 | + |
| 90 | + def before_main_loop(self): |
| 91 | + self.pop_personal = self.pop.copy() |
| 92 | + self.velocities = np.zeros((self.pop_size, self.problem.n_dims)) |
| 93 | + self.prev_velocities = np.zeros((self.pop_size, self.problem.n_dims)) |
| 94 | + |
| 95 | + def evolve(self, epoch): |
| 96 | + """ |
| 97 | + The main evolution step. |
| 98 | + """ |
| 99 | + # Dynamic parameters update |
| 100 | + mu = self.gama * np.exp(-self.alpha * epoch / self.epoch) # Eq. 6 |
| 101 | + omega = (1 - epoch / self.epoch) ** (self.rho * np.sqrt(epoch / self.epoch)) # Eq. 19 |
| 102 | + a = 2590 * (1 - np.exp(-np.log(epoch + 1))) # Eq. 21 (acceleration) |
| 103 | + |
| 104 | + for idx in range(self.pop_size): |
| 105 | + # Phase 1: Search for prey (Eq. 3) |
| 106 | + rr = self.generator.random() |
| 107 | + r1, r2, r3 = self.generator.random(3) |
| 108 | + if rr >= self.pp: |
| 109 | + pos_new = self.pop[idx].solution + self.p1 * (self.pop_personal[idx].solution - self.g_best.solution) * r1 + \ |
| 110 | + self.p2 * (self.g_best.solution - self.pop[idx].solution) * r2 |
| 111 | + else: |
| 112 | + direction = np.sign(self.generator.random(self.problem.n_dims) - 0.5) |
| 113 | + pos_new = self.pop[idx].solution + mu * (((self.problem.ub - self.problem.lb) * r3 + self.problem.lb) * direction) |
| 114 | + self.pop[idx].solution = pos_new |
| 115 | + |
| 116 | + center = np.mean([agent.solution for agent in self.pop], axis=0) |
| 117 | + for idx in range(self.pop_size): |
| 118 | + # Phase 2: Chameleon eyes rotation (Eq. 11 - 15) |
| 119 | + yc = self.pop[idx].solution - center # Eq. 13 |
| 120 | + theta = self.generator.random(self.problem.n_dims) * np.sign(self.generator.random(self.problem.n_dims) - 0.5) * np.pi # Eq. 15 |
| 121 | + # Apply rotation (Eq. 12) |
| 122 | + yr = np.cos(theta) * yc |
| 123 | + self.pop[idx].solution = yr + center # Eq. 11 |
| 124 | + |
| 125 | + for idx in range(self.pop_size): |
| 126 | + # Phase 3: Hunting prey - Tongue projection (Eq. 18 - 20) |
| 127 | + r1, r2 = self.generator.random(2) |
| 128 | + self.velocities[idx] = omega * self.velocities[idx] + \ |
| 129 | + self.c1 * r1 * (self.g_best.solution - self.pop[idx].solution) + \ |
| 130 | + self.c2 * r2 * (self.pop_personal[idx].solution - self.pop[idx].solution) |
| 131 | + delta_v_squared = (self.velocities[idx] ** 2 - self.prev_velocities[idx] ** 2) |
| 132 | + tongue_step = delta_v_squared / (2 * (a + self.EPSILON)) |
| 133 | + self.pop[idx].solution = self.pop[idx].solution + tongue_step |
| 134 | + self.prev_velocities[idx] = self.velocities[idx].copy() |
| 135 | + |
| 136 | + # Adjust boundaries (Line 32) |
| 137 | + for idx in range(self.pop_size): |
| 138 | + self.pop[idx].solution = self.correct_solution(self.pop[idx].solution) |
| 139 | + if self.mode not in self.AVAILABLE_MODES: |
| 140 | + self.pop[idx].target = self.get_target(self.pop[idx].solution) |
| 141 | + # Update fitness in parallel modes |
| 142 | + if self.mode in self.AVAILABLE_MODES: |
| 143 | + self.pop = self.update_target_for_population(self.pop) |
| 144 | + |
| 145 | + # Update personal best positions |
| 146 | + for idx in range(self.pop_size): |
| 147 | + if self.compare_target(self.pop[idx].target, self.pop_personal[idx].target, self.problem.minmax): |
| 148 | + self.pop_personal[idx] = self.pop[idx].copy() |
| 149 | + |
| 150 | + |
| 151 | +class IChameleonSA(Optimizer): |
| 152 | + """ |
| 153 | + The original version of: Improved Chameleon Swarm Algorithm (ICSA) |
| 154 | +
|
| 155 | + Hyperparameters |
| 156 | + --------------- |
| 157 | + + epoch (int): Maximum number of iterations, default = 10000 |
| 158 | + + pop_size (int): Population size, default = 100 |
| 159 | + + beta (float): Lévy flight constant (Eq. 13), default = 1.5 |
| 160 | + + r_chaos (float): Control parameter for logistic mapping (Eq. 10), default = 0.3 |
| 161 | + + k_spiral (int): Variation coefficient for spiral search (Eq. 11), default = 5 |
| 162 | + + p1 (float): [0, 10.] Personal best influence (From PSO), default=2.0 |
| 163 | + + p2 (float): [0, 10.] Global best influence (From PSO), default=2.0 |
| 164 | +
|
| 165 | + Warnings |
| 166 | + -------- |
| 167 | + 1. Despite being claimed as an improved version, this algorithm still requires too many |
| 168 | + parameters and relies on standard PSO update operators |
| 169 | + 2. Additionally, its NFE per iteration is 3x times higher than typical algorithms, |
| 170 | + so users should be mindful of the execution time. |
| 171 | +
|
| 172 | + References |
| 173 | + ---------- |
| 174 | + .. [1] Chen, Yaodan, Li Cao, and Yinggao Yue. "Hybrid Multi-Objective Chameleon Optimization Algorithm |
| 175 | + Based on Multi-Strategy Fusion and Its Applications." Biomimetics 9.10 (2024): 583. |
| 176 | + https://doi.org/10.3390/biomimetics9100583 |
| 177 | +
|
| 178 | + Examples |
| 179 | + ~~~~~~~~ |
| 180 | + >>> import numpy as np |
| 181 | + >>> from mealpy import FloatVar, ChameleonSA |
| 182 | + >>> |
| 183 | + >>> def objective_function(solution): |
| 184 | + >>> return np.sum(solution**2) |
| 185 | + >>> |
| 186 | + >>> problem_dict = { |
| 187 | + >>> "bounds": FloatVar(lb=(-10.,) * 30, ub=(10.,) * 30, name="delta"), |
| 188 | + >>> "minmax": "min", |
| 189 | + >>> "obj_func": objective_function |
| 190 | + >>> } |
| 191 | + >>> |
| 192 | + >>> model = ChameleonSA.IChameleonSA(epoch=1000, pop_size=50, r_chaos=0.5, k_spiral=10., p1=5.0, p2=3.0) |
| 193 | + >>> g_best = model.solve(problem_dict) |
| 194 | + >>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}") |
| 195 | + """ |
| 196 | + |
| 197 | + def __init__(self, epoch=1000, pop_size=100, r_chaos: float=0.3, k_spiral: float=5.0, p1: float=2.0, p2: float=2.0, **kwargs): |
| 198 | + super().__init__(**kwargs) |
| 199 | + self.epoch = self.validator.check_int("epoch", epoch, [1, 100000]) |
| 200 | + self.pop_size = self.validator.check_int("pop_size", pop_size, [10, 10000]) |
| 201 | + self.r_chaos = self.validator.check_float("r_chaos", r_chaos, [0.0, 1.0]) |
| 202 | + self.k_spiral = self.validator.check_float("k_spiral", k_spiral, [0., 100.0]) |
| 203 | + self.p1 = self.validator.check_float("p1", p1, [0., 100.0]) |
| 204 | + self.p2 = self.validator.check_float("p2", p2, [0., 100.0]) |
| 205 | + self.set_parameters(["epoch", "pop_size", "r_chaos", "k_spiral", "p1", "p2"]) |
| 206 | + self.sort_flag = False |
| 207 | + self.V = self.pop_personal = None |
| 208 | + |
| 209 | + def initialization(self) -> None: |
| 210 | + if self.pop is None: |
| 211 | + # 4.1. Logistic Chaotic Map Initialization (Eq. 9 & 10) |
| 212 | + l_seq = self.generator.random((self.pop_size, self.problem.n_dims)) |
| 213 | + for j in range(self.pop_size - 1): |
| 214 | + l_seq[j + 1] = self.r_chaos * l_seq[j] * (1 - l_seq[j]) # Eq. (10) |
| 215 | + pop_pos = self.problem.lb + l_seq * (self.problem.ub - self.problem.lb) # Eq. (9) |
| 216 | + self.pop = [] |
| 217 | + for idx in range(self.pop_size): |
| 218 | + pos_new = self.correct_solution(pop_pos[idx]) |
| 219 | + self.pop.append(self.generate_empty_agent(pos_new)) |
| 220 | + if self.mode not in self.AVAILABLE_MODES: |
| 221 | + self.pop[idx].target = self.get_target(pos_new) |
| 222 | + # Update fitness in parallel modes |
| 223 | + if self.mode in self.AVAILABLE_MODES: |
| 224 | + self.pop = self.update_target_for_population(self.pop) |
| 225 | + self.V = np.zeros((self.pop_size, self.problem.n_dims)) |
| 226 | + self.pop_personal = self.pop.copy() |
| 227 | + |
| 228 | + def evolve(self, epoch): |
| 229 | + mu = np.exp(-(3.5 * epoch/ self.epoch) ** 3) # Eq. (2) |
| 230 | + |
| 231 | + # Phase 1: Search for prey |
| 232 | + mean_fit = np.mean([agent.target.fitness for agent in self.pop]) |
| 233 | + for idx in range(self.pop_size): |
| 234 | + if self.compare_fitness(self.pop[idx].target.fitness, mean_fit, self.problem.minmax): |
| 235 | + r1, r2 = self.generator.random(2) |
| 236 | + pos_new = self.pop[idx].solution + self.p1 * r1 * (self.pop_personal[idx].solution - self.g_best.solution) + \ |
| 237 | + self.p2 * r1 * (self.g_best.solution - self.pop[idx].solution) |
| 238 | + else: |
| 239 | + ll = self.generator.uniform(-1, 1) |
| 240 | + sgn = np.sign(self.generator.random() - 0.5) |
| 241 | + pos_new = self.g_best.solution + np.exp(self.k_spiral * ll) * np.cos(2 * np.pi * ll) * (self.g_best.solution - self.pop[idx].solution) * sgn |
| 242 | + self.pop[idx].solution = pos_new |
| 243 | + |
| 244 | + # Phase 2: Chameleon eyes' rotation |
| 245 | + c_t = 0.075 * (1 + np.cos((np.pi * epoch) / self.epoch)) |
| 246 | + pop_new = [] |
| 247 | + for idx in range(self.pop_size): |
| 248 | + # Levy flight step calculation |
| 249 | + levy_step = self.get_levy_flight_step(1.5, multiplier=1, size=self.problem.n_dims, case=-1) |
| 250 | + # Rotation & Levy flight combination (Eq. 15) |
| 251 | + xl = self.pop[idx].solution + c_t * (self.g_best.solution - self.pop[idx].solution) * levy_step |
| 252 | + pos_new = self.correct_solution(xl) |
| 253 | + agent = self.generate_empty_agent(pos_new) |
| 254 | + pop_new.append(agent) |
| 255 | + # Greedy selection (Eq. 16) |
| 256 | + if self.mode not in self.AVAILABLE_MODES: |
| 257 | + agent.target = self.get_target(pos_new) |
| 258 | + self.pop[idx] = self.get_better_agent(agent, self.pop[idx], self.problem.minmax) |
| 259 | + # Parallel mode |
| 260 | + if self.mode in self.AVAILABLE_MODES: |
| 261 | + pop_new = self.update_target_for_population(pop_new) |
| 262 | + self.pop = self.greedy_selection_population(self.pop, pop_new, self.problem.minmax) |
| 263 | + |
| 264 | + # Phase 3: Hunting prey |
| 265 | + omega_t = (1 - epoch / self.epoch) ** (2 * np.sqrt(epoch / self.epoch)) |
| 266 | + lambda_t = (1 - epoch / self.epoch) ** np.sqrt(epoch / self.epoch) |
| 267 | + a = 2590 * (1 - np.exp(-np.log(epoch))) |
| 268 | + |
| 269 | + for idx in range(self.pop_size): |
| 270 | + # Update velocity (Eq. 18) |
| 271 | + r1, r2 = self.generator.random(2) |
| 272 | + V_new = lambda_t * self.V[idx] + omega_t * r1 * (self.g_best.solution - self.pop[idx].solution) + \ |
| 273 | + omega_t * r2 * (self.pop_personal[idx].solution - self.pop[idx].solution) |
| 274 | + # Update position (Eq. 7) |
| 275 | + if a == 0: |
| 276 | + X_hunt = self.pop[idx].solution |
| 277 | + else: |
| 278 | + X_hunt = self.pop[idx].solution + ((V_new ** 2) - (self.V[idx] ** 2)) / (2 * a) |
| 279 | + self.V[idx] = V_new |
| 280 | + # Refraction reverse learning (Eq. 20) |
| 281 | + X_refract = ((self.problem.lb + self.problem.ub) / 2) + ((self.problem.lb + self.problem.ub) / (2 * epoch)) - (X_hunt / epoch) |
| 282 | + X_hunt = self.correct_solution(X_hunt) |
| 283 | + X_refract = self.correct_solution(X_refract) |
| 284 | + agent_hunt = self.generate_agent(X_hunt) |
| 285 | + agent_refract = self.generate_agent(X_refract) |
| 286 | + |
| 287 | + # Greedy selection (Eq. 16) |
| 288 | + if self.compare_target(agent_refract.target, agent_hunt.target, self.problem.minmax): |
| 289 | + self.pop[idx] = agent_refract |
| 290 | + else: |
| 291 | + self.pop[idx] = agent_hunt |
| 292 | + |
| 293 | + # Evaluate fitness and update Personal |
| 294 | + for idx in range(self.pop_size): |
| 295 | + if self.compare_target(self.pop[idx].target, self.pop_personal[idx].target, self.problem.minmax): |
| 296 | + self.pop_personal[idx] = self.pop[idx].copy() |
0 commit comments