├── library ├── __init__.py ├── utils.py ├── problems │ ├── data │ │ ├── ks_data.py │ │ ├── warehouse_data.py │ │ └── tsp_data.py │ ├── int_max.py │ ├── int_bin.py │ ├── warehouse.py │ ├── tsp.py │ └── ks.py ├── solution.py ├── algorithms │ ├── genetic_algorithms │ │ ├── selection.py │ │ ├── algorithm.py │ │ ├── mutation.py │ │ └── crossover.py │ ├── pso.py │ ├── hill_climbing.py │ └── simulated_annealing.py └── README.md ├── .gitignore ├── requirements.txt ├── notebooks-class ├── images │ ├── fps.png │ ├── pso.png │ ├── cycle-xo.png │ ├── solution.png │ ├── inheritance.jpg │ ├── class-object.jpg │ ├── ks-solutions.png │ ├── std-crossover.png │ ├── swap-mutation.png │ ├── tsp-solution.png │ ├── tsp-solutions.png │ ├── intbin-solution.png │ ├── binary-std-mutation.png │ ├── fitness-landscape.jpeg │ ├── inversion-mutation.png │ ├── fitness-landscape-3d.png │ ├── hillclimbing-solution.png │ ├── tsp-hillclimbing-solution.png │ └── intbin-hillclimbing-solution.png ├── P6-GeneticAlgorithms-Part1.ipynb ├── P5-SimulatedAnnealling-TSP-KS.ipynb ├── P8-Genetic-Algorithms-Part3.ipynb ├── P4-HillClimbing-TSP-and-KS.ipynb ├── P10-PSO.ipynb ├── P2-HillClimbing-IntBin-Part1.ipynb └── P7-GeneticAlgorithms-Part2.ipynb ├── notebooks-solution ├── images │ ├── fps.png │ ├── pso.png │ ├── cycle-xo.png │ ├── solution.png │ ├── inheritance.jpg │ ├── class-object.jpg │ ├── ks-solutions.png │ ├── std-crossover.png │ ├── swap-mutation.png │ ├── tsp-solution.png │ ├── tsp-solutions.png │ ├── intbin-solution.png │ ├── fitness-landscape.jpeg │ ├── inversion-mutation.png │ ├── binary-std-mutation.png │ ├── fitness-landscape-3d.png │ ├── hillclimbing-solution.png │ ├── tsp-hillclimbing-solution.png │ └── intbin-hillclimbing-solution.png ├── P6-GeneticAlgorithms-Part1.ipynb ├── P5-SimulatedAnnealling-TSP-KS.ipynb ├── P2-HillClimbing-IntBin-Part1.ipynb └── P8-Genetic-Algorithms-Part3.ipynb └── README.md /library/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__ 2 | *DS_store 3 | *venv -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==2.2.2 2 | pandas==2.2.3 3 | seaborn==0.13.2 -------------------------------------------------------------------------------- /notebooks-class/images/fps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/fps.png -------------------------------------------------------------------------------- /notebooks-class/images/pso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/pso.png -------------------------------------------------------------------------------- /notebooks-solution/images/fps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/fps.png -------------------------------------------------------------------------------- /notebooks-solution/images/pso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/pso.png -------------------------------------------------------------------------------- /notebooks-class/images/cycle-xo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/cycle-xo.png -------------------------------------------------------------------------------- /notebooks-class/images/solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/solution.png -------------------------------------------------------------------------------- /notebooks-class/images/inheritance.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/inheritance.jpg -------------------------------------------------------------------------------- /notebooks-solution/images/cycle-xo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/cycle-xo.png -------------------------------------------------------------------------------- /notebooks-solution/images/solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/solution.png -------------------------------------------------------------------------------- /notebooks-class/images/class-object.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/class-object.jpg -------------------------------------------------------------------------------- /notebooks-class/images/ks-solutions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/ks-solutions.png -------------------------------------------------------------------------------- /notebooks-class/images/std-crossover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/std-crossover.png -------------------------------------------------------------------------------- /notebooks-class/images/swap-mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/swap-mutation.png -------------------------------------------------------------------------------- /notebooks-class/images/tsp-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/tsp-solution.png -------------------------------------------------------------------------------- /notebooks-class/images/tsp-solutions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/tsp-solutions.png -------------------------------------------------------------------------------- /notebooks-solution/images/inheritance.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/inheritance.jpg -------------------------------------------------------------------------------- /notebooks-class/images/intbin-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/intbin-solution.png -------------------------------------------------------------------------------- /notebooks-solution/images/class-object.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/class-object.jpg -------------------------------------------------------------------------------- /notebooks-solution/images/ks-solutions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/ks-solutions.png -------------------------------------------------------------------------------- /notebooks-solution/images/std-crossover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/std-crossover.png -------------------------------------------------------------------------------- /notebooks-solution/images/swap-mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/swap-mutation.png -------------------------------------------------------------------------------- /notebooks-solution/images/tsp-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/tsp-solution.png -------------------------------------------------------------------------------- /notebooks-solution/images/tsp-solutions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/tsp-solutions.png -------------------------------------------------------------------------------- /notebooks-class/images/binary-std-mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/binary-std-mutation.png -------------------------------------------------------------------------------- /notebooks-class/images/fitness-landscape.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/fitness-landscape.jpeg -------------------------------------------------------------------------------- /notebooks-class/images/inversion-mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/inversion-mutation.png -------------------------------------------------------------------------------- /notebooks-solution/images/intbin-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/intbin-solution.png -------------------------------------------------------------------------------- /notebooks-class/images/fitness-landscape-3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/fitness-landscape-3d.png -------------------------------------------------------------------------------- /notebooks-class/images/hillclimbing-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/hillclimbing-solution.png -------------------------------------------------------------------------------- /notebooks-solution/images/fitness-landscape.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/fitness-landscape.jpeg -------------------------------------------------------------------------------- /notebooks-solution/images/inversion-mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/inversion-mutation.png -------------------------------------------------------------------------------- /notebooks-solution/images/binary-std-mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/binary-std-mutation.png -------------------------------------------------------------------------------- /notebooks-solution/images/fitness-landscape-3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/fitness-landscape-3d.png -------------------------------------------------------------------------------- /notebooks-solution/images/hillclimbing-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/hillclimbing-solution.png -------------------------------------------------------------------------------- /notebooks-class/images/tsp-hillclimbing-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/tsp-hillclimbing-solution.png -------------------------------------------------------------------------------- /notebooks-class/images/intbin-hillclimbing-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-class/images/intbin-hillclimbing-solution.png -------------------------------------------------------------------------------- /notebooks-solution/images/tsp-hillclimbing-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/tsp-hillclimbing-solution.png -------------------------------------------------------------------------------- /notebooks-solution/images/intbin-hillclimbing-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inesmcm26/CIFO-24-25/HEAD/notebooks-solution/images/intbin-hillclimbing-solution.png -------------------------------------------------------------------------------- /library/utils.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import seaborn as sns 3 | 4 | def draw_2D_fitness_landscape(ordered_solution_space): 5 | fitness_values = [solution.fitness() for solution in ordered_solution_space] 6 | 7 | x_labels = [repr(solution) for solution in ordered_solution_space] 8 | 9 | sns.lineplot(x=x_labels, y=fitness_values) 10 | 11 | plt.ylabel('Fitness') 12 | plt.xlabel('Solutions') 13 | plt.show() -------------------------------------------------------------------------------- /library/problems/data/ks_data.py: -------------------------------------------------------------------------------- 1 | values = [ 2 | 360, 83, 59, 130, 431, 67, 230, 52, 93, 125, 670, 892, 600, 38, 48, 147, 3 | 78, 256, 63, 17, 120, 164, 432, 35, 92, 110, 22, 42, 50, 323, 514, 28, 4 | 87, 73, 78, 15, 26, 78, 210, 36, 85, 189, 274, 43, 33, 10, 19, 389, 276, 5 | 312 6 | ] 7 | weights = [ 8 | 7, 0, 30, 22, 80, 94, 11, 81, 70, 64, 59, 18, 0, 36, 3, 8, 15, 42, 9, 0, 9 | 42, 47, 52, 32, 26, 48, 55, 6, 29, 84, 2, 4, 18, 56, 7, 29, 93, 44, 71, 10 | 3, 86, 66, 31, 65, 0, 79, 20, 65, 52, 13 11 | ] 12 | capacity = 850 -------------------------------------------------------------------------------- /library/problems/data/warehouse_data.py: -------------------------------------------------------------------------------- 1 | # Coordinates of 10 customer locations 2 | customer_locations = [ 3 | [38.8898802, -9.0341101], 4 | [38.8911723, -9.0638357], 5 | [38.8839022, -9.1645285], 6 | [38.9769897, -9.1696285], 7 | [38.8698045, -9.2052441], 8 | [38.9840018, -9.0766157], 9 | [38.9533576, -9.1338810], 10 | [38.9322459, -9.2619402], 11 | [38.8781662, -9.1651225], 12 | [38.9571744, -8.9936193] 13 | ] 14 | 15 | # Corresponding delivery cost 16 | delivery_cost = [1.2, 0.8, 1.5, 1.1, 0.9, 1.3, 1.0, 1.4, 0.7, 1.0] 17 | -------------------------------------------------------------------------------- /library/solution.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from abc import ABC, abstractmethod 3 | 4 | class Solution(ABC): 5 | def __init__(self, repr=None): 6 | # To initialize a solution we need to know it's representation. If no representation is given, a solution is randomly initialized. 7 | if repr == None: 8 | repr = self.random_initial_representation() 9 | # Attributes 10 | self.repr = repr 11 | 12 | # Method that is called when we run print(object of the class) 13 | def __repr__(self): 14 | return str(self.repr) 15 | 16 | # Other methods that must be implemented in subclasses 17 | @abstractmethod 18 | def fitness(self): 19 | pass 20 | 21 | @abstractmethod 22 | def random_initial_representation(self): 23 | pass 24 | 25 | class PSOSolution(Solution): 26 | def __init__( 27 | self, 28 | repr = None, 29 | ): 30 | super().__init__(repr=repr) 31 | 32 | self.best_repr = self.repr 33 | self.best_fitness = self.fitness() 34 | self.velocity = np.array([0 for _ in range(len(self.repr))]) -------------------------------------------------------------------------------- /library/problems/data/tsp_data.py: -------------------------------------------------------------------------------- 1 | # TSP data from https://developers.google.com/optimization/routing/tsp 2 | 3 | distance_matrix = [ 4 | [0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972], 5 | [2451, 0, 1745, 1524, 831, 1240, 959, 2596, 403, 1589, 1374, 357, 579], 6 | [713, 1745, 0, 355, 920, 803, 1737, 851, 1858, 262, 940, 1453, 1260], 7 | [1018, 1524, 355, 0, 700, 862, 1395, 1123, 1584, 466, 1056, 1280, 987], 8 | [1631, 831, 920, 700, 0, 663, 1021, 1769, 949, 796, 879, 586, 371], 9 | [1374, 1240, 803, 862, 663, 0, 1681, 1551, 1765, 547, 225, 887, 999], 10 | [2408, 959, 1737, 1395, 1021, 1681, 0, 2493, 678, 1724, 1891, 1114, 701], 11 | [213, 2596, 851, 1123, 1769, 1551, 2493, 0, 2699, 1038, 1605, 2300, 2099], 12 | [2571, 403, 1858, 1584, 949, 1765, 678, 2699, 0, 1744, 1645, 653, 600], 13 | [875, 1589, 262, 466, 796, 547, 1724, 1038, 1744, 0, 679, 1272, 1162], 14 | [1420, 1374, 940, 1056, 879, 225, 1891, 1605, 1645, 679, 0, 1017, 1200], 15 | [2145, 357, 1453, 1280, 586, 887, 1114, 2300, 653, 1272, 1017, 0, 504], 16 | [1972, 579, 1260, 987, 371, 999, 701, 2099, 600, 1162, 1200, 504, 0], 17 | ] -------------------------------------------------------------------------------- /library/algorithms/genetic_algorithms/selection.py: -------------------------------------------------------------------------------- 1 | import random 2 | from copy import deepcopy 3 | 4 | from library.solution import Solution 5 | 6 | 7 | def fitness_proportionate_selection(population: list[Solution], maximization: bool): 8 | if maximization: 9 | fitness_values = [] 10 | for ind in population: 11 | if ind.fitness() < 0: 12 | # If fitness is negative (invalid solution like in Knapsack) 13 | # Set fitness to very small positive value 14 | # Probability of selecting this individual is nearly 0. 15 | fitness_values.append(0.0000001) 16 | else: 17 | fitness_values.append(ind.fitness()) 18 | else: 19 | # Minimization: Use the inverse of the fitness value 20 | # Lower fitness should have higher probability of being selected 21 | fitness_values = [1 / ind.fitness() for ind in population] 22 | 23 | total_fitness = sum(fitness_values) 24 | # Generate random number between 0 and total 25 | random_nr = random.uniform(0, total_fitness) 26 | # For each individual, check if random number is inside the individual's "box" 27 | box_boundary = 0 28 | for ind_idx, ind in enumerate(population): 29 | box_boundary += fitness_values[ind_idx] 30 | if random_nr <= box_boundary: 31 | return deepcopy(ind) 32 | -------------------------------------------------------------------------------- /library/problems/int_max.py: -------------------------------------------------------------------------------- 1 | """Problem definition 2 | 3 | Description: The IntMax problem consists of finding the biggest integer between 1 and some N 4 | 5 | Search space: Integers from 1 to 15. 6 | 7 | Fitness function: f(x)=x (i.e., the number itself). 8 | 9 | Neighbors: Each integer x has at most two neighbors: x-1 and x+1, except for boundaries (1 and 15). 10 | 11 | Goal: Maximize f(x). 12 | """ 13 | 14 | import random 15 | 16 | from library.solution import Solution 17 | 18 | class IntMaxSolution(Solution): 19 | def __init__(self, repr=None): 20 | # Check if valid repr was passed 21 | if repr: 22 | if (not isinstance(repr, int)) or (repr < 1) or (repr > 15): 23 | raise ValueError("IntMax Solution representation must be an integer between 1 and 15") 24 | 25 | super().__init__(repr=repr) 26 | 27 | def fitness(self): 28 | return self.repr 29 | 30 | def random_initial_representation(self): 31 | return random.randint(1, 15) 32 | 33 | # Algorithm specific 34 | class IntMaxHillClimbingSolution(IntMaxSolution): 35 | def get_neighbors(self): 36 | # Boundaries 37 | if self.repr == 1: 38 | return [IntMaxHillClimbingSolution(2)] 39 | elif self.repr == 15: 40 | return [IntMaxHillClimbingSolution(14)] 41 | else: 42 | return [ 43 | IntMaxHillClimbingSolution(self.repr-1), 44 | IntMaxHillClimbingSolution(self.repr+1) 45 | ] -------------------------------------------------------------------------------- /library/README.md: -------------------------------------------------------------------------------- 1 | A modular optimization library designed to solve various optimization problems using optimization algorithms. 2 | 3 | ## How it works 4 | 5 | The library follows a structured approach: 6 | 7 | 1. **Abstract Base Class (Solution)** 8 | - Defines a generic solution structure for all optimization problems. 9 | - Requires implementation of `fitness()` and `random_initial_representation()`. 10 | - Defined in `solution.py` 11 | 12 | 2. **Problem-Specific Solution Classes** 13 | 14 | - Extend `Solution` and implement problem-specific methods. 15 | - Implements the `fitness()` and `random_initial_representation()` methods 16 | - Example: `TSPSolution` for the Traveling Salesperson Problem in `problems/tsp.py` 17 | 18 | 3. **Problem-Algorithm-Specific Solution Classes** 19 | - Extend a problem solution class and implements methods needed for an optimization algorithm to work 20 | - Example: `TSPHillClimbingSolution` inherits from `TSPSolution` and implements the `get_neighbors()` method. Available in `problems/tsp.py`. 21 | 22 | Implementations of the optimization algorithms are also available in the `algorithms` directory. 23 | 24 | ## Adding a New Optimization Problem 25 | 26 | To introduce a new optimization problem: 27 | 28 | 1. Create a **problem-specific solution class** that extends `Solution`. 29 | 2. This class should implement the `fitness()` and `random_initial_representation()` methods specific to the problem. 30 | 31 | ## Applying an Optimization Algorithm to a New Problem 32 | 33 | To optimize a problem using an algorithm: 34 | 35 | 1. Define a **problem-algorithm-specific solution class** that extends the **problem-specific class**. 36 | 2. Implement the required methods for the chosen optimization algorithm. 37 | 3. Instantiate an initial solution using this class and pass it as an argument to the optimization algorithm function. 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Computational Intelligence for Optimization 24/25 - NOVA IMS 2 | 3 | Welcome to the repository for the **Computational Intelligence for Optimization** course at NOVA IMS. This repository contains all the materials and resources related to the course, including weekly Jupyter notebooks and the optimization library that will be built throughout the semester. 4 | 5 | ## Repository Structure 6 | This repository is organized as follows: 7 | 8 | - /notebooks-class: This folder contains the Jupyter notebooks used in class each week 9 | - /notebooks-solution: This folder contains the same Jupyter notebooks used in class but with the cells filled with the solutions. YOU SHOULD NOT EDIT THESE NOTEBOOKS, and instead edit the ones in `notebooks-class` 10 | - /library: The source code for the optimization library we will build throughout the semester. This includes: 11 | - /algorithms: Functions and classes that implement a variety of optimization algorithms 12 | - /problems: Classes that define and implement various optimization problems and provide a way to solve them using the algorithms developed 13 | 14 | ## How to Use This Repository 15 | 16 | 1. **Make sure you have git installed**: Run `git --version` on a command shell. If `git` is not recognized, you need to install it. You can find the instructions [here](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). 17 | 18 | 2. **Clone the Repository**: Clone this repository to your local machine 19 | 20 | ```bash 21 | git clone https://github.com/inesmcm26/CIFO-24-25.git 22 | ``` 23 | 24 | 2. **Install a code editor (VSCode recommended)**: Download a code editor that has support for Jupyer notebooks. If you choose to use VSCode, install the `Jupyter` extension 25 | 26 | 3. **Setup**: Go to `/notebooks/P1-Setup-and-OOP.ipynb` and follow the setup steps. 27 | 28 | 4. **Update the Repository on Your Machine**: To fetch the latest updates from the repository, open a command prompt in the project folder and run `git pull`. You must do this in the beginning of each class or whenever your local copy is out of date. 29 | 30 | ## Getting Help 31 | If you have any questions or encounter issues, please send an email to imagessi@novaims.unl.pt. 32 | -------------------------------------------------------------------------------- /library/problems/int_bin.py: -------------------------------------------------------------------------------- 1 | """Problem definition 2 | 3 | Description: The IntBin problem consists of finding the integer 4 | with greatest number of 1's in its binary representation 5 | 6 | Search space: Integers from 1 to 15. 7 | 8 | Representation: Binary string representing the integer. 9 | 10 | Fitness function: f(x)= Number of 1's in binary representation of x 11 | 12 | Neighbors: Each binary representation of an integer x has as neighbors any other binary with a bit flipped. 13 | 14 | Goal: Maximize f(x). 15 | """ 16 | 17 | import random 18 | from copy import deepcopy 19 | 20 | from library.solution import Solution 21 | 22 | class IntBinSolution(Solution): 23 | """The IntBin Optimization problem aims at finding the integer number 24 | between 1 and 15 with the greatest number of 1's in its binary representation 25 | """ 26 | def __init__(self, repr=None): 27 | # Check if valid repr was passed 28 | if repr: 29 | if not isinstance(repr, str): 30 | raise ValueError("Representation must be a string") 31 | if (not len(repr) == 4) or (not set(repr).issubset({'0', '1'})): 32 | raise ValueError("IntBin solutions must be represented as binary strings of integers from 1 to 15") 33 | 34 | super().__init__(repr=repr) 35 | 36 | # Override the superclass's methods 37 | def random_initial_representation(self): 38 | # Generate random integer between 1 and 15 39 | random_n = random.randint(1, 15) 40 | # Transform it into its binary string representation with 4 digits 41 | return str(format(random_n, '04b')) 42 | 43 | def fitness(self): 44 | return self.repr.count('1') 45 | 46 | # Algorithm specific 47 | class IntBinHillClimbingSolution(IntBinSolution): 48 | def get_neighbors(self): 49 | # Convert binary string to list of bits 50 | # Strings are not mutable, that's why we are converting to list 51 | list_repr = list(self.repr) 52 | 53 | neighbors_repr = [] 54 | 55 | for digit_idx in range(len(list_repr)): 56 | neighbor_list_repr = deepcopy(list_repr) 57 | 58 | if list_repr[digit_idx] == "1": 59 | neighbor_list_repr[digit_idx] = "0" 60 | else: 61 | neighbor_list_repr[digit_idx] = "1" 62 | 63 | # Convert list back to string 64 | neighbor_repr = "".join(neighbor_list_repr) 65 | neighbors_repr.append(neighbor_repr) 66 | 67 | # Edge cases: Invalid neighbor '0000' 68 | if "0000" in neighbors_repr: 69 | neighbors_repr.remove("0000") 70 | 71 | # Create neighbors from binary representations 72 | neighbors = [] 73 | for neighbor_repr in neighbors_repr: 74 | neighbors.append(IntBinHillClimbingSolution(repr=neighbor_repr)) 75 | 76 | return neighbors 77 | 78 | -------------------------------------------------------------------------------- /library/algorithms/pso.py: -------------------------------------------------------------------------------- 1 | import random 2 | import numpy as np 3 | from copy import deepcopy 4 | from solution import PSOSolution 5 | 6 | def particle_swarm_optimization( 7 | population: list[PSOSolution], 8 | w=0.5, # inertia weight 9 | c1=1.5, # cognitive coefficient 10 | c2=1.5, # social coefficient 11 | maximization=False, 12 | max_iter=100, 13 | verbose=False 14 | ): 15 | # 1. Initialize particles and velocities 16 | # and 17 | # 2. Set personal bests 18 | # are already done in the initial population, as it consists of PSOSolutions 19 | 20 | # 3. Set global best 21 | if maximization: 22 | global_best = deepcopy(max(population, key=lambda p: p.fitness())) 23 | else: 24 | global_best = deepcopy(min(population, key=lambda p: p.fitness())) 25 | 26 | global_best_history = [] 27 | 28 | # 2. Repeat until termination condition 29 | for iter in range(max_iter): 30 | if verbose: 31 | print(f"Iteration: {iter+1}") 32 | # 2.1. For each particle 33 | for particle in population: 34 | if verbose: 35 | print(f"Initial particle location: {particle.repr}") 36 | 37 | # Update particle position 38 | new_position = particle.repr + particle.velocity 39 | 40 | # Update particle velocity 41 | r1 = np.array([random.random() for _ in range(len(particle.repr))]) 42 | r2 = np.array([random.random() for _ in range(len(particle.repr))]) 43 | 44 | inertia = w * particle.velocity 45 | cognitive = np.multiply(c1 * r1 , particle.best_repr - particle.repr) 46 | social = np.multiply(c2 * r2, global_best.repr - particle.repr) 47 | 48 | particle.velocity = inertia + cognitive + social 49 | 50 | particle.repr = new_position 51 | 52 | # Update personal best 53 | if particle.fitness() < particle.best_fitness: 54 | particle.best_repr = deepcopy(particle.repr) 55 | particle.best_fitness = particle.fitness() 56 | 57 | if verbose: 58 | print(f"New particle location: {particle.repr}") 59 | print(f"Updated velocity: {particle.velocity}") 60 | 61 | # Update global best 62 | if maximization: 63 | global_best_iter = deepcopy(max(population, key=lambda p: p.fitness())) 64 | if global_best_iter.fitness() > global_best.fitness(): 65 | global_best = global_best_iter 66 | else: 67 | global_best_iter = deepcopy(min(population, key=lambda p: p.fitness())) 68 | if global_best_iter.fitness() < global_best.fitness(): 69 | global_best = deepcopy(global_best_iter) 70 | 71 | global_best_history.append(global_best) 72 | 73 | if verbose: 74 | print(f"Best Fitness: {global_best.fitness()}") 75 | print("---------------------") 76 | 77 | return global_best, global_best_history 78 | -------------------------------------------------------------------------------- /library/algorithms/hill_climbing.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from library.solution import Solution 4 | 5 | def hill_climbing(initial_solution: Solution, maximization=False, max_iter=99999, verbose=False): 6 | """ 7 | Implementation of the Hill Climbing optimization algorithm. 8 | 9 | The algorithm iteratively explores the neighbors of the current solution, moving to a neighbor if it improves the objective function. 10 | The process continues until no improvement is found or the maximum number of iterations is reached. 11 | 12 | Args: 13 | initial_solution (Solution): The starting solution, which must implement the `fitness()` and `get_neighbors()` methods. 14 | maximization (bool, optional): If True, the algorithm maximizes the fitness function; otherwise, it minimizes it. Defaults to False. 15 | max_iter (int, optional): The maximum number of iterations allowed before stopping. Defaults to 99,999. 16 | verbose (bool, optional): If True, prints progress details during execution. Defaults to False. 17 | 18 | Returns: 19 | Solution: The best solution found during the search. 20 | 21 | Notes: 22 | - The initial_solution must implement a `fitness()` and `get_neighbors()` method. 23 | - The algorithm does not guarantee a global optimum; it only finds a local optimum. 24 | """ 25 | 26 | # Run some validations to make sure initial solution is well implemented 27 | run_validations(initial_solution) 28 | 29 | current = initial_solution 30 | improved = True 31 | iter = 1 32 | 33 | while improved: 34 | if verbose: 35 | print(f'Current solution: {current} with fitness {current.fitness()}') 36 | 37 | improved = False 38 | neighbors = current.get_neighbors() # Solution must have a get_neighbors() method 39 | 40 | for neighbor in neighbors: 41 | 42 | if verbose: 43 | print(f'Neighbor: {neighbor} with fitness {neighbor.fitness()}') 44 | 45 | if maximization and (neighbor.fitness() >= current.fitness()): 46 | current = deepcopy(neighbor) 47 | improved = True 48 | elif not maximization and (neighbor.fitness() <= current.fitness()): 49 | current = deepcopy(neighbor) 50 | improved = True 51 | 52 | iter += 1 53 | if iter == max_iter: 54 | break 55 | 56 | return current 57 | 58 | def run_validations(initial_solution): 59 | if not isinstance(initial_solution, Solution): 60 | raise TypeError("Initial solution must be an object of a class that inherits from Solution") 61 | if not hasattr(initial_solution, "get_neighbors"): 62 | print(f"The method 'get_neighbors' must be implemented in the initial solution.") 63 | neighbors = initial_solution.get_neighbors() 64 | if not isinstance(neighbors, list): 65 | raise TypeError("get_neighbors method must return a list") 66 | if not all([isinstance(neighbor, type(initial_solution)) for neighbor in neighbors]): 67 | raise TypeError(f"Neighbors must be of the same type as solution object: {type(initial_solution)}") -------------------------------------------------------------------------------- /library/problems/warehouse.py: -------------------------------------------------------------------------------- 1 | """Problem definition 2 | 3 | Description: The challenge of finding the optimal location for a warehouse that minimizes the total delivery cost to a set of customer locations. 4 | 5 | Search space: All possible warehouse locations 6 | 7 | Representation: An array with latitude and longitude 8 | 9 | Fitness function: f(x)= Distance between warehouse and customer locations weighted by the delivery cost to each customer 10 | 11 | Goal: Minimize f(x). 12 | """ 13 | 14 | import random 15 | import numpy as np 16 | 17 | from library.solution import PSOSolution 18 | 19 | class WarehousePSOSolution(PSOSolution): 20 | def __init__( 21 | self, 22 | customer_locations : list[list[float]], 23 | delivery_cost: list[float], 24 | repr=None, 25 | ): 26 | # Validate repr if it is passed as argument 27 | if repr: 28 | repr = self._validate_repr(repr) 29 | 30 | self.customer_locations = customer_locations 31 | self.delivery_cost = delivery_cost 32 | 33 | super().__init__(repr=repr) 34 | 35 | def _validate_repr(self, repr): 36 | if isinstance(repr, list) or isinstance(repr, tuple): 37 | repr = np.array(repr) 38 | else: 39 | raise ValueError("Representation must be either a list or a tuple") 40 | if len(repr) != 2: 41 | raise ValueError("Representation must contain only two values: latitude and longitude") 42 | if not all([isinstance(coordinate, float) for coordinate in repr]): 43 | raise ValueError("Coordinates should be float values") 44 | return repr 45 | 46 | def fitness(self): 47 | # Compute the Euclidean distance from the warehouse (self.repr) to each customer 48 | distances = np.linalg.norm(np.array(self.customer_locations) - self.repr, axis=1) 49 | 50 | # Total delivery cost is the dot product of distances and corresponding delivery costs 51 | return np.dot(np.array(self.delivery_cost), distances) 52 | 53 | def random_initial_representation(self): 54 | # Randomly initialize a warehouse location within the bounding box of customer locations 55 | lats, lons = zip(*self.customer_locations) # Unzips into two tuples 56 | # Get min and max bounds for each coordinate 57 | min_lat, max_lat = min(lats), max(lats) 58 | min_lon, max_lon = min(lons), max(lons) 59 | 60 | # Define custom range margins (alpha and beta) to control the range of random values 61 | # Since latitude and longitude are so different from each other, let's initilize the latitude between 62 | # a certain alpha_lat and beta_lat, and the longitude between a certain alpha_lon and beta_lon 63 | alpha_lat, beta_lat = random.uniform(min_lat, max_lat), random.uniform(min_lat, max_lat) 64 | alpha_lon, beta_lon = random.uniform(min_lon, max_lon), random.uniform(min_lon, max_lon) 65 | 66 | # Make sure alpha is smaller than beta 67 | if alpha_lat > beta_lat: 68 | alpha_lat, beta_lat = beta_lat, alpha_lat 69 | if alpha_lon > beta_lon: 70 | alpha_lon, beta_lon = beta_lon, alpha_lon 71 | 72 | # Randomly generate a latitude and longitude within the scaled bounds 73 | return np.array([random.uniform(alpha_lat, beta_lat), random.uniform(alpha_lon, beta_lon)]) -------------------------------------------------------------------------------- /library/algorithms/simulated_annealing.py: -------------------------------------------------------------------------------- 1 | import random 2 | import numpy as np 3 | from copy import deepcopy 4 | 5 | from library.solution import Solution 6 | 7 | from copy import deepcopy 8 | 9 | def simulated_annealing( 10 | initial_solution: Solution, 11 | C: float, 12 | L: int, 13 | H: float, 14 | maximization: bool = True, 15 | max_iter: int = 10, 16 | verbose: bool = False, 17 | ): 18 | """Implementation of the Simulated Annealing optimization algorithm. 19 | 20 | The algorithm iteratively explores the search space using a random neighbor of the 21 | current solution. If a better neighbor is found, the current solution is replaced by 22 | that neighbor. Otherwise, the solution may still be replaced by the neighbor with a certain 23 | probability. This probability decreases throughout the execution. The process continues until 24 | the maximum number of iterations is reached. 25 | 26 | The convergence speed of this algorithms depends on the initial value of control parameter C, 27 | he speed at which C is decreased (H), and the number of iterations in which the same C is 28 | maitained (L). 29 | 30 | 31 | Params: 32 | - initial_solution (SASolution): Initial solution to the optimization problem 33 | - C (float): Probability control parameter 34 | - L (int): Number of iterations with same C 35 | - H (float): Decreasing rate of C 36 | - maximization (bool): Is maximization problem? 37 | - max_iter (int): Maximum number of iterations 38 | - verbose (bool): If True, prints progress details during execution. Defaults to False. 39 | """ 40 | # 1. Initialize solution 41 | current_solution = initial_solution 42 | 43 | iter = 1 44 | 45 | if verbose: 46 | print(f'Initial solution: {current_solution.repr} with fitness {current_solution.fitness()}') 47 | 48 | # 2. Repeat until termination condition 49 | while iter <= max_iter: 50 | 51 | # 2.1 For L times 52 | for _ in range(L): 53 | # 2.1.1 Get random neighbor 54 | random_neighbor = current_solution.get_random_neighbor() 55 | 56 | neighbor_fitness = random_neighbor.fitness() 57 | current_fitness = current_solution.fitness() 58 | 59 | if verbose: 60 | print(f"Random neighbor {random_neighbor} with fitness: {neighbor_fitness}") 61 | 62 | # 2.1.2 Decide if neighbor is accepted as new solution 63 | # If neighbor is better, accept it 64 | if ( 65 | (maximization and (neighbor_fitness >= current_fitness)) 66 | or(not maximization and (neighbor_fitness <= current_fitness)) 67 | ): 68 | current_solution = deepcopy(random_neighbor) 69 | if verbose: 70 | print(f'Neighbor is better. Replaced current solution by neighbor.') 71 | 72 | # If neighbor is worse, accept it with a certain probability 73 | # Maximizaton: Neighbor is worse than current solution if fitness is lower 74 | # Minimization: Neighbor is worse than current solution if fitness is higher 75 | elif ( 76 | (maximization and (neighbor_fitness < current_fitness) 77 | or (not maximization and (neighbor_fitness > current_fitness))) 78 | ): 79 | # Generate random number between 0 and 1 80 | random_float = random.random() 81 | # Define probability P 82 | p = np.exp(-abs(current_fitness - neighbor_fitness) / C) 83 | if verbose: 84 | print(f'Probability of accepting worse neighbor: {p}') 85 | # The event happens with probability P if the random number if lower than P 86 | if random_float < p: 87 | current_solution = deepcopy(random_neighbor) 88 | if verbose: 89 | print(f'Neighbor is worse and was accepted.') 90 | else: 91 | if verbose: 92 | print("Neighbor is worse and was not accepted.") 93 | 94 | if verbose: 95 | print(f"New current solution {current_solution} with fitness {current_solution.fitness()}") 96 | 97 | # 2.2 Update C 98 | C = C / H 99 | if verbose: 100 | print(f'Decreased C. New value: {C}') 101 | print('--------------') 102 | 103 | iter += 1 104 | 105 | if verbose: 106 | print(f'Best solution found: {current_solution.repr} with fitness {current_solution.fitness()}') 107 | 108 | # 3. Return solution 109 | return current_solution 110 | -------------------------------------------------------------------------------- /library/algorithms/genetic_algorithms/algorithm.py: -------------------------------------------------------------------------------- 1 | import random 2 | from copy import deepcopy 3 | from library.solution import Solution 4 | from typing import Callable 5 | 6 | def get_best_ind(population: list[Solution], maximization: bool): 7 | fitness_list = [ind.fitness() for ind in population] 8 | if maximization: 9 | return population[fitness_list.index(max(fitness_list))] 10 | else: 11 | return population[fitness_list.index(min(fitness_list))] 12 | 13 | def genetic_algorithm( 14 | initial_population: list[Solution], 15 | max_gen: int, 16 | selection_algorithm: Callable, 17 | maximization: bool = False, 18 | xo_prob: float = 0.9, 19 | mut_prob: float = 0.2, 20 | elitism: bool = True, 21 | verbose: bool = False, 22 | ): 23 | """ 24 | Executes a genetic algorithm to optimize a population of solutions. 25 | 26 | Args: 27 | initial_population (list[Solution]): The starting population of solutions. 28 | max_gen (int): The maximum number of generations to evolve. 29 | selection_algorithm (Callable): Function used for selecting individuals. 30 | maximization (bool, optional): If True, maximizes the fitness function; otherwise, minimizes. Defaults to False. 31 | xo_prob (float, optional): Probability of applying crossover. Defaults to 0.9. 32 | mut_prob (float, optional): Probability of applying mutation. Defaults to 0.2. 33 | elitism (bool, optional): If True, carries the best individual to the next generation. Defaults to True. 34 | verbose (bool, optional): If True, prints detailed logs for debugging. Defaults to False. 35 | 36 | Returns: 37 | Solution: The best solution found on the last population after evolving for max_gen generations. 38 | list[float]: The fitness of the best individual over the generations 39 | """ 40 | best_fitness_over_gens = [] 41 | 42 | # 1. Initialize a population with N individuals 43 | population = initial_population 44 | 45 | # 2. Repeat until termination condition 46 | for gen in range(1, max_gen + 1): 47 | if verbose: 48 | print(f'-------------- Generation: {gen} --------------') 49 | 50 | # 2.1. Create an empty population P' 51 | new_population = [] 52 | 53 | # 2.2. If using elitism, insert best individual from P into P' 54 | if elitism: 55 | new_population.append(deepcopy(get_best_ind(population, maximization))) 56 | 57 | # 2.3. Repeat until P' contains N individuals 58 | while len(new_population) < len(population): 59 | # 2.3.1. Choose 2 individuals from P using a selection algorithm 60 | first_ind = selection_algorithm(population, maximization) 61 | second_ind = selection_algorithm(population, maximization) 62 | 63 | if verbose: 64 | print(f'Selected individuals:\n{first_ind}\n{second_ind}') 65 | 66 | # 2.3.2. Choose an operator between crossover and replication 67 | # 2.3.3. Apply the operator to generate the offspring 68 | if random.random() < xo_prob: 69 | offspring1, offspring2 = first_ind.crossover(second_ind) 70 | if verbose: 71 | print(f'Applied crossover') 72 | else: 73 | offspring1, offspring2 = deepcopy(first_ind), deepcopy(second_ind) 74 | if verbose: 75 | print(f'Applied replication') 76 | 77 | if verbose: 78 | print(f'Offspring:\n{offspring1}\n{offspring2}') 79 | 80 | # 2.3.4. Apply mutation to the offspring 81 | first_new_ind = offspring1.mutation(mut_prob) 82 | # 2.3.5. Insert the mutated individuals into P' 83 | new_population.append(first_new_ind) 84 | 85 | if verbose: 86 | print(f'First mutated individual: {first_new_ind}') 87 | 88 | if len(new_population) < len(population): 89 | second_new_ind = offspring2.mutation(mut_prob) 90 | new_population.append(second_new_ind) 91 | if verbose: 92 | print(f'Second mutated individual: {first_new_ind}') 93 | 94 | # 2.4. Replace P with P' 95 | population = new_population 96 | 97 | if verbose: 98 | print(f'Final best individual in generation: {get_best_ind(population, maximization).fitness()}') 99 | 100 | best_ind = get_best_ind(population, maximization) 101 | best_fitness_over_gens.append(best_ind.fitness()) 102 | 103 | # 3. Return the best individual in P + the best individual fitness over generations 104 | return get_best_ind(population, maximization), best_fitness_over_gens 105 | -------------------------------------------------------------------------------- /library/algorithms/genetic_algorithms/mutation.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | import random 3 | 4 | def binary_standard_mutation(representation: str | list, mut_prob): 5 | """ 6 | Applies standard binary mutation to a binary string or list representation. 7 | 8 | This function supports both binary strings (e.g., "10101") and binary lists 9 | (e.g., [1, 0, 1, 0, 1]) containing either string characters ("0", "1") or 10 | integers (0, 1). Each gene in the representation is independently flipped 11 | with a given mutation probability, while preserving the original data type 12 | of the genes. 13 | 14 | The function preserves the type of the input representation: if the input is 15 | a string, the output will also be a string; if it's a list, the output will 16 | remain a list. 17 | 18 | Parameters: 19 | representation (str or list): The binary representation to mutate. 20 | mut_prob (float): The probability of flipping each gene. 21 | 22 | Returns: 23 | str or list: A new mutated representation of the same type as the input. 24 | 25 | Raises: 26 | ValueError: If the input contains elements other than 0, 1, "0", or "1". 27 | """ 28 | 29 | # Initialize new representation as a copy of current representation 30 | new_representation = deepcopy(representation) 31 | 32 | if random.random() <= mut_prob: 33 | # Strings are not mutable. Let's convert temporarily to a list 34 | if isinstance(representation, str): 35 | new_representation = list(new_representation) 36 | 37 | for char_ix, char in enumerate(representation): 38 | if char == "1": 39 | new_representation[char_ix] = "0" 40 | elif char == 1: 41 | new_representation[char_ix] = 0 42 | elif char == "0": 43 | new_representation[char_ix] = "1" 44 | elif char == 0: 45 | new_representation[char_ix] = 1 46 | else: 47 | raise ValueError(f"Invalid character {char}. Can not apply binary standard mutation") 48 | 49 | # If representation was a string, convert list back to string 50 | if isinstance(representation, str): 51 | new_representation = "".join(new_representation) 52 | 53 | return new_representation 54 | 55 | 56 | def swap_mutation(representation, mut_prob): 57 | """ 58 | Applies swap mutation to a solution representation with a given probability. 59 | 60 | Swap mutation randomly selects two different positions (genes) in the 61 | representation and swaps their values. This operator is commonly used for 62 | permutation-based representations but works for any list or string. 63 | 64 | The function preserves the type of the input representation: if the input is 65 | a string, the output will also be a string; if it's a list, the output will 66 | remain a list. 67 | 68 | Parameters: 69 | representation (str or list): The solution to mutate. 70 | mut_prob (float): The probability of performing the swap mutation. 71 | 72 | Returns: 73 | str or list: A new solution with two genes swapped, of the same type as the input. 74 | """ 75 | 76 | new_representation = deepcopy(representation) 77 | 78 | if random.random() <= mut_prob: 79 | # Strings are not mutable. Let's convert temporarily to a list 80 | if isinstance(representation, str): 81 | new_representation = list(new_representation) 82 | 83 | first_idx = random.randint(0, len(representation) - 1) 84 | 85 | # To guarantee we select two different positions 86 | second_idx = first_idx 87 | while second_idx == first_idx: 88 | second_idx = random.randint(0, len(representation) - 1) 89 | 90 | new_representation[first_idx] = representation[second_idx] 91 | new_representation[second_idx] = representation[first_idx] 92 | 93 | # If representation was a string, convert list back to string 94 | if isinstance(representation, str): 95 | new_representation = "".join(new_representation) 96 | 97 | return new_representation 98 | 99 | 100 | def inversion_mutation(representation: str | list, mut_prob): 101 | """ 102 | Applies inversion mutation to a representation. 103 | 104 | Inversion mutation selects two random indices and reverses the 105 | subsequence between them, with a certain probability. 106 | 107 | Parameters: 108 | ---------- 109 | representation : str or list 110 | The individual to mutate. Should represent a valid permutation. 111 | mut_prob : float 112 | Probability of applying the mutation (between 0 and 1). 113 | 114 | Returns: 115 | ------- 116 | str or list 117 | A new individual with the mutated representation (if mutation occurs), 118 | or a copy of the original. 119 | """ 120 | if random.random() <= mut_prob: 121 | # Select two distinct indices 122 | first_idx = random.randint(0, len(representation)-1) 123 | second_idx = first_idx 124 | # We want to get two indexes that are at least 2 genes away 125 | while abs(second_idx-first_idx) <= 1: 126 | second_idx = random.randint(0, len(representation)-1) 127 | 128 | # Ensure first_idx < second_idx 129 | if first_idx > second_idx: 130 | first_idx, second_idx = second_idx, first_idx 131 | 132 | # Reverse between first and second index 133 | reversed_subsequence = list(reversed(representation[first_idx:second_idx])) 134 | 135 | # Convert back to string if original representation is a string 136 | if isinstance(representation, str): 137 | reversed_subsequence = "".join(reversed_subsequence) 138 | 139 | # Keep everything from second index (excluding it) until the end 140 | new_representation = representation[:first_idx] + reversed_subsequence + representation[second_idx:] 141 | return new_representation 142 | else: 143 | return deepcopy(representation) -------------------------------------------------------------------------------- /library/algorithms/genetic_algorithms/crossover.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | def standard_crossover(parent1_repr, parent2_repr): 4 | """ 5 | Performs standard one-point crossover on two parent representations. 6 | 7 | This operator selects a random crossover point (not at the edges) and 8 | exchanges the tail segments of the two parents to produce two offspring. 9 | The crossover point is the same for both parents and ensures at least one 10 | gene is inherited from each parent before and after the point. 11 | 12 | Parameters: 13 | parent1_repr (str or list): The first parent representation. 14 | parent2_repr (str or list): The second parent representation. 15 | Both parents must have the same length and type. 16 | 17 | Returns: 18 | tuple: A pair of offspring representations (offspring1, offspring2), 19 | of the same type as the parents. 20 | 21 | Raises: 22 | ValueError: If parent representations are not the same length. 23 | """ 24 | 25 | if not (isinstance(parent1_repr, list) or isinstance(parent1_repr, str)): 26 | raise ValueError("Parent 1 representation must be a list or a string") 27 | if not (isinstance(parent2_repr, list) or isinstance(parent2_repr, str)): 28 | raise ValueError("Parent 1 representation must be a list or a string") 29 | if len(parent1_repr) != len(parent2_repr): 30 | raise ValueError("Parent 1 and Parent 2 representations must be the same length") 31 | 32 | # Choose random crossover point 33 | xo_point = random.randint(1, len(parent1_repr) - 1) 34 | 35 | offspring1_repr = parent1_repr[:xo_point] + parent2_repr[xo_point:] 36 | offspring2_repr = parent2_repr[:xo_point] + parent1_repr[xo_point:] 37 | 38 | return offspring1_repr, offspring2_repr 39 | 40 | def cycle_crossover(parent1_repr: str | list, parent2_repr: str | list): 41 | """ 42 | Performs Cycle Crossover (CX) between two parents 43 | 44 | Cycle Crossover preserves the position of elements by identifying a cycle 45 | of indices where the values from each parent will be inherited by each offspring. 46 | The remaining indices are filled with values from the other parent, maintaining valid permutations. 47 | 48 | Args: 49 | parent1_repr (str or list): The first parent representation. 50 | parent2_repr (str or list): The second parent representation. 51 | Both parents must have the same length and type. 52 | 53 | Returns: 54 | tuple: Two offspring permutations resulting from the crossover. 55 | """ 56 | # Randomly choose a starting index for the cycle 57 | initial_random_idx = random.randint(0, len(parent1_repr)-1) 58 | 59 | # Initialize the cycle with the starting index 60 | cycle_idxs = [initial_random_idx] 61 | current_cycle_idx = initial_random_idx 62 | 63 | # Traverse the cycle by following the mapping from parent2 to parent1 64 | while True: 65 | value_parent2 = parent2_repr[current_cycle_idx] 66 | # Find where this value is in parent1 to get the next index in the cycle 67 | next_cycle_idx = parent1_repr.index(value_parent2) 68 | 69 | # Closed the cycle -> Break 70 | if next_cycle_idx == initial_random_idx: 71 | break 72 | 73 | cycle_idxs.append(next_cycle_idx) 74 | current_cycle_idx = next_cycle_idx 75 | 76 | offspring1_repr = [] 77 | offspring2_repr = [] 78 | for idx in range(len(parent1_repr)): 79 | if idx in cycle_idxs: 80 | # Keep values from parent1 in offspring1 in the cycle indexes 81 | offspring1_repr.append(parent1_repr[idx]) 82 | # Keep values from parent2 in offspring2 in the cycle indexes 83 | offspring2_repr.append(parent2_repr[idx]) 84 | else: 85 | # Swap elements from parents in non-cycle indexes 86 | offspring1_repr.append(parent2_repr[idx]) 87 | offspring2_repr.append(parent1_repr[idx]) 88 | 89 | # To keep the same type as the parents representation 90 | if isinstance(parent1_repr, str) and isinstance(parent2_repr, str): 91 | offspring1_repr = "".join(offspring1_repr) 92 | offspring2_repr = "".join(offspring2_repr) 93 | 94 | return offspring1_repr, offspring2_repr 95 | 96 | 97 | import random 98 | 99 | def swap_crossover(parent1_repr: str | list, parent2_repr: str | list): 100 | """ 101 | A minimal crossover operator for permutation-based genetic algorithms. 102 | 103 | This crossover creates a child by copying one parent, and then: 104 | - Randomly selects 1 position. 105 | - At the selected position `i`, looks at the value from the other parent. 106 | - It finds where that value currently exists in the child. 107 | - Then it swaps the value at position `i` with that found value, 108 | ensuring the result remains a valid permutation (no duplicates). 109 | 110 | The process is run twice, first starting from parent1 and then parent2, 111 | producing two offspring. 112 | 113 | Args: 114 | parent1_repr (str or list): The first parent representation. 115 | parent2_repr (str or list): The second parent representation. 116 | Both parents must have the same length and type. 117 | 118 | Returns: 119 | tuple: Two offspring permutations resulting from the crossover. 120 | """ 121 | size = len(parent1_repr) 122 | offspring1_repr = parent1_repr.copy() 123 | offspring2_repr = parent1_repr.copy() 124 | 125 | def swap(parent1_repr, parent2_repr): 126 | offspring_repr = parent1_repr.copy() 127 | # Randomly choose 1 position to perform a swap 128 | swap_positions = random.sample(range(size), 1) 129 | 130 | for pos in swap_positions: 131 | val_from_p2 = parent2_repr[pos] # Get the value from the second parent 132 | index_in_child = offspring_repr.index(val_from_p2) # Where is that value in the child? 133 | 134 | # Swap positions to bring val_from_p2 into pos 135 | offspring_repr[pos], offspring_repr[index_in_child] = offspring_repr[index_in_child], offspring_repr[pos] 136 | 137 | return offspring_repr 138 | 139 | offspring1_repr = swap(parent1_repr=parent1_repr, parent2_repr=parent2_repr) 140 | offspring2_repr = swap(parent1_repr=parent2_repr, parent2_repr=parent1_repr) 141 | 142 | return offspring1_repr, offspring2_repr 143 | -------------------------------------------------------------------------------- /library/problems/tsp.py: -------------------------------------------------------------------------------- 1 | """Problem definition 2 | 3 | Description: The Traveling Salesperson Problem (TSP) is the challenge of finding the shortest possible route that starts in a given city, visits each of the remaining N-1 cities exactly once, and returns to the starting city. 4 | 5 | Search space: All possible permutations of city visit orders, forming valid round-trip routes. 6 | 7 | Representation: List of city indexes that compose the route 8 | 9 | Fitness function: f(x)= Total distance traveled, computed as the sum of distances between consecutive cities in the route. 10 | 11 | Neighbors: A neighbor solution is obtained by swapping the positions of two consecutive cities in the route. 12 | 13 | Goal: Minimize f(x). 14 | """ 15 | import random 16 | from copy import deepcopy 17 | 18 | from library.solution import Solution 19 | 20 | from library.problems.data.tsp_data import distance_matrix 21 | 22 | class TSPSolution(Solution): 23 | """The Travel Salesperson Problem (TSP) aims at finding the shortest possible route that starts and 24 | ends in a given city and visits all other cities once.""" 25 | def __init__( 26 | self, 27 | repr = None, 28 | distance_matrix: list[list[float]] = distance_matrix, 29 | starting_idx: int = 0 30 | ): 31 | self.starting_idx = starting_idx 32 | self.distance_matrix = distance_matrix 33 | 34 | # Validate repr if it is passed as argument 35 | if repr: 36 | self._validate_repr(repr) 37 | 38 | super().__init__(repr=repr) 39 | 40 | def _validate_repr(self, repr): 41 | # Confirm repr is list 42 | if not isinstance(repr, list): 43 | raise TypeError('Representation must be a list') 44 | # Make sure repr is a list of integers 45 | elif not all([isinstance(idx, int) for idx in repr]): 46 | raise TypeError('Representation must be a list of integers (indexes of the route)') 47 | # Validate start and end route points 48 | if (repr[0] != self.starting_idx) or (repr[-1] != self.starting_idx): 49 | raise ValueError("TSP route must start and end in the starting index") 50 | # Validate route length and content 51 | if (len(repr) != (len(distance_matrix) + 1)) or (set(repr) != set([i for i in range(len(distance_matrix))])): 52 | raise ValueError("TSP route must pass through all cities") 53 | 54 | def random_initial_representation(self): 55 | # Route starts in starting idx 56 | route = [self.starting_idx] 57 | # Get city idx to visit and shuffle them 58 | idx_to_visit = [idx for idx in range(len(self.distance_matrix)) if idx != self.starting_idx] 59 | random.shuffle(idx_to_visit) 60 | # Add idx to visit to route 61 | route = route + idx_to_visit 62 | # Route ends in starting idx 63 | route = route + [self.starting_idx] 64 | return route 65 | 66 | def fitness(self): 67 | total_distance = 0 68 | for i in range(len(self.repr)-1): 69 | total_distance += self.distance_matrix[self.repr[i]][self.repr[i+1]] 70 | return total_distance 71 | 72 | 73 | class TSPHillClimbingSolution(TSPSolution): 74 | def get_neighbors(self): 75 | """Neighbors are obtained by swaping the positions of two consecutive cities""" 76 | neighbors = [] 77 | for i in range(1, len(self.repr)-2): 78 | new_route = deepcopy(self.repr) 79 | new_route[i], new_route[i+1] = new_route[i+1], new_route[i] 80 | neighbor = TSPHillClimbingSolution(repr=new_route, distance_matrix=self.distance_matrix) 81 | neighbors.append(neighbor) 82 | 83 | return neighbors 84 | 85 | class TSPSASolution(TSPSolution): 86 | def get_random_neighbor(self): 87 | """Random neighbor is obtained by swaping the positions of two random consecutive cities""" 88 | nr_cities = len(self.distance_matrix) 89 | 90 | # Choose a city idx to switch with the next city 91 | random_city_idx = random.randint(1, nr_cities-3) 92 | 93 | new_route = deepcopy(self.repr) 94 | new_route[random_city_idx] = self.repr[random_city_idx+1] 95 | new_route[random_city_idx+1] = self.repr[random_city_idx] 96 | 97 | return TSPSASolution(repr=new_route, distance_matrix=self.distance_matrix, starting_idx=self.starting_idx) 98 | 99 | class TSPGASolution(TSPSolution): 100 | def __init__( 101 | self, 102 | distance_matrix, 103 | starting_idx, 104 | mutation_function, # Callable 105 | crossover_function, # Callable 106 | repr = None, 107 | ): 108 | # Save as attributes for access in methods 109 | self.mutation_function = mutation_function 110 | self.crossover_function = crossover_function 111 | 112 | super().__init__( 113 | distance_matrix=distance_matrix, 114 | starting_idx=starting_idx, 115 | repr=repr, 116 | ) 117 | 118 | def mutation(self, mut_prob): 119 | """ 120 | Applies the provided mutation operator to the middle portion 121 | of the route (excluding start and end cities). 122 | """ 123 | # Apply mutation to the middle route segment 124 | middle_segment = self.repr[1:-1] # Exclude starting/ending city 125 | mutated_segment = self.mutation_function(middle_segment, mut_prob) 126 | new_repr = [self.starting_idx] + mutated_segment + [self.starting_idx] 127 | 128 | return TSPGASolution( 129 | distance_matrix=self.distance_matrix, 130 | starting_idx=self.starting_idx, 131 | mutation_function=self.mutation_function, 132 | crossover_function=self.crossover_function, 133 | repr=new_repr 134 | ) 135 | 136 | def crossover(self, other_solution): 137 | """ 138 | Applies the provided crossover operator to the middle portions 139 | of two parent routes (excluding start/end cities), and returns 140 | two new offspring solutions. 141 | """ 142 | # Apply crossover to the middle route segment of the parents 143 | parent1_middle = self.repr[1:-1] 144 | parent2_middle = other_solution.repr[1:-1] 145 | 146 | offspring1_middle, offspring2_middle = self.crossover_function(parent1_middle, parent2_middle) 147 | 148 | offspring1_repr = [self.starting_idx] + offspring1_middle + [self.starting_idx] 149 | offspring2_repr = [self.starting_idx] + offspring2_middle + [self.starting_idx] 150 | 151 | return ( 152 | TSPGASolution( 153 | distance_matrix=self.distance_matrix, 154 | starting_idx=self.starting_idx, 155 | mutation_function=self.mutation_function, 156 | crossover_function=self.crossover_function, 157 | repr=offspring1_repr 158 | ), 159 | TSPGASolution( 160 | distance_matrix=self.distance_matrix, 161 | starting_idx=self.starting_idx, 162 | mutation_function=self.mutation_function, 163 | crossover_function=self.crossover_function, 164 | repr=offspring2_repr 165 | ) 166 | ) -------------------------------------------------------------------------------- /notebooks-class/P6-GeneticAlgorithms-Part1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 3, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import random\n", 20 | "from copy import deepcopy\n", 21 | "from library.solution import Solution" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "## Genetic Algorithms\n", 29 | "\n", 30 | "Genetic Algorithms (GAs) are a class of optimization algorithms inspired by **natural selection** and **evolutionary principles**. They are used to find near-optimal solutions to complex problems, especially when traditional methods struggle due to high-dimensional or non-differentiable search spaces.\n", 31 | "\n", 32 | "GAs operate by evolving a population of candidate solutions over multiple iterations (called generations), using biologically inspired operations:\n", 33 | "- **Selection**: Choosing the best individuals based on a fitness function.\n", 34 | "- **Crossover (Recombination)**: Combining two parent solutions to create new offspring.\n", 35 | "- **Mutation**: Introducing small random changes to maintain diversity." 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "### Pseudo-code\n", 43 | "\n", 44 | "1. Initialize a population P of **N** individuals/solutions (usually at random)\n", 45 | "2. Repeat until termination condition (**max number of generations**):\n", 46 | " 1. Create an empty population P'\n", 47 | " 2. If using elitism, insert the best individual from P into P'\n", 48 | " 3. Repeat until P' contains N individuals:\n", 49 | " 1. Choose 2 individuals from population P using a **selection algorithm**\n", 50 | " 2. Choose an operator between crossover and replication with probabilities **$P_c$** and $1-P_c$, respectively\n", 51 | " 3. Apply the operator to the individuals to generate the offspring\n", 52 | " 4. Apply mutation to the offspring. The mutation operator has an hyperparameter **$P_m$** (we'll see what this means for different mutation operators later)\n", 53 | " 5. Insert the mutated individuals into P'\n", 54 | " 4. Replace P with P'\n", 55 | "3. Return the best individual in P\n" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "### Algorithm Implementation\n", 63 | "\n", 64 | "Let's implement the genetic algorithm function. These are the arguments this function will receive:\n", 65 | "- `initial_population`: List of individuals (randomly generated solutions)\n", 66 | "- `max_gen`: Maximum number of generations\n", 67 | "- `selection_algorithm`: A function that receives a population, selects one individual based on fitness and returns it\n", 68 | "- `maximization`: Boolean that indicates if we're solving a maximization or minimization problem\n", 69 | "- `xo_prob`: Probability of crossover (usually big)\n", 70 | "- `mut_prob`: Probability of mutation (usually small)\n", 71 | "- `elistism`: A boolean that indicates if elitism should be used or not" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "# TODO: Implement Genetic Algorithm function" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "**NOTE:** There are many variations of genetic algorithms. The implementation used in our practical classes and the library follows some choices. For example, before inserting the second mutated individual into P', we check whether it would exceed the population size. This can happen with even-sized populations since we always insert two individuals at a time. An alternative approach would be to insert the individual regardless and, if the population exceeds the limit, remove the worst-performing individual at the end.\n", 88 | "\n", 89 | "There are also other assumptions for our implementation of the algorithm to run.\n", 90 | "- individuals have `fitness`, `crossover` and `mutation` methods\n", 91 | "- `crossover` always returns two offspring\n", 92 | "- both `crossover` and `mutation` methods return new individuals instead of modifying individuals in-place" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "### Selection algorithms\n", 100 | "\n", 101 | "Selection is the first main step of a genetic algorithm. Selection algorithms have the following properties:\n", 102 | "- are probabilistic\n", 103 | "- for any pair of individuals A and B, if A if better than B, then the probability of selecting A must be bigger than the probability of selecting B\n", 104 | "- all individuals must have the chance of being selected, even the worst in the population\n", 105 | "- when an individual is selected, it remains in population P and a copy is inserted in P'\n", 106 | "\n", 107 | "In class we'll implement **Fitness Proportionate Selection** (or roulette wheel), but there are other techniques like Ranking or Tournament selection.\n", 108 | "\n", 109 | "#### Fitness Proportionate Selection\n", 110 | "\n", 111 | "Probabilistic selection method used in GAs to choose individuals for reproduction. It mimics a roulette wheel, where individuals with higher fitness have a greater chance of being selected, but lower-fitness individuals still have some probability of selection.\n", 112 | "\n", 113 | "Let $N$ be the number of individuals in population $P$ and $F = {f_1, f_2, ..., f_N}$ be the set of fitness values of the indiiduals in the population. For an individual $i$ in the population, the probability of selecting $i$ is:\n", 114 | "\n", 115 | "$$P(selecting\\ i) = \\frac{f_i}{\\sum_{j=1}^{N} f_j}$$\n", 116 | "\n", 117 | "![Fitness Proportionate Selection Implementation](images/fps.png)\n", 118 | "\n", 119 | "Our implementation fo this selection algorithm will be a function that receives two arguments:\n", 120 | "- `population`: A list of individuals / solutions. These must have a `fitness()` method.\n", 121 | "- `maximization`: Boolean that indicates if we're solving a maximization or minimization problem" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "# TODO: Implement Fitness Proportionate Selection function" 131 | ] 132 | } 133 | ], 134 | "metadata": { 135 | "kernelspec": { 136 | "display_name": "venv", 137 | "language": "python", 138 | "name": "python3" 139 | }, 140 | "language_info": { 141 | "codemirror_mode": { 142 | "name": "ipython", 143 | "version": 3 144 | }, 145 | "file_extension": ".py", 146 | "mimetype": "text/x-python", 147 | "name": "python", 148 | "nbconvert_exporter": "python", 149 | "pygments_lexer": "ipython3", 150 | "version": "3.13.2" 151 | } 152 | }, 153 | "nbformat": 4, 154 | "nbformat_minor": 2 155 | } 156 | -------------------------------------------------------------------------------- /library/problems/ks.py: -------------------------------------------------------------------------------- 1 | """Problem definition 2 | 3 | Description: The Knapsack Problem involves selecting a subset of N items, each with a given value and weight, to pack into a container with a fixed capacity. If the total weight of selected items exceeds the capacity, the solution is invalid. The goal is to maximize the total value of items while ensuring they fit within the container's constraints. 4 | 5 | Search space: All possible subsets of items that can be placed in the knapsack. 6 | 7 | Representation: Binary string of length N (number of items), where 1 indicates the item is included in the knapsack and 0 indicates the item is excluded. 8 | 9 | Fitness function: f(x)= Total value inside the knapsack. If the total size exceeds the knapsack's capacity, the solution is invalid and assigned a fitness of -inf. 10 | 11 | Neighbors: A neighbor solution is obtained by flipping a single bit, meaning adding one item to the knapsack, or removing one item from the knapsack. 12 | 13 | Goal: Maximize f(x). 14 | """ 15 | 16 | import random 17 | from copy import deepcopy 18 | 19 | from library.solution import Solution 20 | 21 | from library.problems.data.ks_data import values, weights, capacity 22 | 23 | class KSSolution(Solution): 24 | """The Knapsack problem aims at finding the best way to pack items in a container 25 | maximizing the container value while not exceeding the capacity""" 26 | def __init__( 27 | self, 28 | values: list[float] = values, 29 | weights: list[float] = weights, 30 | capacity: float = capacity, 31 | repr: str = None, 32 | ): 33 | self.values = values 34 | self.weights = weights 35 | self.capacity = capacity 36 | 37 | if repr: 38 | repr = self._validate_repr(repr) 39 | 40 | super().__init__(repr=repr) 41 | 42 | def _validate_repr(self, repr): 43 | # If repr is given as string, convert to list 44 | if isinstance(repr, str): 45 | repr = [int(bit) for bit in repr] 46 | if not isinstance(repr, list): 47 | raise TypeError("Representation must be string or list") 48 | # All list elements should be integers 49 | if not all([isinstance(bit, int) for bit in repr]): 50 | repr = [int(bit) for bit in repr] 51 | # Validate representation length and content 52 | if (len(repr) != len(self.values)) or (not set(repr).issubset({0, 1})): 53 | raise ValueError("Representation must be a binary string/list with as many values as objects") 54 | return repr 55 | 56 | def random_initial_representation(self): 57 | repr = [] 58 | for _ in range(len(self.values)): 59 | repr.append(random.choice([0, 1])) 60 | return repr 61 | 62 | def total_weight(self): 63 | total = 0 64 | for idx, bin_value in enumerate(self.repr): 65 | if bin_value == 1: 66 | total += self.weights[idx] 67 | return total 68 | 69 | def total_value(self): 70 | total = 0 71 | for idx, bin_value in enumerate(self.repr): 72 | if bin_value == 1: 73 | total += self.values[idx] 74 | return total 75 | 76 | def fitness(self): 77 | total_weight = self.total_weight() 78 | 79 | if total_weight > self.capacity: 80 | return -9999999999999999 81 | 82 | return self.total_value() 83 | 84 | class KSHillClimbingSolution(KSSolution): 85 | def get_neighbors(self): 86 | """Neighbors are obtained by flipping a bit in the representation. This means 87 | adding or removing one item from the container. One neighbor is generated for 88 | each bit flip.""" 89 | neighbors = [] 90 | 91 | for idx, bin_value in enumerate(self.repr): 92 | neighbor_repr = deepcopy(self.repr) 93 | if bin_value == 1: 94 | neighbor_repr[idx] = 0 95 | else: 96 | neighbor_repr[idx] = 1 97 | 98 | neighbor = KSHillClimbingSolution( 99 | repr=neighbor_repr, 100 | values=self.values, 101 | weights=self.weights, 102 | capacity=self.capacity, 103 | ) 104 | neighbors.append(neighbor) 105 | 106 | return neighbors 107 | 108 | class KSSASolution(KSSolution): 109 | def get_random_neighbor(self): 110 | """A random neighbor is obtained by flipping a random bit in the representation. 111 | This means adding or removing one item from the container""" 112 | neighbor_repr = deepcopy(self.repr) 113 | # Get random index 114 | random_idx = random.randint(0, len(self.values)-1) 115 | # Bit flip 116 | if neighbor_repr[random_idx] == 1: 117 | neighbor_repr[random_idx] = 0 118 | else: 119 | neighbor_repr[random_idx] = 1 120 | 121 | return KSSASolution( 122 | repr=neighbor_repr, 123 | values=self.values, 124 | weights=self.weights, 125 | capacity=self.capacity, 126 | ) 127 | 128 | class KSGASolution(KSSolution): 129 | def __init__( 130 | self, 131 | values, 132 | weights, 133 | capacity, 134 | mutation_function, # Callable 135 | crossover_function, # Callable 136 | repr = None 137 | ): 138 | super().__init__( 139 | values=values, 140 | weights=weights, 141 | capacity=capacity, 142 | repr=repr, 143 | ) 144 | 145 | # Save as attributes for access in methods 146 | self.mutation_function = mutation_function 147 | self.crossover_function = crossover_function 148 | 149 | 150 | def mutation(self, mut_prob): 151 | # Apply mutation function to representation 152 | new_repr = self.mutation_function(self.repr, mut_prob) 153 | # Create and return individual with mutated representation 154 | return KSGASolution( 155 | values=self.values, 156 | weights=self.weights, 157 | capacity=self.capacity, 158 | mutation_function=self.mutation_function, 159 | crossover_function=self.crossover_function, 160 | repr=new_repr 161 | ) 162 | 163 | def crossover(self, other_solution): 164 | # Apply crossover function to self representation and other solution representation 165 | offspring1_repr, offspring2_repr = self.crossover_function(self.repr, other_solution.repr) 166 | 167 | # Create and return offspring with new representations 168 | return ( 169 | KSGASolution( 170 | values=self.values, 171 | weights=self.weights, 172 | capacity=self.capacity, 173 | mutation_function=self.mutation_function, 174 | crossover_function=self.crossover_function, 175 | repr=offspring1_repr 176 | ), 177 | KSGASolution( 178 | values=self.values, 179 | weights=self.weights, 180 | capacity=self.capacity, 181 | mutation_function=self.mutation_function, 182 | crossover_function=self.crossover_function, 183 | repr=offspring2_repr 184 | ) 185 | ) -------------------------------------------------------------------------------- /notebooks-class/P5-SimulatedAnnealling-TSP-KS.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import random\n", 20 | "import numpy as np\n", 21 | "from library.solution import Solution\n", 22 | "from library.problems.tsp import TSPSolution\n", 23 | "from library.problems.ks import KSSolution" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "## Simulated Annealing\n", 31 | "\n", 32 | "Simulated Annealing is an optimization algorithm that explores solutions by allowing both improvements and occasional worse moves to escape local optima. The probability of accepting worse solutions decreases over time, controlled by a temperature parameter that gradually cools. This balance between exploration and exploitation helps the algorithm find a global optimum rather than getting stuck in suboptimal solutions." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "### Pseudo-code\n", 40 | "\n", 41 | "1. Define the current solution (usually at random)\n", 42 | "2. Repeat until termination condition (usually nr of iterations):\n", 43 | " 1. Repeat **L** times:\n", 44 | " 1. Choose a random neighbor of the current solution\n", 45 | " 2. If random neighbor is better than current solution, replace current solution by neighbor. Otherwise, accept the nieghbor as the current solution with probability: $$exp(-\\frac{neighbor.fitness - current.fitness}{C})$$\n", 46 | " 2. Decrement **C** by dividing it by **H**\n", 47 | "3. Return current solution" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "### Algorithm Implementation\n", 55 | "\n", 56 | "Let's implement the simmulated annealing algorithm using python. The function that implements the algorithm should receive the following arguments:\n", 57 | "- `initial_solution`: Initial current solution\n", 58 | "- `C`: Control parameter\n", 59 | "- `L`: Number of iterations with same C\n", 60 | "- `H`: Decreasing rate of parameter C\n", 61 | "- `maximization`: boolean that indicates if we're solving a maximization or minimization problem\n", 62 | "- `max_iter`: maximum number of interations." 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "# TODO: Implement simulated annealing algorithm" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "Notice that we assume that a solution has the following methods:\n", 79 | "- `fitness()`\n", 80 | "- `get_random_neighbor()`\n", 81 | "\n", 82 | "Additionally, `get_random_neighbor()` must return a solution that also implements these methods." 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "### Solving TSP with Simulated Annealing" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "To solve TSP with simulated annealing we need to define a `TSPSASolution` class that inherits from `TSPSolution` and implements the `get_random_neighbor()` method.\n", 97 | "\n", 98 | "In the previous notebook, we implemented `TSPSolution`, which provides the `fitness()` and `random_initial_value()` methods. We also created `TSPHillClimbingSolution`, which extends `TSPSolution` and implements `get_neighbors()`.\n", 99 | "\n", 100 | "Simulated Annealing requires selecting only random neighbor rather than generating all neighbors. Therefore, we can create a new class `TSPSASolution`, that implements the method that is required for simulated annealing to work: `get_random_neighbor()`.\n", 101 | "\n", 102 | "We could do this two ways:\n", 103 | "- Inherit from `TSPHillClimbingSolution` and use the `get_neighbors()` method inside the `get_random_neighbor()` method to first get all neighbors, and then radomly select one\n", 104 | "- Inherit from `TSPSolution` and implement only the `get_random_neighbor()`\n", 105 | "\n", 106 | "Let's go with the second one to keep the code as independent, eficient and modular as possible.\n", 107 | "\n", 108 | "![TSP Solutions](images/tsp-solutions.png)" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "A neighbor of a TSP solution can be obtained by swapping two consecutive cities on the route (excluding the starting and end points)." 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "# TODO: Implement TSPSASolution" 125 | ] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "metadata": {}, 130 | "source": [ 131 | "Let's test it" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "solution = TSPSASolution()\n", 141 | "\n", 142 | "print('Solution', solution)\n", 143 | "print('Random neighbor', solution.get_random_neighbor())" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "And now we can apply the simulated annealing algorithm by giving it an random initial solution" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "# TODO: Apply simulated annealing to TSP" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": {}, 165 | "source": [ 166 | "The implementation of `TSPSASolution` can be found in `library/problems/tsp.py`" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "### Solving KS with Simulated Annealing\n", 174 | "\n", 175 | "To solve Knapsack with simulated annealing we need to define a `KSSASolution` class that inherits from `KSSolution` and implements the `get_random_neighbor()` method.\n", 176 | "\n", 177 | "In the previous notebook, we implemented `KSSolution`, which provides the `fitness()` and `random_initial_value()` methods. We also created `KSHillClimbingSolution`, which extends `KSSolution` and implements `get_neighbors()`.\n", 178 | "\n", 179 | "Since Simulated Annealing requires selecting a random neighbor rather than generating all neighbors, we can create a new class, `KSSASolution`, that implements the `get_random_neighbor()` method.\n", 180 | "\n", 181 | "Similarly to what we just did for TSP, let's implement the `KSSASolution` that inherits from `TSPSolution` and implements the `get_random_neighbor()`.\n", 182 | "\n", 183 | "A neighbor of a KS solution can be obtained by randomly flipping a bit, meaning, adding or removing an item from the knapsack." 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": null, 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "# TODO: Implement KSSASolution (short for KnapSack Simulated Annealing Solution)" 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "metadata": {}, 198 | "source": [ 199 | "Let's test it" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": null, 205 | "metadata": {}, 206 | "outputs": [], 207 | "source": [ 208 | "solution = KSSASolution()\n", 209 | "\n", 210 | "print('Solution', solution)\n", 211 | "print('Random neighbor', solution.get_random_neighbor())" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": {}, 217 | "source": [ 218 | "And now we can apply the simulated annealing algorithm by giving it an random initial solution" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "# TODO: Apply simulated annealing to Knapsack" 228 | ] 229 | } 230 | ], 231 | "metadata": { 232 | "kernelspec": { 233 | "display_name": "venv", 234 | "language": "python", 235 | "name": "python3" 236 | }, 237 | "language_info": { 238 | "codemirror_mode": { 239 | "name": "ipython", 240 | "version": 3 241 | }, 242 | "file_extension": ".py", 243 | "mimetype": "text/x-python", 244 | "name": "python", 245 | "nbconvert_exporter": "python", 246 | "pygments_lexer": "ipython3", 247 | "version": "3.13.2" 248 | } 249 | }, 250 | "nbformat": 4, 251 | "nbformat_minor": 2 252 | } 253 | -------------------------------------------------------------------------------- /notebooks-class/P8-Genetic-Algorithms-Part3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from copy import deepcopy\n", 20 | "import random\n", 21 | "from library.problems.tsp import TSPSolution\n", 22 | "from library.problems.data.tsp_data import distance_matrix\n", 23 | "from library.algorithms.genetic_algorithms.algorithm import genetic_algorithm\n", 24 | "from library.algorithms.genetic_algorithms.mutation import swap_mutation\n", 25 | "from library.algorithms.genetic_algorithms.selection import fitness_proportionate_selection" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## Specialized Genetic Operators" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "In the previous notebook, we explored some traditional genetic operators: **standard crossover, binary mutation, and swap mutation**.\n", 40 | "\n", 41 | "However, these traditional operators generate invalid solutions when applied to combinatorial problems such as the Traveling Salesman Problem (TSP), job scheduling, or vehicle routing, where solutions are represented as **permutations**.\n", 42 | "\n", 43 | "In this notebook, we’ll explore **specialized genetic operators**, specifically designed to handle permutations without producing invalid solutions. We’ll explore one crossover and one mutation methods that respects permutation constraints." 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "### Why Standard Crossover/Mutation Fail for Permutation Problems\n", 51 | "\n", 52 | "In permutation-based problems (e.g., [1, 2, 3, 4, 5]), each gene must appear exactly once.\n", 53 | "Standard genetic operators like one-point crossover or value-flip mutation can break this rule, resulting in invalid offspring with duplicates or missing values.\n", 54 | "\n", 55 | "\n", 56 | "#### Standard Mutation - Value Flip\n", 57 | "\n", 58 | "This mutation is inspired by the standard binary mutation: each gene is randomly replaced with another value with some probability.\n", 59 | "\n", 60 | "Individual [1, 2, 3, 4, 5]\n", 61 | "\n", 62 | "Mutated individual (hypothetical): [1, 2, 4, 1, 5] ❌ (Duplicate '1')\n", 63 | "\n", 64 | "#### Standard Crossover\n", 65 | "\n", 66 | "Parent 1: [1, 2 | 3, 4, 5]\n", 67 | "\n", 68 | "Parent 2: [3, 4 | 5, 1, 2]\n", 69 | "\n", 70 | "\n", 71 | "Offspring 1 (invalid): [1, 2, 5, 1, 2] ❌ (Duplicates '1' and '2')\n", 72 | "\n", 73 | "Offspring 2 (invalid): [3, 4, 3, 4, 5] ❌ (Duplicates '3' and '4')" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "### Cycle Crossover\n", 81 | "\n", 82 | "Cycle Crossover keeps items in their original positions across parents by identifying cycles of indices where elements should remain fixed.\n", 83 | "\n", 84 | "**Pseudo-code:**\n", 85 | "\n", 86 | "1. Choose random index in Parent 1 and copy the element to first child.\n", 87 | "3. Copy element in same index in Parent 2 to second child.\n", 88 | "4. Find this element in Parent 1 and copy it to first child, and repeat the process.\n", 89 | "5. Once the cycle completes (we end up back on the initial index), the remaining positions are filled from the other parent.\n", 90 | "\n", 91 | "![Cycle Crossover](images/cycle-xo.png)\n" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "def cycle_crossover(parent1_repr: str | list, parent2_repr: str | list):\n", 101 | " # TODO\n", 102 | " pass" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "Let's test it." 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": null, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "parent1 = [1, 2, 3, 4, 5, 6, 7]\n", 119 | "parent2 = [3, 4, 2, 1, 7, 5, 6]\n", 120 | "\n", 121 | "print(\"Parent 1:\", parent1)\n", 122 | "print(\"Parent 2:\", parent2)\n", 123 | "\n", 124 | "offspring1_repr, offspring2_repr = cycle_crossover(parent1_repr=parent1, parent2_repr=parent2)\n", 125 | "\n", 126 | "print(\"Offspring 1:\", offspring1_repr)\n", 127 | "print(\"Offspring 2:\", offspring2_repr)" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "### Inversion Mutation\n", 135 | "\n", 136 | "Inversion mutation works by selecting two random indices and reversing the subsequence between them, with a certain probability.\n", 137 | "\n", 138 | "![Inversion Mutation](images/inversion-mutation.png)" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": null, 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "def inversion_mutation(representation: str | list, mut_prob):\n", 148 | " # TODO\n", 149 | " pass" 150 | ] 151 | }, 152 | { 153 | "cell_type": "markdown", 154 | "metadata": {}, 155 | "source": [ 156 | "Let's test it." 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": null, 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [ 165 | "representation = [1, 2, 3, 4, 5, 6, 7]\n", 166 | "\n", 167 | "inversion_mutation(representation, mut_prob=1)" 168 | ] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "metadata": {}, 173 | "source": [ 174 | "## Solving TSP with Genetic Algorithms\n", 175 | "\n", 176 | "Just like we did in the previous notebook for the Knapsack Problem, we’ll now solve TSP using genetic algorithms.\n", 177 | "\n", 178 | "To structure our solution, we’ll define a `TSPGASolution` class where we’ll define in the crossover and mutation methods needed to run the `genetic_algorithm` function.\n", 179 | "\n", 180 | "To keep the class flexible and reusable, we’ll pass the `mutation_function` and `crossover_function` as callable arguments when creating an instance of `TSPGASolution`.\n", 181 | "\n", 182 | "### A small but important side note\n", 183 | "\n", 184 | "TSP solutions are represented as permutations of city indices, where the path must start and end at the same city (i.e., the starting index is fixed at both ends).\n", 185 | "\n", 186 | "When applying permutation-based operators like cycle crossover or inversion mutation, we need to preserve this constraint. That means we should only apply genetic operators to the middle portion of the route: excluding the first and last cities, which must remain the same.\n", 187 | "\n", 188 | "So in practice, our crossover and mutation functions will only operate on the inner part of the individual, keeping the boundaries intact." 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "class TSPGASolution(TSPSolution):\n", 198 | " # TODO\n", 199 | " pass" 200 | ] 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "metadata": {}, 205 | "source": [ 206 | "Let's apply the genetic algorithm to TSP using cycle crossover and inversion mutation. Here the probability of mutation should be relatively small because inversion mutation may be very destructive." 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": null, 212 | "metadata": {}, 213 | "outputs": [], 214 | "source": [ 215 | "# TODO: Apply GA to TSP with cycle crossover and inversion mutation" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "And now let's use cycle crossover again, but this time we use swap mutation.\n", 223 | "Now we can set a higher probability of miutation because swap mutation is less destructive than inversion mutation." 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "# TODO: Apply GA to TSP with cycle crossover and swap mutation" 233 | ] 234 | } 235 | ], 236 | "metadata": { 237 | "kernelspec": { 238 | "display_name": "venv", 239 | "language": "python", 240 | "name": "python3" 241 | }, 242 | "language_info": { 243 | "codemirror_mode": { 244 | "name": "ipython", 245 | "version": 3 246 | }, 247 | "file_extension": ".py", 248 | "mimetype": "text/x-python", 249 | "name": "python", 250 | "nbconvert_exporter": "python", 251 | "pygments_lexer": "ipython3", 252 | "version": "3.13.3" 253 | } 254 | }, 255 | "nbformat": 4, 256 | "nbformat_minor": 2 257 | } 258 | -------------------------------------------------------------------------------- /notebooks-class/P4-HillClimbing-TSP-and-KS.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from copy import deepcopy\n", 20 | "from random import shuffle, choice\n", 21 | "from library.solution import Solution\n", 22 | "from library.algorithms.hill_climbing import hill_climbing" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "In previous notebooks, we defined the `Solution` class as an **abstract class** with methods that must be implemented by subclasses, depending on the specific optimization problem. While the implementation of solutions depend on the problem, all solutions share common principles: they require a **representation**, a **fitness function**, and a method for **random initialization**.\n", 30 | "\n", 31 | "By extending this class, we can define solution classes specific to different optimization problems. For example, we created the `IntBinSolution` class to represent solutions for the IntBin optimization problem.\n", 32 | "\n", 33 | "We then applied the Hill Climbing algorithm to the IntBin problem by further extending `IntBinSolution` to implement the `get_neighbors()` method, which is essential for Hill Climbing algorithm. To do this, we created a new class, `IntBinHillClimbingSolution`.\n", 34 | "\n", 35 | "Today, we'll use Hill Climbing to solve two new problems: the Traveling Salesperson Problem (TSP) and the Knapsack Problem (KS)." 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "## Traveling Salesperson Problem\n", 43 | "\n", 44 | "**Description:** The Traveling Salesperson Problem (TSP) is the challenge of finding the shortest possible route that starts in a given city, visits each of the remaining N-1 cities exactly once, and returns to the starting city.\n", 45 | "\n", 46 | "**Search space:** All possible permutations of city visit orders, forming valid round-trip routes.\n", 47 | "\n", 48 | "**Representation:** List of city indexes that compose the route\n", 49 | "\n", 50 | "**Fitness function:** f(x) = Total distance traveled, computed as the sum of distances between consecutive cities in the route.\n", 51 | "\n", 52 | "**Goal:** Minimize f(x)." 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "Let's begin by implementing the `TSPSolution` class, which inherits from `Solution`. As a result, it must implement the `fitness()` and `random_initial_representation()` methods.\n", 60 | "\n", 61 | "This class represents a solution to the Traveling Salesperson Problem (TSP) and does not include any implementation related to the optimization algorithm that will be used to solve it.\n", 62 | "\n", 63 | "![TSP Solution](images/tsp-solution.png)" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "from library.problems.data.tsp_data import distance_matrix\n", 73 | "\n", 74 | "#TODO: Implement TSPSolution class" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "Let's test it" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "solution = TSPSolution()\n", 91 | "\n", 92 | "print('Random solution:', solution)\n", 93 | "print('Fitness:', solution.fitness())" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "### Solving TSP with Hill Climbing\n", 101 | "\n", 102 | "To use Hill Climbing to solve TSP we need to define a `TSPHillClimbingSolution` class that implements the `get_neighbors()` method. We also need to ensure that this function returns a list of solutions that also implement the `get_neighbors()` method, therefore, return a list of solutions of type `TSPHillClimbingSolution` too.\n", 103 | "\n", 104 | "A TSP neighbor solution is obtained by swapping the positions of two consecutive cities in the route.\n", 105 | "\n", 106 | "![TSP Hill Climbing Solution](images/tsp-hillclimbing-solution.png)" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "#TODO: Implement TSPSHillClimbingSolution class" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "Let's test it" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "solution = TSPHillClimbingSolution()\n", 132 | "print('Solution:', solution)\n", 133 | "\n", 134 | "neighbors = solution.get_neighbors()\n", 135 | "print('Neihghbors:')\n", 136 | "for neighbor in neighbors:\n", 137 | " print(neighbor)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "And now we can apply the hill climbing algorithm by passing it a random initial solution." 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "#TODO: Apply hill climbing to TSP" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "The implementations of `TSPSolution` and `TSPHillClimbingSolution` can be found in `library/problems/tsp.py`" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "## Knapsack Problem\n", 168 | "\n", 169 | "**Description:** The Knapsack Problem involves selecting a subset of N items, each with a given value and weight, to pack into a container with a fixed capacity. If the total weight of selected items exceeds the capacity, the solution is invalid. The goal is to maximize the total value of items while ensuring they fit within the container's constraints.\n", 170 | "\n", 171 | "**Search space:** All possible subsets of items that can be placed in the knapsack.\n", 172 | "\n", 173 | "**Representation:** Binary string of length N (number of items), where 1 indicates the item is included in the knapsack and 0 indicates the item is excluded.\n", 174 | "\n", 175 | "**Fitness function:** f(x)= Total value inside the knapsack. If the total size exceeds the knapsack's capacity, the solution is invalid and assigned a fitness of -inf.\n", 176 | "\n", 177 | "**Goal:** Maximize f(x)." 178 | ] 179 | }, 180 | { 181 | "cell_type": "markdown", 182 | "metadata": {}, 183 | "source": [ 184 | "Similarly to what we've done for TSP, let's begin by implementing the `KSSolution` class, which inherits from `Solution` and implementes the `fitness()` and `random_initial_representation()` methods.\n", 185 | "\n", 186 | "This class represents a solution to the Knapsack problem (KS) and does not include any implementation related to the optimization algorithm that will be used to solve it." 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "metadata": {}, 193 | "outputs": [], 194 | "source": [ 195 | "from library.problems.data.ks_data import values, weights, capacity\n", 196 | "\n", 197 | "#TODO: Implement KSSolution class" 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "metadata": {}, 203 | "source": [ 204 | "Let's test it" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "solution = KSSolution()\n", 214 | "\n", 215 | "print(solution)\n", 216 | "print(solution.fitness())" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "### Solving KS with Hill Climbing\n", 224 | "\n", 225 | "A neighbor solution is obtained by flipping a single bit, meaning adding one item to the knapsack, or removing one item from the knapsack.\n", 226 | "\n", 227 | "Let's create the `KSHillClimbingSolution` that inherits from `KSSolution` and implements the `get_neighbors` method.\n" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": null, 233 | "metadata": {}, 234 | "outputs": [], 235 | "source": [ 236 | "#TODO: Implement KSHillClimbingSolution class" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": {}, 242 | "source": [ 243 | "Let's test it" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "solution = KSHillClimbingSolution()\n", 253 | "print('Solution:', solution)\n", 254 | "\n", 255 | "neighbors = solution.get_neighbors()\n", 256 | "print('Neihghbors:')\n", 257 | "for neighbor in neighbors:\n", 258 | " print(neighbor)" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "metadata": {}, 264 | "source": [ 265 | "And now we can apply the hill climbing algorithm by passing it a random initial solution." 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": null, 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [ 274 | "#TODO: Apply hill climbing to KS problem" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "The implementations of `KSSolution` and `KSHillClimbingSolution` can be found in `library/problems/knapsack.py`" 282 | ] 283 | } 284 | ], 285 | "metadata": { 286 | "kernelspec": { 287 | "display_name": "venv", 288 | "language": "python", 289 | "name": "python3" 290 | }, 291 | "language_info": { 292 | "codemirror_mode": { 293 | "name": "ipython", 294 | "version": 3 295 | }, 296 | "file_extension": ".py", 297 | "mimetype": "text/x-python", 298 | "name": "python", 299 | "nbconvert_exporter": "python", 300 | "pygments_lexer": "ipython3", 301 | "version": "3.13.2" 302 | } 303 | }, 304 | "nbformat": 4, 305 | "nbformat_minor": 2 306 | } 307 | -------------------------------------------------------------------------------- /notebooks-class/P10-PSO.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "!python3 -m pip install folium\n", 10 | "# or\n", 11 | "!python -m pip install folium" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import sys\n", 21 | "sys.path.append('..')" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import random\n", 31 | "import folium\n", 32 | "import numpy as np\n", 33 | "from copy import deepcopy\n", 34 | "\n", 35 | "from library.solution import Solution\n", 36 | "from library.problems.data.warehouse_data import customer_locations, delivery_cost" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "## Particle Swarm Optimization (PSO)\n", 44 | "\n", 45 | "PSO is a population-based optimization algorithm inspired by the collective behavior of bird flocks, unlike Genetic Algorithms (GAs), which draw from evolutionary theory. In PSO, particles (potential solutions) move through the search space, adjusting their positions based on their own experience and that of the whole swarm. This social interaction guides the swarm toward optimal solutions. Because its inspiration comes from social behavior rather than evolution, PSO is not typically classified under Evolutionary Computation.\n", 46 | "\n", 47 | "It is used to optimize continuous optimization problems where individuals can be represented $m$-dimensional arrays." 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "### Terminology\n", 55 | "\n", 56 | "- **Individuals (particles):** \n", 57 | " Each particle is an $m$-dimensional vector of real numbers: $\\mathbf{x}_i = (x_{i1}, x_{i2}, \\dots, x_{im}) \\in \\mathbb{R}^m$\n", 58 | "\n", 59 | "- **Population (swarm):** \n", 60 | " A set of $n$ particles: $\\{\\mathbf{x}_1, \\mathbf{x}_2, \\dots, \\mathbf{x}_n\\}$\n", 61 | "\n", 62 | " Where: \n", 63 | " - $x_i$ is the $i$-th particle of the swarm ($i = 1, \\dots, n$) \n", 64 | " - $x_{ij}$ is the $j$-th component of particle $i$ ($j = 1, \\dots, m$)\n", 65 | "\n", 66 | "- **Velocities:** \n", 67 | " Each particle has an associated velocity vector: $\\mathbf{v}_i = (v_{i1}, v_{i2}, \\dots, v_{im}) \\in \\mathbb{R}^m$\n", 68 | "\n", 69 | "- **Local best (personal best):** \n", 70 | " The best position ever visited by particle $i$: $\\mathbf{b}_i \\in \\mathbb{R}^m$\n", 71 | "\n", 72 | "- **Global best:** \n", 73 | " The best position found by any particle in the swarm: $\\mathbf{g} \\in \\mathbb{R}^m$" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "### Pseudo-code\n", 81 | "\n", 82 | "1. **Initialize** particles $\\mathbf{x}_i$ and velocities $\\mathbf{v}_i$ for each particle $i = 1, \\dots, n$:\n", 83 | " - Each $\\mathbf{x}_i \\in \\mathbb{R}^m$ is initialized randomly, with each component $\\mathbf{x}_{ij}$ drawn uniformly from the interval $[\\boldsymbol{\\alpha}_i, \\boldsymbol{\\beta}_i]$ for $j = 1, \\dots, m$.\n", 84 | " - $\\mathbf{v}_i$ is typically initialized as the zero vector.\n", 85 | "\n", 86 | "2. **Set personal bests**: \n", 87 | " $\\mathbf{b}_i \\leftarrow \\mathbf{x}_i$ for all $i = 1, \\dots, n$\n", 88 | "\n", 89 | "3. **Set global best**: \n", 90 | " $\\mathbf{g} \\leftarrow \\arg\\min_{\\mathbf{x}_i} f(\\mathbf{x}_i)$ (or $\\arg\\max$, depending on optimization goal)\n", 91 | "\n", 92 | "4. **Repeat until termination condition is met**:\n", 93 | " - For each particle $i = 1, \\dots, n$:\n", 94 | " 1. **Update position**: \n", 95 | " $\\mathbf{x}_i \\leftarrow \\mathbf{x}_i + \\mathbf{v}_i$\n", 96 | " 2. **Update velocity**: \n", 97 | " $\\mathbf{v}_i \\leftarrow w * \\mathbf{v}_i + c_1 * r_1 \\cdot (\\mathbf{b}_i - \\mathbf{x}_i) + c_2 * r_2 \\cdot (\\mathbf{g} - \\mathbf{x}_i)$ \n", 98 | " where:\n", 99 | " - $w$ is the inertia weight \n", 100 | " - $c_1$, $c_2$ are acceleration coefficients \n", 101 | " - $\\mathbf{r}_1, \\mathbf{r}_2 \\in \\mathbb{R}^m$ are random vectors with each component drawn from $[0, 1]$\n", 102 | " - $\\cdot$ denotes element-wise (Hadamard) product\n", 103 | " 3. **Update personal best**: \n", 104 | " If $f(\\mathbf{x}_i) < f(\\mathbf{b}_i)$, then $\\mathbf{b}_i \\leftarrow \\mathbf{x}_i$\n", 105 | " 4. **Update global best**: \n", 106 | " If $f(\\mathbf{x}_i) < f(\\mathbf{g})$, then $\\mathbf{g} \\leftarrow \\mathbf{x}_i$\n", 107 | "\n", 108 | "5. **Return global best** $\\mathbf{g}$\n", 109 | "\n", 110 | "\n", 111 | "#### High-level intuition\n", 112 | "\n", 113 | "Each particle represents a point in an $m$-dimensional space and has a velocity vector that dictates its movement. At every iteration, a particle updates its position based on its current velocity, and its velocity is updated according to three main influences:\n", 114 | "\n", 115 | "$\\mathbf{v}_i \\leftarrow w \\cdot \\mathbf{v}_i + c_1 * r_1 \\cdot (\\mathbf{b}_i - \\mathbf{x}_i) + c_2 * r_2 \\cdot (\\mathbf{g} - \\mathbf{x}_i)$ \n", 116 | "\n", 117 | "\n", 118 | "Intuition Behind Each Term:\n", 119 | "- $w * \\mathbf{v}_i$: Keeps some momentum from the previous direction.\n", 120 | "- $c_1 * \\mathbf{r}_1 \\cdot (\\mathbf{b}_i - \\mathbf{x}_i)$: Pulls the particle toward its own best-known position (individual memory).\n", 121 | "- $c_2 * \\mathbf{r}_2 \\cdot (\\mathbf{g} - \\mathbf{x}_i)$: Pulls the particle toward the best position found by the entire swarm (collective wisdom).\n", 122 | "\n", 123 | "This balance between exploration (inertia and randomness) and exploitation (personal and social bests) drives the swarm to converge toward optimal or near-optimal solutions over time.\n", 124 | "\n", 125 | "![image.png](images/pso.png)" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "### Algorithm\n", 133 | "\n", 134 | "First let's define the `PSOSolution` class, which extends the `Solution` class by introducing additional attributes during initialization. This is still an abstract class, so every class that inherits from this one (problem-specific classes) will have to implement the `fitness` and `random_initial_representation` methods." 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "class PSOSolution(Solution):\n", 144 | " # TODO\n", 145 | " pass" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "Now let's implement the algorithm." 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "# TODO: Implement the PSO algorithm" 162 | ] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "metadata": {}, 167 | "source": [ 168 | "## Warehouse Location Optimization Problem\n", 169 | "\n", 170 | "**Goal:** Find the optimal location for a warehouse that minimizes the total delivery cost to a set of customer locations. \n", 171 | "\n", 172 | "\n", 173 | "Let:\n", 174 | "\n", 175 | "- $\\mathbf{x} = [x, y]$: coordinates of the warehouse (decision variables)\n", 176 | "- $(x_i, y_i)$ for $i=1, \\ldots, n$: coordinates of $n$ customer locations\n", 177 | "- $c_i$: delivery cost weight for customer $i$\n", 178 | "\n", 179 | "We define the cost function as:\n", 180 | "\n", 181 | "$f(\\mathbf{x}) = \\sum_{i=1}^n c_i \\cdot \\sqrt{(x - x_i)^2 + (y - y_i)^2}$\n", 182 | "\n", 183 | "The goal is to minimize the cost function.\n", 184 | "\n", 185 | "Let's start by defining the `WarehousePSOSolution` class.\n" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "class WarehousePSOSolution(PSOSolution):\n", 195 | " # TODO\n", 196 | " pass" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "Let's test it" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": null, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "solution = WarehousePSOSolution(customer_locations=customer_locations, delivery_cost=delivery_cost)\n", 213 | "\n", 214 | "print(f\"Solution: {solution} with fitness {solution.fitness()}\")" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": {}, 220 | "source": [ 221 | "### Solving the Warehouse Location Problem using PSO" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": null, 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [ 230 | "# TODO: Run PSO to solve the warehouse location problem" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "Now, let’s visualize the customer locations, the final warehouse position, and all the global best solutions identified throughout the search process." 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": null, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "lats, lons = zip(*customer_locations)\n", 247 | "# Center of the map\n", 248 | "map_center = [np.mean(lats), np.mean(lons)]\n", 249 | "\n", 250 | "# Create the map\n", 251 | "my_map = folium.Map(location=map_center, zoom_start=12)\n", 252 | "\n", 253 | "# Add customer markers\n", 254 | "for coord in customer_locations:\n", 255 | " folium.Marker(location=coord).add_to(my_map)\n", 256 | "\n", 257 | "# Add markers for each step in the evolution (except the last)\n", 258 | "for idx, global_best in enumerate(global_best_history[:-1]):\n", 259 | " if (idx == 0) or (np.any(global_best.repr != global_best_history[idx-1].repr)):\n", 260 | " folium.Marker(\n", 261 | " location=global_best.repr,\n", 262 | " popup=f'Warehouse Position #{idx + 1}',\n", 263 | " tooltip=f'Step {idx + 1}',\n", 264 | " icon=folium.Icon(color=\"gray\", icon=\"building\")\n", 265 | " ).add_to(my_map)\n", 266 | "\n", 267 | "# Add the final warehouse position with the building icon\n", 268 | "final_position = global_best_history[-1].repr\n", 269 | "folium.Marker(\n", 270 | " location=final_position,\n", 271 | " popup='Final Warehouse Location',\n", 272 | " tooltip='Final Position',\n", 273 | " icon=folium.Icon(color='red', icon='building', prefix=\"fa\")\n", 274 | ").add_to(my_map)\n", 275 | "\n", 276 | "my_map" 277 | ] 278 | } 279 | ], 280 | "metadata": { 281 | "kernelspec": { 282 | "display_name": "venv", 283 | "language": "python", 284 | "name": "python3" 285 | }, 286 | "language_info": { 287 | "codemirror_mode": { 288 | "name": "ipython", 289 | "version": 3 290 | }, 291 | "file_extension": ".py", 292 | "mimetype": "text/x-python", 293 | "name": "python", 294 | "nbconvert_exporter": "python", 295 | "pygments_lexer": "ipython3", 296 | "version": "3.13.3" 297 | } 298 | }, 299 | "nbformat": 4, 300 | "nbformat_minor": 2 301 | } 302 | -------------------------------------------------------------------------------- /notebooks-solution/P6-GeneticAlgorithms-Part1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 3, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import random\n", 20 | "from copy import deepcopy\n", 21 | "from library.solution import Solution" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "## Genetic Algorithms\n", 29 | "\n", 30 | "Genetic Algorithms (GAs) are a class of optimization algorithms inspired by **natural selection** and **evolutionary principles**. They are used to find near-optimal solutions to complex problems, especially when traditional methods struggle due to high-dimensional or non-differentiable search spaces.\n", 31 | "\n", 32 | "GAs operate by evolving a population of candidate solutions over multiple iterations (called generations), using biologically inspired operations:\n", 33 | "- **Selection**: Choosing the best individuals based on a fitness function.\n", 34 | "- **Crossover (Recombination)**: Combining two parent solutions to create new offspring.\n", 35 | "- **Mutation**: Introducing small random changes to maintain diversity." 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "### Pseudo-code\n", 43 | "\n", 44 | "1. Initialize a population P of **N** individuals/solutions (usually at random)\n", 45 | "2. Repeat until termination condition (**max number of generations**):\n", 46 | " 1. Create an empty population P'\n", 47 | " 2. If using elitism, insert the best individual from P into P'\n", 48 | " 3. Repeat until P' contains N individuals:\n", 49 | " 1. Choose 2 individuals from population P using a **selection algorithm**\n", 50 | " 2. Choose an operator between crossover and replication with probabilities **$P_c$** and $1-P_c$, respectively\n", 51 | " 3. Apply the operator to the individuals to generate the offspring\n", 52 | " 4. Apply mutation to the offspring. The mutation operator has an hyperparameter **$P_m$** (we'll see what this means for different mutation operators later)\n", 53 | " 5. Insert the mutated individuals into P'\n", 54 | " 4. Replace P with P'\n", 55 | "3. Return the best individual in P\n" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "### Algorithm Implementation\n", 63 | "\n", 64 | "Let's implement the genetic algorithm function. These are the arguments this function will receive:\n", 65 | "- `initial_population`: List of individuals (randomly generated solutions)\n", 66 | "- `max_gen`: Maximum number of generations\n", 67 | "- `selection_algorithm`: A function that receives a population, selects one individual based on fitness and returns it\n", 68 | "- `maximization`: Boolean that indicates if we're solving a maximization or minimization problem\n", 69 | "- `xo_prob`: Probability of crossover (usually big)\n", 70 | "- `mut_prob`: Probability of mutation (usually small)\n", 71 | "- `elistism`: A boolean that indicates if elitism should be used or not" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "from typing import Callable\n", 81 | "\n", 82 | "def get_best_ind(population: list[Solution], maximization: bool):\n", 83 | " fitness_list = [ind.fitness() for ind in population]\n", 84 | " if maximization:\n", 85 | " return population[fitness_list.index(max(fitness_list))]\n", 86 | " else:\n", 87 | " return population[fitness_list.index(min(fitness_list))]\n", 88 | "\n", 89 | "def genetic_algorithm(\n", 90 | " initial_population: list[Solution],\n", 91 | " max_gen: int,\n", 92 | " selection_algorithm: Callable,\n", 93 | " maximization: bool = False,\n", 94 | " xo_prob: float = 0.9,\n", 95 | " mut_prob: float = 0.1,\n", 96 | " elitism: bool = True,\n", 97 | " verbose: bool = False,\n", 98 | "):\n", 99 | " # 1. Initialize a population with N individuals\n", 100 | " population = initial_population\n", 101 | "\n", 102 | " # 2. Repeat until termination condition\n", 103 | " for gen in range(1, max_gen + 1):\n", 104 | " if verbose:\n", 105 | " print(f'-------------- Generation: {gen} --------------')\n", 106 | "\n", 107 | " # 2.1. Create an empty population P'\n", 108 | " new_population = []\n", 109 | "\n", 110 | " # 2.2. If using elitism, insert best individual from P into P'\n", 111 | " if elitism:\n", 112 | " new_population.append(deepcopy(get_best_ind(initial_population, maximization)))\n", 113 | " \n", 114 | " # 2.3. Repeat until P' contains N individuals\n", 115 | " while len(new_population) < len(population):\n", 116 | " # 2.3.1. Choose 2 individuals from P using a selection algorithm\n", 117 | " first_ind = selection_algorithm(population, maximization)\n", 118 | " second_ind = selection_algorithm(population, maximization)\n", 119 | "\n", 120 | " if verbose:\n", 121 | " print(f'Selected individuals:\\n{first_ind}\\n{second_ind}')\n", 122 | "\n", 123 | " # 2.3.2. Choose an operator between crossover and replication\n", 124 | " # 2.3.3. Apply the operator to generate the offspring\n", 125 | " if random.random() < xo_prob:\n", 126 | " offspring1, offspring2 = first_ind.crossover(second_ind)\n", 127 | " if verbose:\n", 128 | " print(f'Applied crossover')\n", 129 | " else:\n", 130 | " offspring1, offspring2 = deepcopy(first_ind), deepcopy(second_ind)\n", 131 | " if verbose:\n", 132 | " print(f'Applied replication')\n", 133 | " \n", 134 | " if verbose:\n", 135 | " print(f'Offspring:\\n{offspring1}\\n{offspring2}')\n", 136 | " \n", 137 | " # 2.3.4. Apply mutation to the offspring\n", 138 | " first_new_ind = offspring1.mutation(mut_prob)\n", 139 | " # 2.3.5. Insert the mutated individuals into P'\n", 140 | " new_population.append(first_new_ind)\n", 141 | "\n", 142 | " if verbose:\n", 143 | " print(f'First mutated individual: {first_new_ind}')\n", 144 | " \n", 145 | " if len(new_population) < len(population):\n", 146 | " second_new_ind = offspring2.mutation(mut_prob)\n", 147 | " new_population.append(second_new_ind)\n", 148 | " if verbose:\n", 149 | " print(f'Second mutated individual: {first_new_ind}')\n", 150 | " \n", 151 | " # 2.4. Replace P with P'\n", 152 | " population = new_population\n", 153 | "\n", 154 | " if verbose:\n", 155 | " print(f'Final best individual in generation: {get_best_ind(population, maximization)}')\n", 156 | "\n", 157 | " # 3. Return the best individual in P\n", 158 | " return get_best_ind(population, maximization)\n" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "**NOTE:** There are many variations of genetic algorithms. The implementation used in our practical classes and the library follows some choices. For example, before inserting the second mutated individual into P', we check whether it would exceed the population size. This can happen with odd-sized populations since we always insert two individuals at a time. An alternative approach would be to insert the individual regardless and, if the population exceeds the limit, remove the worst-performing individual at the end.\n", 166 | "\n", 167 | "There are also other assumptions for our implementation of the algorithm to run.\n", 168 | "- individuals have `fitness`, `crossover` and `mutation` methods\n", 169 | "- `crossover` always returns two offspring\n", 170 | "- both `crossover` and `mutation` methods return new individuals instead of modifying individuals in-place" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "metadata": {}, 176 | "source": [ 177 | "### Selection algorithms\n", 178 | "\n", 179 | "Selection is the first main step of a genetic algorithm. Selection algorithms have the following properties:\n", 180 | "- are probabilistic\n", 181 | "- for any pair of individuals A and B, if A if better than B, then the probability of selecting A must be bigger than the probability of selecting B\n", 182 | "- all individuals must have the chance of being selected, even the worst in the population\n", 183 | "- when an individual is selected, it remains in population P and a copy is inserted in P'\n", 184 | "\n", 185 | "In class we'll implement **Fitness Proportionate Selection** (or roulette wheel), but there are other techniques like Ranking or Tournament selection.\n", 186 | "\n", 187 | "#### Fitness Proportionate Selection\n", 188 | "\n", 189 | "Probabilistic selection method used in GAs to choose individuals for reproduction. It mimics a roulette wheel, where individuals with higher fitness have a greater chance of being selected, but lower-fitness individuals still have some probability of selection.\n", 190 | "\n", 191 | "Let $N$ be the number of individuals in population $P$ and $F = {f_1, f_2, ..., f_N}$ be the set of fitness values of the indiiduals in the population. For an individual $i$ in the population, the probability of selecting $i$ is:\n", 192 | "\n", 193 | "$$P(selecting\\ i) = \\frac{f_i}{\\sum_{j=1}^{N} f_j}$$\n", 194 | "\n", 195 | "![Fitness Proportionate Selection Implementation](images/fps.png)\n", 196 | "\n", 197 | "Our implementation fo this selection algorithm will be a function that receives two arguments:\n", 198 | "- `population`: A list of individuals / solutions. These must have a `fitness()` method.\n", 199 | "- `maximization`: Boolean that indicates if we're solving a maximization or minimization problem" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": null, 205 | "metadata": {}, 206 | "outputs": [], 207 | "source": [ 208 | "def fitness_proportionate_selection(population: list[Solution], maximization: bool):\n", 209 | " total_fitness = sum([ind.fitness() for ind in population])\n", 210 | "\n", 211 | " if maximization:\n", 212 | " fitness_values = [ind.fitness() for ind in population]\n", 213 | " else:\n", 214 | " # Minimization: Use the inverse of the fitness value\n", 215 | " # Lower fitness should have higher probability of being selected\n", 216 | " fitness_values = [1 / ind.fitness() for ind in population]\n", 217 | "\n", 218 | " total_fitness = sum(fitness_values)\n", 219 | " # Generate random number between 0 and total\n", 220 | " random_nr = random.uniform(0, total_fitness)\n", 221 | " box_boundary = 0\n", 222 | " # For each individual, check if random number is inside the individual's \"box\"\n", 223 | " for ind_idx, ind in enumerate(population):\n", 224 | " box_boundary += fitness_values[ind_idx]\n", 225 | " if random_nr <= box_boundary:\n", 226 | " return deepcopy(ind)" 227 | ] 228 | } 229 | ], 230 | "metadata": { 231 | "kernelspec": { 232 | "display_name": "venv", 233 | "language": "python", 234 | "name": "python3" 235 | }, 236 | "language_info": { 237 | "codemirror_mode": { 238 | "name": "ipython", 239 | "version": 3 240 | }, 241 | "file_extension": ".py", 242 | "mimetype": "text/x-python", 243 | "name": "python", 244 | "nbconvert_exporter": "python", 245 | "pygments_lexer": "ipython3", 246 | "version": "3.13.2" 247 | } 248 | }, 249 | "nbformat": 4, 250 | "nbformat_minor": 2 251 | } 252 | -------------------------------------------------------------------------------- /notebooks-class/P2-HillClimbing-IntBin-Part1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from abc import ABC, abstractmethod\n", 20 | "from random import randint" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "## Optimization problems and algorithms" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "### Optimization problem\n", 35 | "\n", 36 | "An **optimization problem** is a problem that involves finding the best solution from a set of possible solutions, according to some criteria or objective. The goal is to either **maximize** or **minimize** a particular quantity (called the **objective function**) while satisfying certain constraints.\n", 37 | "\n", 38 | "**A solution to an optimization problem is problem dependent.**" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "### Optimization algorithm\n", 46 | "\n", 47 | "An **optimization algorithm** is a method used to find the best solution to an optimization problem. It is an interative algorithm that is able to return a solution at each iteration. The goal is to navigate the solution space, explore potential solutions, and ultimately identify the optimal or near-optimal solution according to the objective function.\n", 48 | "\n", 49 | "**An optimization algorithm is not problem dependent.** You can use the exact same algorithm to solve any optimization problem, as long as you know how you can navigate the solution space." 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "### Solution\n", 57 | "\n", 58 | "Let's start by implementig a generic `Solution` class.\n", 59 | "\n", 60 | "What should a generic solution to an optimization problem look like?\n", 61 | "\n", 62 | "Given that a solution to a problem `A` is different from a solution to a problem `B`, it is hard to characterize this generic solution. But there are some things that are common to any optimization problem solution.\n", 63 | "\n", 64 | "Let's think of attributes and methods that are common to any optimization problem.\n", 65 | "\n", 66 | "A solution must have the following attributes and methods:\n", 67 | "- representation: How the solution is enconded. Having a representation makes it possible for an algorithm to manipulate and evaluate the solution\n", 68 | "- fitness(): The function that determines how good a solution is\n", 69 | "- random_initial_value(): If no representation is defined for the solution, it has to be possible to initialize it randomly\n", 70 | "\n", 71 | "\n", 72 | "![Solution Class](images/solution.png)\n", 73 | "\n", 74 | "Since these are problem-dependent, we can not implement them now. However, we can **enforce their implementation** in any subclass by defining this class as an **abstract class** with abstract methods. To do this, the class must inherit from `ABC`. Abstract methods have no implementation in the abstract class itself, but any subclass must implement them to allow object instantiation." 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "class Solution(ABC):\n", 84 | " #TODO\n", 85 | " pass" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | "The final implementation is available in `library/solution.py`" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "## Hill Climbing\n", 100 | "\n", 101 | "Hill Climbing is one of the most intuitive and immediate techniques for solving optimization problems. It works by iteratively improving fitness in a stepwise refinement process, using the concept of neighborhood to explore potential solutions." 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "### Pseudo-code\n", 109 | "\n", 110 | "1. Initialize current solution (usually at random)\n", 111 | "2. Repeat\n", 112 | " 1. Get neighbors of current solution\n", 113 | " 2. Find best neighbor\n", 114 | " 3. If best neighbor is better or equal than current solution, replace current solution by best neighbor\n", 115 | " 4. If current solution hasn't changed, break the cycle\n", 116 | "3. Return current solution" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "### Algorithm Implementation\n", 124 | "\n", 125 | "Let's implement this algorithm using python. The function that implements the algorithm should receive the following arguments:\n", 126 | "- `initial solution`: an instance of a solution to an optimization problem\n", 127 | "- `maximization`: boolean that indicates if we're solving a maximization or minimization problem\n", 128 | "- `max_iter`: maximum number of interations. By default should be very big." 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "#TODO: Implement hill climbing algorithm" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "Notice that we assume that a solution has the following methods:\n", 145 | "- `fitness()`\n", 146 | "- `get_neighbors()`\n", 147 | "\n", 148 | "Additionally, `get_neighbors()` must return a list of solutions that also implement these methods." 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "The final implementation is available in `library/algorithms/hill_climbing.py`" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": {}, 161 | "source": [ 162 | "## IntBin Optimization Problem\n", 163 | "\n", 164 | "**Description:** The IntBin problem consists of finding the integer with greatest number of 1's in its binary representation\n", 165 | "\n", 166 | "**Search space:** Integers from 1 to 15.\n", 167 | "\n", 168 | "**Representation:** Binary string of 4 digits representing the integer.\n", 169 | "\n", 170 | "**Fitness function:** f(x)= Number of 1's in binary representation of x\n", 171 | "\n", 172 | "**Goal:** Maximize f(x)." 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": {}, 178 | "source": [ 179 | "### IntBin Solution\n", 180 | "\n", 181 | "Using the previously defined generic `Solution`, we can now define the `IntBinSolution` class that implements the fitness and random intial value methods for the IntBin problem.\n", 182 | "\n", 183 | "This class represents a solution to the IntBin problem and **does not include any implementation related to the optimization algorithm that will be used to solve it**.\n", 184 | "\n", 185 | "![IntBin Solution](images/intbin-solution.png)" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "#TODO: Implement IntBinSolution class" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "Let's test it." 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": null, 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [ 210 | "# Intialize with representation\n", 211 | "solution = IntBinSolution('0001')\n", 212 | "\n", 213 | "# Initialize with random representation\n", 214 | "solution_random = IntBinSolution()\n", 215 | "\n", 216 | "print(f'Solution {solution} with fitness {solution.fitness()}')\n", 217 | "print(f'Random solution {solution_random} with fitness {solution_random.fitness()}')" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "metadata": {}, 223 | "source": [ 224 | "### Solving IntBin with Hill Climbing\n", 225 | "\n", 226 | "To solve the IntBin problem using Hill Climbing, we need to define how to navigate the solution space. In hill climbing, the search space is navigated with the concept of neighborhood.\n", 227 | "\n", 228 | "The algorithm requires the solution to have a `get_neighbors()` method. Therefore, we can **extend** the `IntBinSolution` and create a `IntBinHillClimbingSolution` that implements the `get_neighbors()` method.\n", 229 | "\n", 230 | "![IntBin Hill Climbing Solution Inheritance](images/intbin-hillclimbing-solution.png)\n", 231 | "\n", 232 | "There are two options to get the neighbors of a solution:\n", 233 | "- Option 1 - Integer neighborhood: Each integer x has at most two neighbors: x-1 and x+1, except for boundaries (1 and 15).\n", 234 | "- Option 2 - Bit flip neighborhood: Each binary representation of an integer x has as neighbors any other binary with a bit flipped." 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "metadata": {}, 240 | "source": [ 241 | "#### Option 1 - Integer neighborhood: Each integer x has at most two neighbors: x-1 and x+1, except for boundaries (1 and 15).\n", 242 | "\n", 243 | "Let's create a `IntBin_IntNeighborhood_HillClimbingSolution` class that inherits from `IntBinSolution` and implements the `get_neighbors()` method.\n", 244 | "\n", 245 | "We also need to make sure that this method return a list of IntBin solutions that also implement `get_neighbors()`, meaning it should return a list of `IntBin_IntNeighborhood_HillClimbingSolution` instances." 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": null, 251 | "metadata": {}, 252 | "outputs": [], 253 | "source": [ 254 | "#TODO: Implement IntBin_IntNeighborhood_HillClimbingSolution class" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "Let's test it" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "# Initialize a random solution\n", 271 | "solution = IntBin_IntNeighborhood_HillClimbingSolution('1010')\n", 272 | "print('Solution', solution)\n", 273 | "\n", 274 | "neighbors = solution.get_neighbors()\n", 275 | "print('Neighbors:')\n", 276 | "for neighbor in neighbors:\n", 277 | " print(neighbor)" 278 | ] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "metadata": {}, 283 | "source": [ 284 | "We can now apply the HillClimbing algorithm to the IntBin problem by passing it an initial solution" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "#TODO: Apply hill climbing to IntBin" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "Let's see if the final solution changes with multiple runs" 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": null, 306 | "metadata": {}, 307 | "outputs": [], 308 | "source": [ 309 | "#TODO: Apply hill climbing to IntBin 10 times with differnt random initial solutions" 310 | ] 311 | }, 312 | { 313 | "cell_type": "markdown", 314 | "metadata": {}, 315 | "source": [ 316 | "Different runs produce different solutions, and not always the global optimum (1111) is found" 317 | ] 318 | }, 319 | { 320 | "cell_type": "markdown", 321 | "metadata": {}, 322 | "source": [ 323 | "In the next notebook, we will implement the IntBin problem using Hill Climbing, adopting Option 2 (Bit Flip Neighborhood) to explore the solution space and analyse the differences." 324 | ] 325 | } 326 | ], 327 | "metadata": { 328 | "kernelspec": { 329 | "display_name": "venv", 330 | "language": "python", 331 | "name": "python3" 332 | }, 333 | "language_info": { 334 | "codemirror_mode": { 335 | "name": "ipython", 336 | "version": 3 337 | }, 338 | "file_extension": ".py", 339 | "mimetype": "text/x-python", 340 | "name": "python", 341 | "nbconvert_exporter": "python", 342 | "pygments_lexer": "ipython3", 343 | "version": "3.13.2" 344 | } 345 | }, 346 | "nbformat": 4, 347 | "nbformat_minor": 2 348 | } 349 | -------------------------------------------------------------------------------- /notebooks-solution/P5-SimulatedAnnealling-TSP-KS.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import random\n", 20 | "import numpy as np\n", 21 | "from library.solution import Solution\n", 22 | "from library.problems.tsp import TSPSolution\n", 23 | "from library.problems.ks import KSSolution" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "## Simulated Annealing\n", 31 | "\n", 32 | "Simulated Annealing is an optimization algorithm that explores solutions by allowing both improvements and occasional worse moves to escape local optima. The probability of accepting worse solutions decreases over time, controlled by a temperature parameter that gradually cools. This balance between exploration and exploitation helps the algorithm find a global optimum rather than getting stuck in suboptimal solutions." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "### Pseudo-code\n", 40 | "\n", 41 | "1. Define the current solution (usually at random)\n", 42 | "2. Repeat until termination condition (usually nr of iterations):\n", 43 | " 1. Repeat **L** times:\n", 44 | " 1. Choose a random neighbor of the current solution\n", 45 | " 2. If random neighbor is better than current solution, replace current solution by neighbor. Otherwise, accept the nieghbor as the current solution with probability: $$exp(-\\frac{|neighbor.fitness - current.fitness|}{C})$$\n", 46 | " 2. Decrement **C** by dividing it by **H**\n", 47 | "3. Return current solution" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "### Algorithm Implementation\n", 55 | "\n", 56 | "Let's implement the simmulated annealing algorithm using python. The function that implements the algorithm should receive the following arguments:\n", 57 | "- `initial_solution`: Initial current solution\n", 58 | "- `C`: Control parameter\n", 59 | "- `L`: Number of iterations with same C\n", 60 | "- `H`: Decreasing rate of parameter C\n", 61 | "- `maximization`: boolean that indicates if we're solving a maximization or minimization problem\n", 62 | "- `max_iter`: maximum number of interations." 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "from copy import deepcopy\n", 72 | "\n", 73 | "def simulated_annealing(\n", 74 | " initial_solution: Solution,\n", 75 | " C: float = 100,\n", 76 | " L: int = 10,\n", 77 | " H: float = 1.1,\n", 78 | " maximization: bool = True,\n", 79 | " max_iter: int | None = 10,\n", 80 | " verbose: bool = False,\n", 81 | "):\n", 82 | " # 1. Initialize solution\n", 83 | " current_solution = initial_solution\n", 84 | "\n", 85 | " iter = 1\n", 86 | "\n", 87 | " if verbose:\n", 88 | " print(f'Initial solution: {current_solution.repr} with fitness {current_solution.fitness()}')\n", 89 | "\n", 90 | " # 2. Repeat until termination condition\n", 91 | " while iter <= max_iter:\n", 92 | " \n", 93 | " # 2.1 For L times\n", 94 | " for _ in range(L):\n", 95 | " # 2.1.1 Get random neighbor\n", 96 | " random_neighbor = current_solution.get_random_neighbor()\n", 97 | "\n", 98 | " neighbor_fitness = random_neighbor.fitness()\n", 99 | " current_fitness = current_solution.fitness()\n", 100 | "\n", 101 | " if verbose:\n", 102 | " print(f\"Random neighbor {random_neighbor} with fitness: {neighbor_fitness}\")\n", 103 | "\n", 104 | " # 2.1.2 Decide if neighbor is accepted as new solution\n", 105 | " # If neighbor is better, accept it\n", 106 | " if (\n", 107 | " (maximization and (neighbor_fitness >= current_fitness))\n", 108 | " or(not maximization and (neighbor_fitness <= current_fitness))\n", 109 | " ):\n", 110 | " current_solution = deepcopy(random_neighbor)\n", 111 | " if verbose:\n", 112 | " print(f'Neighbor is better. Replaced current solution by neighbor.')\n", 113 | "\n", 114 | " # If neighbor is worse, accept it with a certain probability\n", 115 | " # Maximizaton: Neighbor is worse than current solution if fitness is lower\n", 116 | " # Minimization: Neighbor is worse than current solution if fitness is higher\n", 117 | " elif (\n", 118 | " (maximization and (neighbor_fitness < current_fitness)\n", 119 | " or (not maximization and (neighbor_fitness > current_fitness)))\n", 120 | " ):\n", 121 | " # Generate random number between 0 and 1\n", 122 | " random_float = random.random()\n", 123 | " # Define probability P\n", 124 | " p = np.exp(-abs(current_fitness - neighbor_fitness) / C)\n", 125 | " if verbose:\n", 126 | " print(f'Probability of accepting worse neighbor: {p}')\n", 127 | " # The event happens with probability P if the random number if lower than P\n", 128 | " if random_float < p:\n", 129 | " current_solution = deepcopy(random_neighbor)\n", 130 | " if verbose:\n", 131 | " print(f'Neighbor is worse and was accepted.')\n", 132 | " else:\n", 133 | " if verbose:\n", 134 | " print(\"Neighbor is worse and was not accepted.\")\n", 135 | "\n", 136 | " if verbose:\n", 137 | " print(f\"New current solution {current_solution} with fitness {current_solution.fitness()}\")\n", 138 | "\n", 139 | " # 2.2 Update C\n", 140 | " C = C / H\n", 141 | " if verbose:\n", 142 | " print(f'Decreased C. New value: {C}')\n", 143 | " print('--------------')\n", 144 | "\n", 145 | " iter += 1\n", 146 | "\n", 147 | " if verbose:\n", 148 | " print(f'Best solution found: {current_solution.repr} with fitness {current_solution.fitness()}')\n", 149 | " \n", 150 | " # 3. Return solution\n", 151 | " return current_solution" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Notice that we assume that a solution has the following methods:\n", 159 | "- `fitness()`\n", 160 | "- `get_random_neighbor()`\n", 161 | "\n", 162 | "Additionally, `get_random_neighbor()` must return a solution that also implements these methods." 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "metadata": {}, 168 | "source": [ 169 | "### Solving TSP with Simulated Annealing" 170 | ] 171 | }, 172 | { 173 | "cell_type": "markdown", 174 | "metadata": {}, 175 | "source": [ 176 | "To solve TSP with simulated annealing we need to define a `TSPSASolution` class that inherits from `TSPSolution` and implements the `get_random_neighbor()` method.\n", 177 | "\n", 178 | "In the previous notebook, we implemented `TSPSolution`, which provides the `fitness()` and `random_initial_value()` methods. We also created `TSPHillClimbingSolution`, which extends `TSPSolution` and implements `get_neighbors()`.\n", 179 | "\n", 180 | "Simulated Annealing requires selecting only random neighbor rather than generating all neighbors. Therefore, we can create a new class `TSPSASolution`, that implements the method that is required for simulated annealing to work: `get_random_neighbor()`.\n", 181 | "\n", 182 | "We could do this two ways:\n", 183 | "- Inherit from `TSPHillClimbingSolution` and use the `get_neighbors()` method inside the `get_random_neighbor()` method to first get all neighbors, and then radomly select one\n", 184 | "- Inherit from `TSPSolution` and implement only the `get_random_neighbor()`\n", 185 | "\n", 186 | "Let's go with the second one to keep the code as independent, eficient and modular as possible.\n", 187 | "\n", 188 | "![TSP Solutions](images/tsp-solutions.png)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "A neighbor of a TSP solution can be obtained by swapping two consecutive cities on the route (excluding the starting and end points)." 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": 4, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "class TSPSASolution(TSPSolution):\n", 205 | " def get_random_neighbor(self):\n", 206 | " nr_cities = len(self.distance_matrix)\n", 207 | "\n", 208 | " # Choose a city idx to switch with the next city \n", 209 | " random_city_idx = random.randint(1, nr_cities-3)\n", 210 | "\n", 211 | " new_route = deepcopy(self.repr)\n", 212 | " new_route[random_city_idx] = self.repr[random_city_idx+1]\n", 213 | " new_route[random_city_idx+1] = self.repr[random_city_idx]\n", 214 | "\n", 215 | " return TSPSASolution(repr=new_route, distance_matrix=self.distance_matrix, starting_idx=self.starting_idx)" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "Let's test it" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": 5, 228 | "metadata": {}, 229 | "outputs": [ 230 | { 231 | "name": "stdout", 232 | "output_type": "stream", 233 | "text": [ 234 | "Solution [0, 8, 7, 12, 1, 2, 5, 6, 10, 11, 4, 9, 3, 0]\n", 235 | "Random neighbor [0, 8, 7, 12, 1, 2, 5, 10, 6, 11, 4, 9, 3, 0]\n" 236 | ] 237 | } 238 | ], 239 | "source": [ 240 | "solution = TSPSASolution()\n", 241 | "\n", 242 | "print('Solution', solution)\n", 243 | "print('Random neighbor', solution.get_random_neighbor())" 244 | ] 245 | }, 246 | { 247 | "cell_type": "markdown", 248 | "metadata": {}, 249 | "source": [ 250 | "And now we can apply the simulated annealing algorithm by giving it an random initial solution" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": null, 256 | "metadata": {}, 257 | "outputs": [], 258 | "source": [ 259 | "initial_solution = TSPSASolution()\n", 260 | "simulated_annealing(initial_solution, maximization=False, verbose=True)" 261 | ] 262 | }, 263 | { 264 | "cell_type": "markdown", 265 | "metadata": {}, 266 | "source": [ 267 | "The implementation of `TSPSASolution` can be found in `library/problems/tsp.py`" 268 | ] 269 | }, 270 | { 271 | "cell_type": "markdown", 272 | "metadata": {}, 273 | "source": [ 274 | "### Solving KS with Simulated Annealing\n", 275 | "\n", 276 | "To solve Knapsack with simulated annealing we need to define a `KSSASolution` class that inherits from `KSSolution` and implements the `get_random_neighbor()` method.\n", 277 | "\n", 278 | "In the previous notebook, we implemented `KSSolution`, which provides the `fitness()` and `random_initial_value()` methods. We also created `KSHillClimbingSolution`, which extends `KSSolution` and implements `get_neighbors()`.\n", 279 | "\n", 280 | "Since Simulated Annealing requires selecting a random neighbor rather than generating all neighbors, we can create a new class, `KSSASolution`, that implements the `get_random_neighbor()` method.\n", 281 | "\n", 282 | "Similarly to what we just did for TSP, let's implement the `KSSASolution` that inherits from `TSPSolution` and implements the `get_random_neighbor()`.\n", 283 | "\n", 284 | "A neighbor of a KS solution can be obtained by randomly flipping a bit, meaning, adding or removing an item from the knapsack." 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "class KSSASolution(KSSolution):\n", 294 | " def get_random_neighbor(self):\n", 295 | " neighbor_repr = deepcopy(self.repr)\n", 296 | " # Get random index\n", 297 | " random_idx = random.randint(0, len(self.values)-1)\n", 298 | " # Bit flip\n", 299 | " if neighbor_repr[random_idx] == 1:\n", 300 | " neighbor_repr[random_idx] = 0\n", 301 | " else:\n", 302 | " neighbor_repr[random_idx] = 1\n", 303 | " \n", 304 | " return KSSASolution(\n", 305 | " repr=neighbor_repr,\n", 306 | " values=self.values,\n", 307 | " weights=self.weights,\n", 308 | " capacity=self.capacity,\n", 309 | " )" 310 | ] 311 | }, 312 | { 313 | "cell_type": "markdown", 314 | "metadata": {}, 315 | "source": [ 316 | "Let's test it" 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": 8, 322 | "metadata": {}, 323 | "outputs": [ 324 | { 325 | "name": "stdout", 326 | "output_type": "stream", 327 | "text": [ 328 | "Solution [1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1]\n", 329 | "Random neighbor [1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1]\n" 330 | ] 331 | } 332 | ], 333 | "source": [ 334 | "solution = KSSASolution()\n", 335 | "\n", 336 | "print('Solution', solution)\n", 337 | "print('Random neighbor', solution.get_random_neighbor())" 338 | ] 339 | }, 340 | { 341 | "cell_type": "markdown", 342 | "metadata": {}, 343 | "source": [ 344 | "And now we can apply the simulated annealing algorithm by giving it an random initial solution" 345 | ] 346 | }, 347 | { 348 | "cell_type": "code", 349 | "execution_count": null, 350 | "metadata": {}, 351 | "outputs": [], 352 | "source": [ 353 | "initial_solution = KSSASolution()\n", 354 | "simulated_annealing(initial_solution, maximization=True, max_iter=10, verbose=True)" 355 | ] 356 | } 357 | ], 358 | "metadata": { 359 | "kernelspec": { 360 | "display_name": "venv", 361 | "language": "python", 362 | "name": "python3" 363 | }, 364 | "language_info": { 365 | "codemirror_mode": { 366 | "name": "ipython", 367 | "version": 3 368 | }, 369 | "file_extension": ".py", 370 | "mimetype": "text/x-python", 371 | "name": "python", 372 | "nbconvert_exporter": "python", 373 | "pygments_lexer": "ipython3", 374 | "version": "3.13.2" 375 | } 376 | }, 377 | "nbformat": 4, 378 | "nbformat_minor": 2 379 | } 380 | -------------------------------------------------------------------------------- /notebooks-class/P7-GeneticAlgorithms-Part2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from copy import deepcopy\n", 20 | "import random\n", 21 | "from library.problems.ks import KSSolution\n", 22 | "from library.problems.data.ks_data import weights, values, capacity\n", 23 | "from library.algorithms.genetic_algorithms.algorithm import genetic_algorithm\n", 24 | "from library.algorithms.genetic_algorithms.selection import fitness_proportionate_selection" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "## Genetic operators" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "### Binary Standard Mutation\n", 39 | "\n", 40 | "This mutation operator is used for binary string or list representations, such as '10001' or [1, 0, 0, 0, 1], found in problems like the Knapsack or IntBin problems.\n", 41 | "\n", 42 | "Standard binary mutation works by iterating over each position (or gene) in the binary string. For each gene, there is a fixed mutation probability that determines whether the bit should be flipped (a 0 becomes 1 and vice versa)\n", 43 | "\n", 44 | "![Binary Standard Mutation](images/binary-std-mutation.png)\n", 45 | "\n", 46 | "Let's implement a function for standard binary mutation. It takes a binary representation and a mutation probability as inputs and returns a new representation.\n" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "def binary_standard_mutation(representation: str | list, mut_prob):\n", 56 | " # TODO\n", 57 | " pass" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "Let's test on the Knapsack problem:" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "solution = KSSolution(values=values, weights=weights, capacity=capacity)\n", 74 | "\n", 75 | "print(\"Solution:\", solution)\n", 76 | "\n", 77 | "new_solution_repr = binary_standard_mutation(solution.repr, mut_prob=0.2)\n", 78 | "\n", 79 | "print(\"New solution:\", new_solution_repr)" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | "### Swap mutation\n", 87 | "\n", 88 | "Swap mutation is a general-purpose operator suitable for any type of string or list-based representation.\n", 89 | "\n", 90 | "It works by randomly selecting two positions (genes) in the solution and swapping their values. This swap is applied with a certain mutation probability.\n", 91 | "\n", 92 | "![Swap Mutation](images/swap-mutation.png)\n", 93 | "\n", 94 | "Let's implement the swap mutation function. It takes a representation and a mutation probability as inputs and returns a new solution where two genes may have been swapped." 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "def swap_mutation(representation: str | list, mut_prob):\n", 104 | " # TODO\n", 105 | " pass" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "Now let's test on the Knapsack problem." 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "solution = KSSolution(values=values, weights=weights, capacity=capacity)\n", 122 | "\n", 123 | "print(\"Solution:\", solution)\n", 124 | "\n", 125 | "new_solution_repr = swap_mutation(solution.repr, mut_prob=0.8)\n", 126 | "\n", 127 | "print(\"New solution:\", new_solution_repr)" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "### Standard Crossover\n", 135 | "\n", 136 | "Standard crossover takes two parent solutions, randomly selects a crossover point (an index between two consecutive genes) and exchanges the tail segments of the parents at that point. This process produces two new offspring that are combinations of their parents’ genetic material.\n", 137 | "\n", 138 | "![Standard Crossover](images/std-crossover.png)\n", 139 | "\n", 140 | "Let's implement the standard crossover function. It takes two parent representations as input and returns two offspring representations created by recombining segments from the parents at a randomly chosen crossover point." 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "def standard_crossover(parent1_repr, parent2_repr):\n", 150 | " # TODO\n", 151 | " pass" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Now let's test on the Knapsack problem." 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "parent1 = KSSolution(values=values, weights=weights, capacity=capacity)\n", 168 | "parent2 = KSSolution(values=values, weights=weights, capacity=capacity)\n", 169 | "\n", 170 | "print(\"Parent 1:\", parent1)\n", 171 | "print(\"Parent 2:\", parent2)\n", 172 | "\n", 173 | "offspring1_repr, offspring2_repr = standard_crossover(parent1.repr, parent2.repr)\n", 174 | "\n", 175 | "print(\"Offspring 1:\", offspring1_repr)\n", 176 | "print(\"Offspring 2:\", offspring2_repr)" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": {}, 182 | "source": [ 183 | "## Solving Knapsack with Genetic Algorithms\n", 184 | "\n", 185 | "![Knapsack Solutions](images/ks-solutions.png)\n", 186 | "\n", 187 | "In the last notebook we implemented the genetic algorithm function. This function receives the following arguments:\n", 188 | "- `initial_population`: List of individuals (randomly generated solutions)\n", 189 | "- `max_gen`: Maximum number of generations\n", 190 | "- `selection_algorithm`: A function that receives a population, selects one individual based on fitness and returns it\n", 191 | "- `maximization`: Boolean that indicates if we're solving a maximization or minimization problem\n", 192 | "- `xo_prob`: Probability of crossover (usually big)\n", 193 | "- `mut_prob`: Probability of mutation (usually small)\n", 194 | "- `elistism`: A boolean that indicates if elitism should be used or not\n", 195 | "\n", 196 | "For this function to work, we need to comply with some assumptions\n", 197 | "- individuals have `fitness`, `crossover` and `mutation` methods\n", 198 | "- `crossover` always returns two offspring\n", 199 | "- both `crossover` and `mutation` methods return new individuals instead of modifying individuals in-place\n", 200 | "\n", 201 | "To solve the Knapsack Problem (KS) using this GA framework, we can define a new class, `KSGASolution`, which extends `KSSolution`. This allows us to inherit methods like `fitness`, `random_initial_representation`, and the `repr` attribute.\n", 202 | "\n", 203 | "In `KSGASolution`, we'll implement the required `crossover` and `mutation` methods, adhering to the above assumptions.\n", 204 | "\n", 205 | "For simplicity, let's use the standard crossover and binary standard mutation." 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | "class KSGASolution(KSSolution):\n", 215 | " # TODO\n", 216 | " pass" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "Or we could just the functions we implemented in the beginning of the notebook!" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "class KSGASolution(KSSolution):\n", 233 | " # TODO\n", 234 | " pass" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "metadata": {}, 240 | "source": [ 241 | "#### How can I test different crossover and mutation operators?\n", 242 | "\n", 243 | "There are two approaches:\n", 244 | "- Create separate classes for each combination of crossover and mutation operators.\n", 245 | "\n", 246 | " For example:\n", 247 | " - `KS_StdXO_StdMut_GASolution`\n", 248 | " - `KS_StdXO_SwapMut_GASolution`\n", 249 | "\n", 250 | " This method works but can quickly become repetitive and hard to maintain as the number of combinations grows.\n", 251 | "\n", 252 | "- Make the solution class accept crossover and mutation functions as parameters during initialization.\n", 253 | "\n", 254 | " These functions would operate directly on the internal representation of the individual.\n", 255 | " ✅ This approach is much more modular and flexible!\n", 256 | " You can easily swap operators without needing to define new classes each time, making experimentation and tuning much easier." 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": null, 262 | "metadata": {}, 263 | "outputs": [], 264 | "source": [ 265 | "class KSGASolution(KSSolution):\n", 266 | " def __init__(\n", 267 | " self,\n", 268 | " values,\n", 269 | " weights,\n", 270 | " capacity,\n", 271 | " mutation_function, # Callable\n", 272 | " crossover_function, # Callable\n", 273 | " repr = None\n", 274 | " ):\n", 275 | " super().__init__(\n", 276 | " values=values,\n", 277 | " weights=weights,\n", 278 | " capacity=capacity,\n", 279 | " repr=repr,\n", 280 | " )\n", 281 | "\n", 282 | " # Save as attributes for access in methods\n", 283 | " self.mutation_function = mutation_function\n", 284 | " self.crossover_function = crossover_function\n", 285 | "\n", 286 | " \n", 287 | " def mutation(self, mut_prob):\n", 288 | " # Apply mutation function to representation\n", 289 | " new_repr = self.mutation_function(self.repr, mut_prob)\n", 290 | " # Create and return individual with mutated representation\n", 291 | " return KSGASolution(\n", 292 | " values=self.values,\n", 293 | " weights=self.weights,\n", 294 | " capacity=self.capacity,\n", 295 | " mutation_function=self.mutation_function,\n", 296 | " crossover_function=self.crossover_function,\n", 297 | " repr=new_repr\n", 298 | " )\n", 299 | "\n", 300 | " def crossover(self, other_solution):\n", 301 | " # Apply crossover function to self representation and other solution representation\n", 302 | " offspring1_repr, offspring2_repr = self.crossover_function(self.repr, other_solution.repr)\n", 303 | "\n", 304 | " # Create and return offspring with new representations\n", 305 | " return (\n", 306 | " KSGASolution(\n", 307 | " values=self.values,\n", 308 | " weights=self.weights,\n", 309 | " capacity=self.capacity,\n", 310 | " mutation_function=self.mutation_function,\n", 311 | " crossover_function=self.crossover_function,\n", 312 | " repr=offspring1_repr\n", 313 | " ),\n", 314 | " KSGASolution(\n", 315 | " values=self.values,\n", 316 | " weights=self.weights,\n", 317 | " capacity=self.capacity,\n", 318 | " mutation_function=self.mutation_function,\n", 319 | " crossover_function=self.crossover_function,\n", 320 | " repr=offspring2_repr\n", 321 | " )\n", 322 | " )" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": {}, 328 | "source": [ 329 | "Let's test." 330 | ] 331 | }, 332 | { 333 | "cell_type": "code", 334 | "execution_count": null, 335 | "metadata": {}, 336 | "outputs": [], 337 | "source": [ 338 | "repr = [0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1]\n", 339 | "\n", 340 | "# Using std crossover and std mutation\n", 341 | "solution1 = KSGASolution(\n", 342 | " values=values,\n", 343 | " weights=weights,\n", 344 | " capacity=capacity,\n", 345 | " mutation_function=binary_standard_mutation,\n", 346 | " crossover_function=standard_crossover,\n", 347 | " repr=repr\n", 348 | ")\n", 349 | "\n", 350 | "\n", 351 | "# Using std crossover and swap mutation\n", 352 | "solution2 = KSGASolution(\n", 353 | " values=values,\n", 354 | " weights=weights,\n", 355 | " capacity=capacity,\n", 356 | " mutation_function=swap_mutation,\n", 357 | " crossover_function=standard_crossover,\n", 358 | " repr=repr\n", 359 | ")" 360 | ] 361 | }, 362 | { 363 | "cell_type": "code", 364 | "execution_count": null, 365 | "metadata": {}, 366 | "outputs": [], 367 | "source": [ 368 | "# Apply binary standard mutation\n", 369 | "solution1.mutation(mut_prob=0.2)" 370 | ] 371 | }, 372 | { 373 | "cell_type": "code", 374 | "execution_count": null, 375 | "metadata": {}, 376 | "outputs": [], 377 | "source": [ 378 | "# Apply swap mutation\n", 379 | "solution2.mutation(mut_prob=0.2)" 380 | ] 381 | }, 382 | { 383 | "cell_type": "markdown", 384 | "metadata": {}, 385 | "source": [ 386 | "### Apply genetic algorithm\n", 387 | "\n", 388 | "Let's run the genetic algorithm to solve Knapsack using standard crossover and standard binary mutation" 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": null, 394 | "metadata": {}, 395 | "outputs": [], 396 | "source": [ 397 | "# TODO: Apply genetic algorithm to KS using standard crossover and binary standard mutation" 398 | ] 399 | }, 400 | { 401 | "cell_type": "markdown", 402 | "metadata": {}, 403 | "source": [ 404 | "And finally, let's apply the genetic algorithm again, but this time using swap mutation with a higher probability since it is not as disruptive as standard binary mutation." 405 | ] 406 | }, 407 | { 408 | "cell_type": "code", 409 | "execution_count": null, 410 | "metadata": {}, 411 | "outputs": [], 412 | "source": [ 413 | "# TODO: Apply genetic algorithm to KS using standard crossover and swap mutation" 414 | ] 415 | } 416 | ], 417 | "metadata": { 418 | "kernelspec": { 419 | "display_name": "venv", 420 | "language": "python", 421 | "name": "python3" 422 | }, 423 | "language_info": { 424 | "codemirror_mode": { 425 | "name": "ipython", 426 | "version": 3 427 | }, 428 | "file_extension": ".py", 429 | "mimetype": "text/x-python", 430 | "name": "python", 431 | "nbconvert_exporter": "python", 432 | "pygments_lexer": "ipython3", 433 | "version": "3.13.3" 434 | } 435 | }, 436 | "nbformat": 4, 437 | "nbformat_minor": 2 438 | } 439 | -------------------------------------------------------------------------------- /notebooks-solution/P2-HillClimbing-IntBin-Part1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from abc import ABC, abstractmethod\n", 20 | "from random import randint" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "## Optimization problems and algorithms" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "### Optimization problem\n", 35 | "\n", 36 | "An **optimization problem** is a problem that involves finding the best solution from a set of possible solutions, according to some criteria or objective. The goal is to either **maximize** or **minimize** a particular quantity (called the **objective function**) while satisfying certain constraints.\n", 37 | "\n", 38 | "**A solution to an optimization problem is problem dependent.**" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "### Optimization algorithm\n", 46 | "\n", 47 | "An **optimization algorithm** is a method used to find the best solution to an optimization problem. It is an interative algorithm that is able to return a solution at each iteration. The goal is to navigate the solution space, explore potential solutions, and ultimately identify the optimal or near-optimal solution according to the objective function.\n", 48 | "\n", 49 | "**An optimization algorithm is not problem dependent.** You can use the exact same algorithm to solve any optimization problem, as long as you know how you can navigate the solution space." 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "### Solution\n", 57 | "\n", 58 | "Let's start by implementig a generic `Solution` class.\n", 59 | "\n", 60 | "What should a generic solution to an optimization problem look like?\n", 61 | "\n", 62 | "Given that a solution to a problem `A` is different from a solution to a problem `B`, it is hard to characterize this generic solution. But there are some things that are common to any optimization problem solution.\n", 63 | "\n", 64 | "Let's think of attributes and methods that are common to any optimization problem.\n", 65 | "\n", 66 | "A solution must have the following attributes and methods:\n", 67 | "- representation: How the solution is enconded. Having a representation makes it possible for an algorithm to manipulate and evaluate the solution\n", 68 | "- fitness(): The function that determines how good a solution is\n", 69 | "- random_initial_representation(): If no representation is defined for the solution, it has to be possible to initialize it randomly.\n", 70 | " - **NOTE:** In class we called this method `random_initial_value` but `random_initial_representation` is more explicit. Let's use this name moving forward. The implementation in the library also adopts this name.\n", 71 | "\n", 72 | "\n", 73 | "![Solution Class](images/solution.png)\n", 74 | "\n", 75 | "Since these are problem-dependent, we can not implement them now. However, we can **enforce their implementation** in any subclass by defining this class as an **abstract class** with abstract methods. To do this, the class must inherit from `ABC`. Abstract methods have no implementation in the abstract class itself, but any subclass must implement them to allow object instantiation." 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 3, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "class Solution(ABC):\n", 85 | " def __init__(self, repr=None):\n", 86 | " # To initialize a solution we need to know it's representation.\n", 87 | " # If no representation is given, a representation is randomly initialized.\n", 88 | " if repr == None:\n", 89 | " repr = self.random_initial_representation()\n", 90 | " # Attributes\n", 91 | " self.repr = repr\n", 92 | "\n", 93 | " # Method that is called when we run print(object of the class)\n", 94 | " def __repr__(self):\n", 95 | " return str(self.repr)\n", 96 | "\n", 97 | " # Other methods that must be implemented in subclasses\n", 98 | " @abstractmethod\n", 99 | " def fitness(self):\n", 100 | " pass\n", 101 | "\n", 102 | " @abstractmethod\n", 103 | " def random_initial_representation():\n", 104 | " pass\n" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "The final implementation is available in `library/solution.py`" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "## Hill Climbing\n", 119 | "\n", 120 | "Hill Climbing is one of the most intuitive and immediate techniques for solving optimization problems. It works by iteratively improving fitness in a stepwise refinement process, using the concept of neighborhood to explore potential solutions." 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "### Pseudo-code\n", 128 | "\n", 129 | "1. Initialize current solution (usually at random)\n", 130 | "2. Repeat\n", 131 | " 1. Get neighbors of current solution\n", 132 | " 2. Find best neighbor\n", 133 | " 3. If best neighbor is better or equal than current solution, replace current solution by best neighbor\n", 134 | " 4. If current solution hasn't changed, break the cycle\n", 135 | "3. Return current solution" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "### Algorithm Implementation\n", 143 | "\n", 144 | "Let's implement this algorithm using python. The function that implements the algorithm should receive the following arguments:\n", 145 | "- `initial solution`: an instance of a solution to an optimization problem\n", 146 | "- `maximization`: boolean that indicates if we're solving a maximization or minimization problem\n", 147 | "- `max_iter`: maximum number of interations. By default should be very big." 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": 4, 153 | "metadata": {}, 154 | "outputs": [], 155 | "source": [ 156 | "from copy import deepcopy\n", 157 | "\n", 158 | "def hill_climbing(initial_solution: Solution, maximization=False, max_iter=99999, verbose=False):\n", 159 | " current = initial_solution\n", 160 | " improved = True\n", 161 | " iter = 1\n", 162 | "\n", 163 | " while improved:\n", 164 | " if verbose:\n", 165 | " print(f'Current solution: {current} with fitness {current.fitness()}')\n", 166 | "\n", 167 | " improved = False\n", 168 | " neighbors = current.get_neighbors()\n", 169 | "\n", 170 | " for neighbor in neighbors:\n", 171 | "\n", 172 | " if verbose:\n", 173 | " print(f'Neighbor: {neighbor} with fitness {neighbor.fitness()}')\n", 174 | "\n", 175 | " if maximization and (neighbor.fitness() >= current.fitness()):\n", 176 | " current = deepcopy(neighbor)\n", 177 | " improved = True\n", 178 | " elif not maximization and (neighbor.fitness() <= current.fitness()):\n", 179 | " current = deepcopy(neighbor)\n", 180 | " improved = True\n", 181 | " \n", 182 | " iter += 1\n", 183 | " if iter == max_iter:\n", 184 | " break\n", 185 | " \n", 186 | " return current" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": {}, 192 | "source": [ 193 | "Notice that we assume that a solution has the following methods:\n", 194 | "- `fitness()`\n", 195 | "- `get_neighbors()`\n", 196 | "\n", 197 | "Additionally, `get_neighbors()` must return a list of solutions that also implement these methods." 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "metadata": {}, 203 | "source": [ 204 | "The final implementation is available in `library/algorithms/hill_climbing.py`" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "## IntBin Optimization Problem\n", 212 | "\n", 213 | "**Description:** The IntBin problem consists of finding the integer with greatest number of 1's in its binary representation\n", 214 | "\n", 215 | "**Search space:** Integers from 1 to 15.\n", 216 | "\n", 217 | "**Representation:** Binary string of 4 digits representing the integer.\n", 218 | "\n", 219 | "**Fitness function:** f(x)= Number of 1's in binary representation of x\n", 220 | "\n", 221 | "**Goal:** Maximize f(x)." 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "metadata": {}, 227 | "source": [ 228 | "### IntBin Solution\n", 229 | "\n", 230 | "Using the previously defined generic `Solution`, we can now define the `IntBinSolution` class that implements the fitness and random intial representation methods for the IntBin problem.\n", 231 | "\n", 232 | "This class represents a solution to the IntBin problem and **does not include any implementation related to the optimization algorithm that will be used to solve it**.\n", 233 | "\n", 234 | "![IntBin Solution](images/intbin-solution.png)" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": 5, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "class IntBinSolution(Solution):\n", 244 | " # The constructor can be ommited here since it only calls the super class constructor\n", 245 | " def __init__(self, repr=None):\n", 246 | " super().__init__(repr=repr)\n", 247 | "\n", 248 | " # Override the superclass's methods\n", 249 | " def random_initial_representation(self):\n", 250 | " # Generate random integer between 1 and 15\n", 251 | " random_n = randint(1, 15)\n", 252 | " # Transform it into its binary string representation with 4 digits\n", 253 | " return str(format(random_n, '04b'))\n", 254 | "\n", 255 | " def fitness(self):\n", 256 | " return self.repr.count('1')" 257 | ] 258 | }, 259 | { 260 | "cell_type": "markdown", 261 | "metadata": {}, 262 | "source": [ 263 | "Let's test it." 264 | ] 265 | }, 266 | { 267 | "cell_type": "code", 268 | "execution_count": 6, 269 | "metadata": {}, 270 | "outputs": [ 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "Solution 0001 with fitness 1\n", 276 | "Random solution 1101 with fitness 3\n" 277 | ] 278 | } 279 | ], 280 | "source": [ 281 | "# Intialize with representation\n", 282 | "solution = IntBinSolution('0001')\n", 283 | "\n", 284 | "# Initialize with random representation\n", 285 | "solution_random = IntBinSolution()\n", 286 | "\n", 287 | "print(f'Solution {solution} with fitness {solution.fitness()}')\n", 288 | "print(f'Random solution {solution_random} with fitness {solution_random.fitness()}')" 289 | ] 290 | }, 291 | { 292 | "cell_type": "markdown", 293 | "metadata": {}, 294 | "source": [ 295 | "### Solving IntBin with Hill Climbing\n", 296 | "\n", 297 | "To solve the IntBin problem using Hill Climbing, we need to define how to navigate the solution space. In hill climbing, the search space is navigated with the concept of neighborhood.\n", 298 | "\n", 299 | "The algorithm requires the solution to have a `get_neighbors()` method. Therefore, we can **extend** the `IntBinSolution` and create a `IntBinHillClimbingSolution` that implements the `get_neighbors()` method.\n", 300 | "\n", 301 | "![IntBin Hill Climbing Solution Inheritance](images/intbin-hillclimbing-solution.png)\n", 302 | "\n", 303 | "There are two options to get the neighbors of a solution:\n", 304 | "- Option 1 - Integer neighborhood: Each integer x has at most two neighbors: x-1 and x+1, except for boundaries (1 and 15).\n", 305 | "- Option 2 - Bit flip neighborhood: Each binary representation of an integer x has as neighbors any other binary with a bit flipped." 306 | ] 307 | }, 308 | { 309 | "cell_type": "markdown", 310 | "metadata": {}, 311 | "source": [ 312 | "#### Option 1 - Integer neighborhood: Each integer x has at most two neighbors: x-1 and x+1, except for boundaries (1 and 15).\n", 313 | "\n", 314 | "Let's create a `IntBin_IntNeighborhood_HillClimbingSolution` class that inherits from `IntBinSolution` and implements the `get_neighbors()` method.\n", 315 | "\n", 316 | "We also need to make sure that this method return a list of IntBin solutions that also implement `get_neighbors()`, meaning it should return a list of `IntBin_IntNeighborhood_HillClimbingSolution` instances." 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": 11, 322 | "metadata": {}, 323 | "outputs": [], 324 | "source": [ 325 | "class IntBin_IntNeighborhood_HillClimbingSolution(IntBinSolution):\n", 326 | " # This method must return a list of IntBin_IntNeighborhood_HillClimbingSolution objects (the neighbors)\n", 327 | " def get_neighbors(self):\n", 328 | " # Convert binary string representation into integer\n", 329 | " int_repr = int(self.repr, 2)\n", 330 | "\n", 331 | " if int_repr == 1:\n", 332 | " return [IntBin_IntNeighborhood_HillClimbingSolution(repr=format(2, '04b'))]\n", 333 | " elif int_repr == 15:\n", 334 | " return [IntBin_IntNeighborhood_HillClimbingSolution(repr=format(14, '04b'))]\n", 335 | " else:\n", 336 | " return [\n", 337 | " IntBin_IntNeighborhood_HillClimbingSolution(repr=format(int_repr-1, '04b')),\n", 338 | " IntBin_IntNeighborhood_HillClimbingSolution(repr=format(int_repr+1, '04b'))\n", 339 | " ]" 340 | ] 341 | }, 342 | { 343 | "cell_type": "markdown", 344 | "metadata": {}, 345 | "source": [ 346 | "Let's test it" 347 | ] 348 | }, 349 | { 350 | "cell_type": "code", 351 | "execution_count": 8, 352 | "metadata": {}, 353 | "outputs": [ 354 | { 355 | "name": "stdout", 356 | "output_type": "stream", 357 | "text": [ 358 | "Solution 1010\n", 359 | "Neighbors:\n", 360 | "1001\n", 361 | "1011\n" 362 | ] 363 | } 364 | ], 365 | "source": [ 366 | "# Initialize a random solution\n", 367 | "solution = IntBin_IntNeighborhood_HillClimbingSolution('1010')\n", 368 | "print('Solution', solution)\n", 369 | "\n", 370 | "neighbors = solution.get_neighbors()\n", 371 | "print('Neighbors:')\n", 372 | "for neighbor in neighbors:\n", 373 | " print(neighbor)" 374 | ] 375 | }, 376 | { 377 | "cell_type": "markdown", 378 | "metadata": {}, 379 | "source": [ 380 | "We can now apply the HillClimbing algorithm to the IntBin problem by passing it an initial solution" 381 | ] 382 | }, 383 | { 384 | "cell_type": "code", 385 | "execution_count": 12, 386 | "metadata": {}, 387 | "outputs": [ 388 | { 389 | "name": "stdout", 390 | "output_type": "stream", 391 | "text": [ 392 | "Current solution: 1010 with fitness 2\n", 393 | "Neighbor: 1001 with fitness 2\n", 394 | "Neighbor: 1011 with fitness 3\n", 395 | "Current solution: 1011 with fitness 3\n", 396 | "Neighbor: 1010 with fitness 2\n", 397 | "Neighbor: 1100 with fitness 2\n", 398 | "Best solution: 1011\n" 399 | ] 400 | } 401 | ], 402 | "source": [ 403 | "# Randomly initialize a solution\n", 404 | "initial_solution = IntBin_IntNeighborhood_HillClimbingSolution()\n", 405 | "\n", 406 | "best_solution = hill_climbing(initial_solution, maximization=True, verbose=True)\n", 407 | "\n", 408 | "print('Best solution:', best_solution)" 409 | ] 410 | }, 411 | { 412 | "cell_type": "markdown", 413 | "metadata": {}, 414 | "source": [ 415 | "Let's see if the final solution changes with multiple runs" 416 | ] 417 | }, 418 | { 419 | "cell_type": "code", 420 | "execution_count": 16, 421 | "metadata": {}, 422 | "outputs": [ 423 | { 424 | "name": "stdout", 425 | "output_type": "stream", 426 | "text": [ 427 | "Final solution: 1011\n", 428 | "Final solution: 0111\n", 429 | "Final solution: 0011\n", 430 | "Final solution: 0111\n", 431 | "Final solution: 1011\n", 432 | "Final solution: 0111\n", 433 | "Final solution: 1011\n", 434 | "Final solution: 0111\n", 435 | "Final solution: 1111\n", 436 | "Final solution: 1011\n" 437 | ] 438 | } 439 | ], 440 | "source": [ 441 | "for _ in range(10):\n", 442 | " initial_solution = IntBin_IntNeighborhood_HillClimbingSolution()\n", 443 | " print('Final solution:', hill_climbing(initial_solution, maximization=True))" 444 | ] 445 | }, 446 | { 447 | "cell_type": "markdown", 448 | "metadata": {}, 449 | "source": [ 450 | "Different runs produce different solutions, and not always the global optimum (1111) is found" 451 | ] 452 | }, 453 | { 454 | "cell_type": "markdown", 455 | "metadata": {}, 456 | "source": [ 457 | "In the next notebook, we will implement the IntBin problem using Hill Climbing, adopting Option 2 (Bit Flip Neighborhood) to explore the solution space and analyse the differences." 458 | ] 459 | } 460 | ], 461 | "metadata": { 462 | "kernelspec": { 463 | "display_name": "venv", 464 | "language": "python", 465 | "name": "python3" 466 | }, 467 | "language_info": { 468 | "codemirror_mode": { 469 | "name": "ipython", 470 | "version": 3 471 | }, 472 | "file_extension": ".py", 473 | "mimetype": "text/x-python", 474 | "name": "python", 475 | "nbconvert_exporter": "python", 476 | "pygments_lexer": "ipython3", 477 | "version": "3.13.2" 478 | } 479 | }, 480 | "nbformat": 4, 481 | "nbformat_minor": 2 482 | } 483 | -------------------------------------------------------------------------------- /notebooks-solution/P8-Genetic-Algorithms-Part3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "sys.path.append('..')" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 129, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from copy import deepcopy\n", 20 | "import random\n", 21 | "from library.problems.tsp import TSPSolution\n", 22 | "from library.problems.data.tsp_data import distance_matrix\n", 23 | "from library.algorithms.genetic_algorithms.algorithm import genetic_algorithm\n", 24 | "from library.algorithms.genetic_algorithms.mutation import swap_mutation\n", 25 | "from library.algorithms.genetic_algorithms.selection import fitness_proportionate_selection" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## Specialized Genetic Operators" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "In the previous notebook, we explored some traditional genetic operators: **standard crossover, binary mutation, and swap mutation**.\n", 40 | "\n", 41 | "However, these traditional operators generate invalid solutions when applied to combinatorial problems such as the Traveling Salesman Problem (TSP), job scheduling, or vehicle routing, where solutions are represented as **permutations**.\n", 42 | "\n", 43 | "In this notebook, we’ll explore **specialized genetic operators**, specifically designed to handle permutations without producing invalid solutions. We’ll explore one crossover and one mutation methods that respects permutation constraints." 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "### Why Standard Crossover/Mutation Fail for Permutation Problems\n", 51 | "\n", 52 | "In permutation-based problems (e.g., [1, 2, 3, 4, 5]), each gene must appear exactly once.\n", 53 | "Standard genetic operators like one-point crossover or value-flip mutation can break this rule, resulting in invalid offspring with duplicates or missing values.\n", 54 | "\n", 55 | "\n", 56 | "#### Standard Mutation - Value Flip\n", 57 | "\n", 58 | "This mutation is inspired by the standard binary mutation: each gene is randomly replaced with another value with some probability.\n", 59 | "\n", 60 | "Individual [1, 2, 3, 4, 5]\n", 61 | "\n", 62 | "Mutated individual (hypothetical): [1, 2, 4, 1, 5] ❌ (Duplicate '1')\n", 63 | "\n", 64 | "#### Standard Crossover\n", 65 | "\n", 66 | "Parent 1: [1, 2 | 3, 4, 5]\n", 67 | "\n", 68 | "Parent 2: [3, 4 | 5, 1, 2]\n", 69 | "\n", 70 | "\n", 71 | "Offspring 1 (invalid): [1, 2, 5, 1, 2] ❌ (Duplicates '1' and '2')\n", 72 | "\n", 73 | "Offspring 2 (invalid): [3, 4, 3, 4, 5] ❌ (Duplicates '3' and '4')" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "### Cycle Crossover\n", 81 | "\n", 82 | "Cycle Crossover keeps items in their original positions across parents by identifying cycles of indices where elements should remain fixed.\n", 83 | "\n", 84 | "**Pseudo-code:**\n", 85 | "\n", 86 | "1. Choose random index in Parent 1 and copy the element to first child.\n", 87 | "3. Copy element in same index in Parent 2 to second child.\n", 88 | "4. Find this element in Parent 1 and copy it to first child, and repeat the process.\n", 89 | "5. Once the cycle completes (we end up back on the initial index), the remaining positions are filled from the other parent.\n", 90 | "\n", 91 | "![Cycle Crossover](images/cycle-xo.png)\n" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "def cycle_crossover(parent1_repr: str | list, parent2_repr: str | list):\n", 101 | " \"\"\"\n", 102 | " Performs Cycle Crossover (CX) between two parents\n", 103 | "\n", 104 | " Cycle Crossover preserves the position of elements by identifying a cycle\n", 105 | " of indices where the values from each parent will be inherited by each offspring.\n", 106 | " The remaining indices are filled with values from the other parent, maintaining valid permutations.\n", 107 | "\n", 108 | " Args:\n", 109 | " parent1_repr (str or list): The first parent representation.\n", 110 | " parent2_repr (str or list): The second parent representation.\n", 111 | " Both parents must have the same length and type.\n", 112 | "\n", 113 | " Returns:\n", 114 | " tuple: Two offspring permutations resulting from the crossover.\n", 115 | " \"\"\"\n", 116 | " # Randomly choose a starting index for the cycle\n", 117 | " initial_random_idx = random.randint(0, len(parent1_repr)-1)\n", 118 | "\n", 119 | " # Initialize the cycle with the starting index\n", 120 | " cycle_idxs = [initial_random_idx]\n", 121 | " current_cycle_idx = initial_random_idx\n", 122 | "\n", 123 | " # Traverse the cycle by following the mapping from parent2 to parent1\n", 124 | " while True:\n", 125 | " value_parent2 = parent2_repr[current_cycle_idx]\n", 126 | " # Find where this value is in parent1 to get the next index in the cycle\n", 127 | " next_cycle_idx = parent1_repr.index(value_parent2)\n", 128 | "\n", 129 | " # Closed the cycle -> Break\n", 130 | " if next_cycle_idx == initial_random_idx:\n", 131 | " break\n", 132 | "\n", 133 | " cycle_idxs.append(next_cycle_idx)\n", 134 | " current_cycle_idx = next_cycle_idx\n", 135 | " \n", 136 | " offspring1_repr = []\n", 137 | " offspring2_repr = []\n", 138 | " for idx in range(len(parent1_repr)):\n", 139 | " if idx in cycle_idxs:\n", 140 | " # Keep values from parent1 in offspring1 in the cycle indexes\n", 141 | " offspring1_repr.append(parent1_repr[idx])\n", 142 | " # Keep values from parent2 in offspring2 in the cycle indexes\n", 143 | " offspring2_repr.append(parent2_repr[idx])\n", 144 | " else:\n", 145 | " # Swap elements from parents in non-cycle indexes\n", 146 | " offspring1_repr.append(parent2_repr[idx])\n", 147 | " offspring2_repr.append(parent1_repr[idx])\n", 148 | "\n", 149 | " # To keep the same type as the parents representation\n", 150 | " if isinstance(parent1_repr, str) and isinstance(parent2_repr, str):\n", 151 | " offspring1_repr = \"\".join(offspring1_repr)\n", 152 | " offspring2_repr = \"\".join(offspring2_repr)\n", 153 | "\n", 154 | " return offspring1_repr, offspring2_repr" 155 | ] 156 | }, 157 | { 158 | "cell_type": "markdown", 159 | "metadata": {}, 160 | "source": [ 161 | "Let's test it." 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": 61, 167 | "metadata": {}, 168 | "outputs": [ 169 | { 170 | "name": "stdout", 171 | "output_type": "stream", 172 | "text": [ 173 | "Parent 1: [1, 2, 3, 4, 5, 6, 7]\n", 174 | "Parent 2: [3, 4, 2, 1, 7, 5, 6]\n", 175 | "Offspring 1: [1, 2, 3, 4, 7, 5, 6]\n", 176 | "Offspring 2: [3, 4, 2, 1, 5, 6, 7]\n" 177 | ] 178 | } 179 | ], 180 | "source": [ 181 | "parent1 = [1, 2, 3, 4, 5, 6, 7]\n", 182 | "parent2 = [3, 4, 2, 1, 7, 5, 6]\n", 183 | "\n", 184 | "print(\"Parent 1:\", parent1)\n", 185 | "print(\"Parent 2:\", parent2)\n", 186 | "\n", 187 | "offspring1_repr, offspring2_repr = cycle_crossover(parent1_repr=parent1, parent2_repr=parent2)\n", 188 | "\n", 189 | "print(\"Offspring 1:\", offspring1_repr)\n", 190 | "print(\"Offspring 2:\", offspring2_repr)" 191 | ] 192 | }, 193 | { 194 | "cell_type": "markdown", 195 | "metadata": {}, 196 | "source": [ 197 | "### Inversion Mutation\n", 198 | "\n", 199 | "Inversion mutation works by selecting two random indices and reversing the subsequence between them, with a certain probability.\n", 200 | "\n", 201 | "![Inversion Mutation](images/inversion-mutation.png)" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": null, 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [ 210 | "def inversion_mutation(representation: str | list, mut_prob):\n", 211 | " \"\"\"\n", 212 | " Applies inversion mutation to a representation.\n", 213 | "\n", 214 | " Inversion mutation selects two random indices and reverses the \n", 215 | " subsequence between them, with a certain probability.\n", 216 | "\n", 217 | " Parameters:\n", 218 | " ----------\n", 219 | " representation : str or list\n", 220 | " The individual to mutate. Should represent a valid permutation.\n", 221 | " mut_prob : float\n", 222 | " Probability of applying the mutation (between 0 and 1).\n", 223 | "\n", 224 | " Returns:\n", 225 | " -------\n", 226 | " str or list\n", 227 | " A new individual with the mutated representation (if mutation occurs),\n", 228 | " or a copy of the original.\n", 229 | " \"\"\"\n", 230 | " if random.random() <= mut_prob:\n", 231 | " # Select two distinct indices\n", 232 | " first_idx = random.randint(0, len(representation)-1)\n", 233 | " second_idx = first_idx\n", 234 | " # We want to get two indexes that are at least 2 genes away\n", 235 | " while abs(second_idx-first_idx) <= 1:\n", 236 | " second_idx = random.randint(0, len(representation)-1)\n", 237 | " \n", 238 | " # Ensure first_idx < second_idx\n", 239 | " if first_idx > second_idx:\n", 240 | " first_idx, second_idx = second_idx, first_idx\n", 241 | "\n", 242 | " # Reverse between first and second index\n", 243 | " reversed_subsequence = list(reversed(representation[first_idx:second_idx]))\n", 244 | "\n", 245 | " # Convert back to string if original representation is a string\n", 246 | " if isinstance(representation, str):\n", 247 | " reversed_subsequence = \"\".join(reversed_subsequence)\n", 248 | "\n", 249 | " # Keep everything from second index (excluding it) until the end\n", 250 | " new_representation = representation[:first_idx] + reversed_subsequence + representation[second_idx:]\n", 251 | " return new_representation\n", 252 | " else:\n", 253 | " return deepcopy(representation)" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "metadata": {}, 259 | "source": [ 260 | "Let's test it." 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 125, 266 | "metadata": {}, 267 | "outputs": [ 268 | { 269 | "data": { 270 | "text/plain": [ 271 | "[1, 2, 7, 6, 5, 4, 3]" 272 | ] 273 | }, 274 | "execution_count": 125, 275 | "metadata": {}, 276 | "output_type": "execute_result" 277 | } 278 | ], 279 | "source": [ 280 | "representation = [1, 2, 3, 4, 5, 6, 7]\n", 281 | "\n", 282 | "inversion_mutation(representation, mut_prob=1)" 283 | ] 284 | }, 285 | { 286 | "cell_type": "markdown", 287 | "metadata": {}, 288 | "source": [ 289 | "## Solving TSP with Genetic Algorithms\n", 290 | "\n", 291 | "Just like we did in the previous notebook for the Knapsack Problem, we’ll now solve TSP using genetic algorithms.\n", 292 | "\n", 293 | "To structure our solution, we’ll define a `TSPGASolution` class where we’ll define in the crossover and mutation methods needed to run the `genetic_algorithm` function.\n", 294 | "\n", 295 | "To keep the class flexible and reusable, we’ll pass the `mutation_function` and `crossover_function` as callable arguments when creating an instance of `TSPGASolution`.\n", 296 | "\n", 297 | "### A small but important side note\n", 298 | "\n", 299 | "TSP solutions are represented as permutations of city indices, where the path must start and end at the same city (i.e., the starting index is fixed at both ends).\n", 300 | "\n", 301 | "When applying permutation-based operators like cycle crossover or inversion mutation, we need to preserve this constraint. That means we should only apply genetic operators to the middle portion of the route: excluding the first and last cities, which must remain the same.\n", 302 | "\n", 303 | "So in practice, our crossover and mutation functions will only operate on the inner part of the individual, keeping the boundaries intact." 304 | ] 305 | }, 306 | { 307 | "cell_type": "code", 308 | "execution_count": null, 309 | "metadata": {}, 310 | "outputs": [], 311 | "source": [ 312 | "class TSPGASolution(TSPSolution):\n", 313 | " def __init__(\n", 314 | " self,\n", 315 | " distance_matrix,\n", 316 | " starting_idx,\n", 317 | " mutation_function, # Callable\n", 318 | " crossover_function, # Callable\n", 319 | " repr = None,\n", 320 | " ):\n", 321 | " self.mutation_function = mutation_function\n", 322 | " self.crossover_function = crossover_function\n", 323 | "\n", 324 | " super().__init__(\n", 325 | " distance_matrix=distance_matrix,\n", 326 | " starting_idx=starting_idx,\n", 327 | " repr=repr,\n", 328 | " )\n", 329 | " \n", 330 | " def mutation(self, mut_prob):\n", 331 | " \"\"\"\n", 332 | " Applies the provided mutation operator to the middle portion \n", 333 | " of the route (excluding start and end cities).\n", 334 | " \"\"\"\n", 335 | " # Apply mutation to the middle route segment\n", 336 | " middle_segment = self.repr[1:-1] # Exclude starting/ending city\n", 337 | " mutated_segment = self.mutation_operator(middle_segment, mut_prob)\n", 338 | " new_repr = [self.starting_idx] + mutated_segment + [self.starting_idx]\n", 339 | " \n", 340 | " return TSPGASolution(\n", 341 | " distance_matrix=self.distance_matrix,\n", 342 | " starting_idx=self.starting_idx,\n", 343 | " mutation_function=self.mutation_function,\n", 344 | " crossover_function=self.crossover_function,\n", 345 | " repr=new_repr\n", 346 | " )\n", 347 | "\n", 348 | " def crossover(self, other_solution):\n", 349 | " \"\"\"\n", 350 | " Applies the provided crossover operator to the middle portions\n", 351 | " of two parent routes (excluding start/end cities), and returns\n", 352 | " two new offspring solutions.\n", 353 | " \"\"\"\n", 354 | " # Apply crossover to the middle route segment of the parents\n", 355 | " parent1_middle = self.repr[1:-1]\n", 356 | " parent2_middle = other_solution.repr[1:-1]\n", 357 | "\n", 358 | " offspring1_middle, offspring2_middle = self.crossover_function(parent1_middle, parent2_middle)\n", 359 | "\n", 360 | " offspring1_repr = [self.starting_idx] + offspring1_middle + [self.starting_idx]\n", 361 | " offspring2_repr = [self.starting_idx] + offspring2_middle + [self.starting_idx]\n", 362 | "\n", 363 | " return (\n", 364 | " TSPGASolution(\n", 365 | " distance_matrix=self.distance_matrix,\n", 366 | " starting_idx=self.starting_idx,\n", 367 | " mutation_function=self.mutation_function,\n", 368 | " crossover_function=self.crossover_function,\n", 369 | " repr=offspring1_repr\n", 370 | " ),\n", 371 | " TSPGASolution(\n", 372 | " distance_matrix=self.distance_matrix,\n", 373 | " starting_idx=self.starting_idx,\n", 374 | " mutation_function=self.mutation_function,\n", 375 | " crossover_function=self.crossover_function,\n", 376 | " repr=offspring2_repr\n", 377 | " )\n", 378 | " )" 379 | ] 380 | }, 381 | { 382 | "cell_type": "markdown", 383 | "metadata": {}, 384 | "source": [ 385 | "Let's apply the genetic algorithm to TSP using cycle crossover and inversion mutation. Here the probability of mutation should be relatively small because inversion mutation may be very destructive." 386 | ] 387 | }, 388 | { 389 | "cell_type": "code", 390 | "execution_count": null, 391 | "metadata": {}, 392 | "outputs": [], 393 | "source": [ 394 | "POP_SIZE = 50\n", 395 | "initial_population = [\n", 396 | " TSPGASolution(\n", 397 | " distance_matrix=distance_matrix,\n", 398 | " starting_idx=0,\n", 399 | " crossover_function=cycle_crossover,\n", 400 | " mutation_function=inversion_mutation\n", 401 | " )\n", 402 | " for _ in range(POP_SIZE)\n", 403 | "]\n", 404 | "\n", 405 | "best_solution = genetic_algorithm(\n", 406 | " initial_population=initial_population,\n", 407 | " max_gen=100,\n", 408 | " selection_algorithm=fitness_proportionate_selection,\n", 409 | " maximization=False,\n", 410 | " xo_prob=0.8,\n", 411 | " mut_prob=0.2,\n", 412 | " elitism=True,\n", 413 | " verbose=True\n", 414 | ")\n", 415 | "\n", 416 | "print(\"Best solution fitness\", best_solution.fitness())" 417 | ] 418 | }, 419 | { 420 | "cell_type": "markdown", 421 | "metadata": {}, 422 | "source": [ 423 | "And now let's use cycle crossover again, but this time we use swap mutation.\n", 424 | "\n", 425 | "Now we can set a higher probability of miutation because swap mutation is less destructive than inversion mutation." 426 | ] 427 | }, 428 | { 429 | "cell_type": "code", 430 | "execution_count": null, 431 | "metadata": {}, 432 | "outputs": [], 433 | "source": [ 434 | "initial_population = [\n", 435 | " TSPGASolution(\n", 436 | " distance_matrix=distance_matrix,\n", 437 | " starting_idx=0,\n", 438 | " crossover_function=cycle_crossover,\n", 439 | " mutation_function=swap_mutation\n", 440 | " )\n", 441 | " for _ in range(POP_SIZE)\n", 442 | "]\n", 443 | "\n", 444 | "best_solution = genetic_algorithm(\n", 445 | " initial_population=initial_population,\n", 446 | " max_gen=100,\n", 447 | " selection_algorithm=fitness_proportionate_selection,\n", 448 | " maximization=False,\n", 449 | " xo_prob=0.8,\n", 450 | " mut_prob=0.2,\n", 451 | " elitism=True,\n", 452 | " verbose=True\n", 453 | ")\n", 454 | "\n", 455 | "print(\"Best solution fitness\", best_solution.fitness())" 456 | ] 457 | } 458 | ], 459 | "metadata": { 460 | "kernelspec": { 461 | "display_name": "venv", 462 | "language": "python", 463 | "name": "python3" 464 | }, 465 | "language_info": { 466 | "codemirror_mode": { 467 | "name": "ipython", 468 | "version": 3 469 | }, 470 | "file_extension": ".py", 471 | "mimetype": "text/x-python", 472 | "name": "python", 473 | "nbconvert_exporter": "python", 474 | "pygments_lexer": "ipython3", 475 | "version": "3.13.3" 476 | } 477 | }, 478 | "nbformat": 4, 479 | "nbformat_minor": 2 480 | } 481 | --------------------------------------------------------------------------------