Source code for customhys.metaheuristic
"""
This module contains the Metaheuristic class.
Created on Thu Sep 26 16:56:01 2019
@author: Jorge Mario Cruz-Duarte (jcrvz.github.io), e-mail: j.m.cruzduarte@ieee.org
"""
import numpy as np
from . import operators as Operators
from .population import Population
__all__ = ["Metaheuristic", "Population", "Operators"]
__operators__ = Operators.__all__
__selectors__ = ["greedy", "probabilistic", "metropolis", "all", "none"]
[docs]
class Metaheuristic:
"""
This is the Metaheuristic class, each object corresponds to a metaheuristic implemented with a sequence of
search operators from op, and it is based on a population from Population.
"""
[docs]
def __init__(
self,
problem,
search_operators=None,
num_agents: int = 30,
num_iterations: int = 100,
initial_scheme: str = "random",
verbose: bool = False,
):
"""
Create a population-based metaheuristic by employing different simple search operators.
:param dict problem:
This is a dictionary containing the 'function' that maps a 1-by-D array of real values to a real value,
'is_constrained' flag that indicates the solution is inside the search space, and the 'boundaries' (a tuple
with two lists of size D). These two lists correspond to the lower and upper limits of domain, such as:
``boundaries = (lower_boundaries, upper_boundaries)``
**Note:** Dimensions (D) of search domain are read from these boundaries. The problem can be obtained from
the ``benchmark_func`` module.
:param list search_operators:
A list of available search operators. These operators must correspond to those available in the
``operators`` module. This parameter is mandatory for metaheuristic implementations, for using parts of this
class, these can be provided as a list of ``operators``.
:param int num_agents: Optional.
Number of agents or population size. The default is 30.
:param int num_iterations: Optional.
Number of iterations or generations that the metaheuristic is going to perform. The default is 100.
:return: None.
"""
# Read the problem function
self.finalisation_conditions = None
self._problem_function = problem["function"]
# Create population
self.pop = Population(problem["boundaries"], num_agents, problem["is_constrained"])
# Check and read the search_operators
self._search_operators_orig = []
self.perturbators = []
self.selectors = []
if search_operators is not None:
if isinstance(search_operators, np.ndarray):
search_operators = search_operators.tolist()
if not isinstance(search_operators, list):
search_operators = [search_operators]
if len(search_operators) > 0:
# Store original operators for direct calling
self._search_operators_orig = search_operators
self.perturbators, self.selectors = Operators.process_operators(search_operators)
# TODO: consider operators without being dictionary
# Define the maximum number of iterations
self.num_iterations = num_iterations
# Read the number of dimensions
self.num_dimensions = self.pop.num_dimensions
# Read the number of agents
self.num_agents = num_agents
# Initialise historical variables
self.historical: dict = {}
# Set additional variables
self.verbose = verbose
# Set the initial scheme
self.initial_scheme = initial_scheme
[docs]
def apply_initialiser(self):
# Set initial iteration
self.pop.iteration = 0
# Initialise the population
self.pop.initialise_positions(self.initial_scheme) # Default: random
# Evaluate fitness values
self.pop.evaluate_fitness(self._problem_function)
# Update population, particular, and global
self.pop.update_positions("population", "all") # Default
self.pop.update_positions("particular", "all")
self.pop.update_positions("global", "greedy")
[docs]
def apply_search_operator(self, perturbator, selector):
# Dynamic HH can pass a full operator tuple directly.
if isinstance(perturbator, tuple):
operator_name, operator_params, _ = perturbator
else:
# Find the index of this perturbator and use original parameters
idx = self.perturbators.index(perturbator)
operator_name, operator_params, _ = self._search_operators_orig[idx]
# Get the operator function and call it directly
operator_func = getattr(Operators, operator_name)
operator_func(self.pop, **operator_params)
# Evaluate fitness values
self.pop.evaluate_fitness(self._problem_function)
# Update population
if selector in __selectors__:
self.pop.update_positions("population", selector)
else:
self.pop.update_positions()
# Update global position
self.pop.update_positions("global", "greedy")
[docs]
def run(self):
"""
Run the metaheuristic for solving the defined problem.
:return: None.
"""
if (not self.perturbators) or (not self.selectors):
raise Operators.OperatorsError("There are not perturbator or selector!")
# Apply initialiser / Random Sampling
self.apply_initialiser()
# Initialise and update historical variables
self.reset_historicals()
self.update_historicals()
# Report which operators are going to use
self._verbose("\nSearch operators to employ:")
for perturbator, selector in zip(self.perturbators, self.selectors, strict=True):
self._verbose(f"{perturbator} with {selector}")
self._verbose("{}".format("-" * 50))
# Start optimisaton procedure
while not self.finaliser():
# Update the current iteration
self.pop.iteration += 1
# Implement the sequence of operators and selectors
for perturbator, selector in zip(self.perturbators, self.selectors, strict=True):
# Apply the corresponding search operator
self.apply_search_operator(perturbator, selector)
# Update historical variables
self.update_historicals()
# Verbose (if so) some information
self._verbose("{}\npop. radius: {}".format(self.pop.iteration, self.historical["radius"][-1]))
self._verbose(self.pop.get_state())
[docs]
def set_finalisation_conditions(self, conditions):
# TODO: Check that it works for budgets <=
if not isinstance(conditions, list):
conditions = list(conditions)
self.finalisation_conditions = conditions
[docs]
def finaliser(self):
criteria = self.pop.iteration >= self.num_iterations
if self.finalisation_conditions is not None:
for condition in self.finalisation_conditions:
criteria |= condition()
return criteria
[docs]
def get_solution(self):
"""
Deliver the last position and fitness value obtained after ``run`` the metaheuristic procedure.
:returns: ndarray, float
"""
return self.historical["position"][-1], self.historical["fitness"][-1]
[docs]
def reset_historicals(self):
"""
Reset the ``historical`` variables.
:return: None.
"""
self.historical = {"fitness": [], "position": [], "centroid": [], "radius": []}
[docs]
def update_historicals(self):
"""
Update the ``historical`` variables.
:return: None.
"""
# Update historical variables
self.historical["fitness"].append(np.copy(self.pop.global_best_fitness))
self.historical["position"].append(np.copy(self.pop.global_best_position))
# Update population centroid and radius
current_centroid = np.array(self.pop.positions).mean(0)
self.historical["centroid"].append(np.copy(current_centroid))
self.historical["radius"].append(
np.max(np.linalg.norm(self.pop.positions - np.tile(current_centroid, (self.num_agents, 1)), 2, 1))
)
def _verbose(self, text_to_print):
"""
Print each step performed during the solution procedure. It only works if ``verbose`` flag is True.
:param str text_to_print:
Explanation about what the metaheuristic is doing.
:return: None.
"""
if self.verbose:
print(text_to_print)