Implementing the Simulated Annealing metaheuristic with MetaGen

In this example a simple SimulatedAnnealing algorithm has been developed using the metagen framework.

Initialization

The SA class is defined, and its constructor (__init__) is provided with the following parameters:

  • domain: Domain: The domain of possible solutions.

  • fitness: Callable[[Solution], float]: A function that calculates the fitness of a solution.

  • search_space_size: int = 30: The number of potential solutions to generate.

  • n_iterations: int = 20: The number of search iterations to perform.

  • alteration_limit: float = 0.1: The alteration applied to every Solution to generate the neighbors.

  • initial_temp: float = 50.0: The initial temperature for the simulated annealing.

  • cooling_rate: float = 0.99: Meassures the speed of the cooling procedure.

  • The constructor stores these parameters as instance variables.

Generating initial Solution

Initially, a random solution is gennerated from the defined Domain.

Best Solution search The simulated annealing process attempts to find a global optimum by allowing occasional acceptance of worse solutions.

The algorithm, iterates for the specified number of iterations (n_iterations).

In each iteration:
  • Creates a neighboring solution by copying and mutating the current solution.

  • Evaluates the neighbor’s fitness.

  • Computes an exploration rate based on the fitness difference and current temperature.

  • Accepts the neighbor as the new solution if it is better or based on a probability influenced by the exploration rate.

  • Lowers the temperature according to the cooling rate.

Finally, the run method returns the best solution found after all iterations.

from metagen.framework import Domain, Solution
from collections.abc import Callable
from copy import deepcopy
import random
import math

class SA:

    def __init__(self, domain: Domain, fitness_func: Callable[[Solution], float], n_iterations: int = 50, alteration_limit: float=0.1, initial_temp: float = 50.0, cooling_rate: float=0.99) -> None:

        self.domain: Domain = domain
        self.n_iterations: int = n_iterations
        self.initial_temp: float = initial_temp
        self.alteration_limit: Any = alteration_limit
        self.cooling_rate: float = cooling_rate
        self.solution = None
        self.fitness_func: Callable[[Solution], float] = fitness_func

        self.initialize()

    def initialize(self):
        """
        Initialize the population of solutions by creating and evaluating initial solutions.
        """
        self.solution = Solution()
        self.solution.evaluate(self.fitness_func)


    def run(self) -> Solution:
        """
        Run the simulated annealing for the specified number of generations and return the best solution found.

        :return: The best solution found by the simulated annealing.
        :rtype: Solution
        """

        current_iteration = 0
        temperature = self.initial_temp


        while current_iteration <= self.n_iterations:

            neighbour = deepcopy(self.solution)

            neighbour.mutate(alteration_limit=self.alteration_limit)

            neighbour.evaluate(self.fitness_func)

            exploration_rate = math.exp((self.solution.fitness - neighbour.fitness) / temperature)

            if neighbour.fitness < self.solution.fitness or exploration_rate > random.random():
                self.solution = neighbour

            temperature *= self.cooling_rate

            current_iteration += 1

        return self.solution