Source code for population

"""This module defines the Population class.

This module defines the Population class, used to create a population of
individuals. It defines the methods allowed on populations including:

  * Mutation
  * Crossover
  * Elitism
"""
from __future__ import annotations
from typing import Type, Dict, List, Union, Callable, Any, Tuple
import numpy as np

from individual import Individual

PopulationType = List[Individual]
Configuration = Dict[str, Any]
GenotypeConfig = Dict[str, Tuple[type, List[Union[int, float]]]]

[docs]class Population: """The Population class, used to define a population of individuals. Defines a population of individuals, performs the mutations, evaluations and selection of individuals. """ def __init__(self, config:Configuration): """Inits a population given a population size and genotype shape.""" self.Individual:Type[Individual] = Individual # pylint: disable=C0103 self.config:Configuration = config if "elitism" in config: self.elitism:Union[int, float] = config["elitism"] else: self.elitism: None = None self.pop_size:int = self.config["pop_size"] self.indiv_shape:GenotypeConfig = self.config["indiv"] if "constraint" in self.config: if self.config["constraint"] not in ["Hard", "Soft"]: print("Constraint should be Hard or Soft. Defaulting to Soft") self.constraint:str = "Soft" else: self.constraint:str = self.config["constraint"] else: self.constraint:str = "Soft" self.fitness:Callable = self.config["fitness"] if "optimization function" in self.config: self.optimization_func:Callable = self.config["optimization function"] else: self.optimization_func:Callable = lambda x:x self.pop:List[Individual] = self._init_pop() def _init_pop(self) -> PopulationType: """Generate a random population of size pop_size.""" pop = [] for _ in range(self.pop_size): pop.append(self.Individual(self.indiv_shape, constraint=self.constraint)) return pop def __len__(self): return len(self.pop)
[docs] def select(self) -> None: """Performs the selection of elites and mutates the rest of the population. """ new_pop = [] if self.elitism is not None: elites = self._get_elites() new_pop += elites pop_idx_list = list(range(len(self))) mutants_idx = list(np.random.choice(pop_idx_list, len(self) - len(new_pop))) mutants = [] for idx in mutants_idx: mutants.append(self._get_individual(idx)) self.mutate(mutants) new_pop += mutants self.pop = new_pop
def _get_individual(self, idx:int) -> Individual: """Returns a deepcopy of an individual in the population by index.""" return self.pop[idx].copy()
[docs] def evaluate(self) -> None: """Evaluates and sorts all individuals in the population from best to worst. """ evaluation_func = lambda i:i.evaluate(self.optimization_func, self.fitness) self.pop = sorted(self.pop, key=evaluation_func)
[docs] def scores(self) -> List[float]: """Returns a list of fitness scores of all individuals in the population.""" evaluation_func = lambda i:i.evaluate(self.optimization_func, self.fitness) score_mapping = map(evaluation_func, self.pop) return list(score_mapping)
[docs] def mutate(self, mutant_pop:Union[None, PopulationType]=None) -> None: """Performs random mutations on the population or a given subset. Args: mutant_pop (Optional): A list of individuals to mutate, generally a subset of the population. """ if mutant_pop is not None: for individual in mutant_pop: individual.mutate() else: for individual in self.pop: individual.mutate()
[docs] def crossover(self, crossover_rate:Union[int, float]) -> None: """Performs crossovers for a set of individuals in the population. Given a crossover rate, performs crossovers for a set of randomly selected individuals in the population. To avoid the population's score deprecating due to undesired crossovers, the best parent and the best child are kept (instead of removing both parents and adding both children). Args: crossover_rate (Union[int, float]): The crossover rate (percentage). This is the number of individuals in the population subject to crossovers. """ parent_size = int(crossover_rate * len(self.pop)) if parent_size%2 ==1: parent_size += 1 pop_idx = list(range(len(self.pop))) parents = np.random.choice(pop_idx, parent_size, replace=False) male = [] female = [] for i, parent in enumerate(sorted(parents, reverse=True)): if i < int(parent_size/2): male.append(self.pop.pop(parent)) else: female.append(self.pop.pop(parent)) for i in range(len(male)): male_score = male[i].evaluate(self.optimization_func, self.fitness) female_score = female[i].evaluate(self.optimization_func, self.fitness) if male_score < female_score: best = male[i] else: best = female[i] self.pop.append(best) children = [] for i in range(int(parent_size/2)): children.append(self._cross(male[i], female[i])) self.pop += children
def _cross(self, parent1: Individual, parent2: Individual) -> Individual: """Performs crossovers between two individuals. Args: parent1 (Individual): The first parent used in the crossover. parent2 (Individual): The second parent used in the crossover. Returns: best_child (Individual): The best of the two children generated by the crossover. """ keys = parent1.genotype.keys() # crossing over of the entire genotype makes no sense position = np.random.choice(list(range(len(keys)))[1:]) child1 = Individual(self.indiv_shape) child2 = Individual(self.indiv_shape) for i, key in enumerate(keys): if i < position: child1.genotype[key] = parent1.genotype[key].copy() child2.genotype[key] = parent2.genotype[key].copy() else: child1.genotype[key] = parent2.genotype[key].copy() child2.genotype[key] = parent1.genotype[key].copy() child1_score = child1.evaluate(self.optimization_func, self.fitness) child2_score = child2.evaluate(self.optimization_func, self.fitness) if child1_score < child2_score: best_child = child1 else: best_child = child2 return best_child def _get_elites(self) -> PopulationType: """Returns the elites of the population. Returns the highest performing individuals in the population. The percentage of elites can be adjusted in the configuration. """ elites = self.pop[:int(self.elitism*self.pop_size)] return elites
[docs] def show(self) -> None: """Displays the populations 5 best and worst performing individuals.""" for individual in self.pop[:5]: indiv_score = individual.evaluate(self.optimization_func, self.fitness) indiv_info = (f"fit: {indiv_score} \n"+ "indiv: \n"+ "{individual.display()}") print(indiv_info) print("...\n") for individual in self.pop[-5:]: indiv_score = individual.evaluate(self.optimization_func, self.fitness) indiv_info = (f"fit: {indiv_score} \n"+ "indiv: \n"+ "{individual.display()}") print(indiv_info)