#!/usr/bin/env python
# Created by "Thieu" at 17:18, 21/05/2022 ----------%
# 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
[docs]class OriginalPO(Optimizer):
"""
The original version of: Political Optimizer (PO) Algorithm
Parameters
----------
epoch : int
Maximum number of iterations, in range [1, 100000]. Default is 10000.
pop_size : int
Number of population size, in range [2, 100]. Default is 8 (Please read the note below about this parameter).
lamda_max : float
Upper limit of the party switching rate, in range [1.0, 100.0]. Default is 1.0.
.. attention::
- `pop_size`: In this algorithm, the `pop_size` parameter corresponds to 'n' from the paper.
It defines the number of political parties and the number of electoral constituencies.
- Actual Population Size: The true number of candidate solutions generated and evaluated
is `pop_size ** 2`. For example, setting `pop_size = 8` (the paper's recommended value)
yields an actual working population of 64 candidates (8 parties * 8 candidates).
References
----------
1. Askari, Q., Younas, I., & Saeed, M. (2020). Political Optimizer: A novel socio-inspired meta-heuristic
for global optimization. Knowledge-based systems, 195, 105709. https://doi.org/10.1016/j.knosys.2020.105709
Examples
~~~~~~~~
>>> import numpy as np
>>> from mealpy import FloatVar, PO
>>>
>>> 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 = PO.OriginalPO(epoch=1000, pop_size=10, lamda_max=1.0)
>>> 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="Political Optimizer", year=2020, difficulty="hard", kind="original")
def __init__(self, epoch: int = 10000, pop_size: int = 8, lamda_max: float=1.0, **kwargs: object) -> None:
"""
Args:
epoch (int): maximum number of iterations, default = 10000
pop_size (int): number of population size, default = 8
lamda_max (float) : Upper limit of the party switching rate, default=1.0
"""
super().__init__(**kwargs)
self.epoch = self.validator.check_int("epoch", epoch, [1, 100000])
self.pop_n = self.validator.check_int("pop_size", pop_size, [2, 100])
self.pop_size = self.pop_n ** 2
self.lamda_max = self.validator.check_int("lamda_max", lamda_max, [1.0, 100])
self.set_parameters(["epoch", "pop_size", "lamda_max"])
self.sort_flag = False
self.lam = None
self.pop_prev = None
self.leaders_idx, self.winners_idx = None, None
[docs] def before_main_loop(self):
self.lam = self.lamda_max
self.pop_prev = self.pop.copy()
fitness = np.array([agent.target.fitness for agent in self.pop])
self.leaders_idx, self.winners_idx = self.get_leaders_and_winners(fitness)
[docs] def rppus_update(self, p_curr, p_prev, m_star, is_improved):
"""
Recent Past-based Position Updating Strategy (RPPUS).
"""
r = self.generator.random(self.problem.n_dims)
# Determine the relationships between previous, current, and referenced positions
c1 = ((p_prev <= p_curr) & (p_curr <= m_star)) | ((p_prev >= p_curr) & (p_curr >= m_star))
c2 = ((p_prev <= m_star) & (m_star <= p_curr)) | ((p_prev >= m_star) & (m_star >= p_curr))
c3 = ((m_star <= p_prev) & (p_prev <= p_curr)) | ((m_star >= p_prev) & (p_prev >= p_curr))
# Eq (9) Updates for improved fitness
eq9_u1 = m_star + r * (m_star - p_curr)
eq9_u2 = m_star + (2 * r - 1) * np.abs(m_star - p_curr)
eq9_u3 = m_star + (2 * r - 1) * np.abs(m_star - p_prev)
# Eq (10) Updates for deteriorated fitness
eq10_u1 = m_star + (2 * r - 1) * np.abs(m_star - p_curr)
eq10_u2 = p_prev + r * (p_curr - p_prev)
eq10_u3 = m_star + (2 * r - 1) * np.abs(m_star - p_prev)
update = np.zeros_like(p_curr)
# Apply Eq (9)
if is_improved:
update = np.where(c1, eq9_u1, update)
update = np.where(c2, eq9_u2, update)
update = np.where(c3, eq9_u3, update)
# Apply Eq (10)
else:
update = np.where(c1, eq10_u1, update)
update = np.where(c2, eq10_u2, update)
update = np.where(c3, eq10_u3, update)
# Fallback for any unassigned edge conditions
unassigned = ~(c1 | c2 | c3)
update = np.where(unassigned, m_star + r * (m_star - p_curr), update)
return update
[docs] def get_leaders_and_winners(self, fitness):
"""
Extract the indices of the party leaders and constituency winners
from the flattened 1D fitness array.
"""
leaders_idx = np.zeros(self.pop_n, dtype=int)
winners_idx = np.zeros(self.pop_n, dtype=int)
# Each party i occupies indices from (i * n) to (i * n + n - 1)
for idx in range(self.pop_n):
start_idx = idx * self.pop_n
end_idx = start_idx + self.pop_n
if self.problem.minmax == "min":
leaders_idx[idx] = start_idx + np.argmin(fitness[start_idx:end_idx])
else:
leaders_idx[idx] = start_idx + np.argmax(fitness[start_idx:end_idx])
# Each constituency j occupies indices j, j+n, j+2n, ..., j+(n-1)n
for jdx in range(self.pop_n):
constituency_indices = np.arange(jdx, self.pop_size, self.pop_n)
if self.problem.minmax == "min":
winners_idx[jdx] = constituency_indices[np.argmin(fitness[constituency_indices])]
else:
winners_idx[jdx] = constituency_indices[np.argmax(fitness[constituency_indices])]
return leaders_idx, winners_idx
[docs] def evolve(self, epoch):
"""
The main operations (equations) of algorithm. Inherit from Optimizer class
Args:
epoch (int): The current iteration
"""
pop_temp = self.pop.copy()
fitness = np.array([agent.target.fitness for agent in self.pop])
# Election Campaign Phase
for k in range(self.pop_size):
party_idx = k // self.pop_n
const_idx = k % self.pop_n
is_improved = False
if self.compare_target(self.pop[k].target, self.pop_prev[k].target, self.problem.minmax):
is_improved = True
# Update w.r.t the party leader
m_party = self.pop[self.leaders_idx[party_idx]].solution
pos_new = self.rppus_update(self.pop[k].solution, self.pop_prev[k].solution, m_party, is_improved)
# Update w.r.t the constituency winner
m_const = self.pop[self.winners_idx[const_idx]].solution
pos_new = self.rppus_update(pos_new, self.pop_prev[k].solution, m_const, is_improved)
# Apply boundary constraints
self.pop[k].solution = self.amend_solution(pos_new)
# Party Switching Phase
for k in range(self.pop_size):
if self.generator.random() < self.lam:
r = self.generator.integers(self.pop_n) # Select a random party
# Find the least fit member of party r
start_idx = r * self.pop_n
end_idx = start_idx + self.pop_n
if self.problem.minmax == "min":
q_local = np.argmax(fitness[start_idx:end_idx])
else:
q_local = np.argmin(fitness[start_idx:end_idx])
q = start_idx + q_local
# Swap the candidate solutions
agent = self.pop[q].copy()
self.pop[q] = self.pop[k].copy()
self.pop[k] = agent.copy()
fitness[k], fitness[q] = fitness[q], fitness[k]
# Election Phase: Recalculate fitness
for idx in range(self.pop_size):
if self.mode not in self.AVAILABLE_MODES:
self.pop[idx].target = self.get_target(self.pop[idx].solution)
if self.mode in self.AVAILABLE_MODES:
self.pop = self.update_target_for_population(self.pop)
fitness = np.array([agent.target.fitness for agent in self.pop])
self.leaders_idx, self.winners_idx = self.get_leaders_and_winners(fitness)
# Parliamentary Affairs Phase
for j in range(self.pop_n):
c_j_idx = self.winners_idx[j]
# Select a random constituency winner r (where r != j)
r = self.sample_indexes_exclude_one(self.generator, self.pop_n, exclude_idx=j, n_samples=1)
c_r_idx = self.winners_idx[r]
a = self.generator.random()
c_new = self.pop[c_r_idx].solution + (2 * a - 1) * np.abs(self.pop[c_r_idx].solution - self.pop[c_j_idx].solution)
pos_new = self.correct_solution(c_new)
agent = self.generate_agent(pos_new)
if self.compare_target(agent.target, self.pop[c_j_idx].target, self.problem.minmax):
self.pop[c_j_idx] = agent
# Update historical archives and party switching rate
self.pop_prev = pop_temp
self.lam -= self.lamda_max / self.epoch