Source code for lightweight_genetic_algorithm.population

from abc import ABC, abstractmethod
import numpy as np

[docs] class Gene(ABC): """ Abstract base class for a gene. Each subclass defines a gene in a specific genotype space. Attributes ---------- mutation_methods : list of str The mutation methods that can be applied to the gene. Methods ------- random_initialization() Provides a random value appropriate for the gene. set_value() Sets the value of the gene. """ mutation_methods = []
[docs] @abstractmethod def random_initialization(self): """ Provides a random value appropriate for the gene. Returns ------- value A random value suitable for initializing the gene. """ pass
[docs] @abstractmethod def set_value(self): """ Sets the value of the gene. Note ---- Implementation should define how the value is set. """ pass
[docs] class NumericGene(Gene): """ A numeric gene represented by a real number within a range. Parameters ---------- gene_range : tuple of float The range (low, high) of the gene values. value : float, optional The value of the gene. If not provided, the gene will be initialized with a random value. Attributes ---------- low : float The lower bound of the gene values. high : float The upper bound of the gene values. value : float The current value of the gene. Methods ------- get_gene_range() Returns the gene range as a tuple (low, high). random_initialization() Generates and returns a random value within the gene range. set_value(value) Sets the value of the gene to the specified value. copy() Creates and returns a copy of the gene. """ mutation_methods = ["additive", "multiplicative", "random"] crossover_methods = ["between", "midpoint", "either or"] def __init__(self, gene_range, value=None): self.low, self.high = gene_range self.value = value if value is not None else self.random_initialization()
[docs] def get_gene_range(self): """ Returns the gene range. Returns ------- tuple of float A tuple (low, high) representing the gene range. """ return (self.low, self.high)
[docs] def random_initialization(self): """ Generates and returns a random value within the gene range. Returns ------- float A random value within the gene range. """ return np.random.uniform(low=self.low, high=self.high)
[docs] def set_value(self, value): """ Sets the value of the gene to the specified value. Parameters ---------- value : float The new value for the gene. """ self.value = value
[docs] def copy(self): """ Creates and returns a copy of the gene. Returns ------- NumericGene A new instance of NumericGene with the same range and value. """ return NumericGene((self.low, self.high), self.value)
[docs] class CategoricalGene(Gene): """ A categorical gene that can take any value from a set of categories. Parameters ---------- categories : list The allowed categories for the gene. value : object, optional The value of the gene. Must be one of the allowed categories. If not provided, the gene will be initialized with a random value. Attributes ---------- categories : list The allowed categories for the gene. value : object The current value of the gene. Methods ------- random_initialization() Selects and returns a random value from the categories. set_value(value) Sets the value of the gene to the specified value. copy() Creates and returns a copy of the gene. Raises ------ ValueError If the provided `value` is not in the allowed categories. """ mutation_methods = ["categorical"] crossover_methods = ["either or"] def __init__(self, categories, value=None): self.categories = categories if value is not None and value not in self.categories: raise ValueError("A categorical gene is being set to a value not in the allowed categories.") self.value = value if value is not None else self.random_initialization()
[docs] def random_initialization(self): """ Selects and returns a random value from the categories. Returns ------- object A random value from the allowed categories. """ return np.random.choice(self.categories)
[docs] def set_value(self, value): """ Sets the value of the gene to the specified value. Parameters ---------- value : object The new value for the gene. Raises ------ ValueError If the provided `value` is not in the allowed categories. """ if value not in self.categories: raise ValueError("A categorical gene is being set to a value not in the allowed categories.") else: self.value = value
[docs] def copy(self): """ Creates and returns a copy of the gene. Returns ------- CategoricalGene A new instance of CategoricalGene with the same categories and value. """ return CategoricalGene(self.categories, self.value)
[docs] class Individual: """ Represents an individual in the population, defined by its genes. Parameters ---------- genes : list of Gene The genes that define the individual. fitness_function : callable The fitness function used to calculate the fitness of the individual. The function should take a list of gene values as its first argument and return a scalar value. fitness_function_args : tuple Additional arguments for the fitness function. fitness : float, optional The fitness of the individual. If not provided, the fitness function will be evaluated. This allows avoiding redundant evaluations of the fitness function. Attributes ---------- genes : numpy.ndarray An array containing the genes of the individual. genes_values : numpy.ndarray An array containing the values of the genes. fitness_function : callable The fitness function used to calculate the fitness of the individual. fitness_function_args : tuple Additional arguments for the fitness function. fitness : float The fitness of the individual. Methods ------- get_genes() Returns a copy of the genes. get_gene_values() Returns a copy of the gene values. get_fitness_function() Returns the fitness function used by the individual. copy() Creates and returns a copy of the individual. Raises ------ ValueError If the fitness function evaluation fails, indicating incompatibility with the individual's genes. """ def __init__(self, genes, fitness_function, fitness_function_args, fitness=None): self.genes = np.array( [gene.copy() for gene in genes] ) self.genes_values = np.array([gene.value for gene in self.genes]) self.fitness_function = fitness_function self.fitness_function_args = fitness_function_args if fitness is None: try: self.fitness = fitness_function(self.genes_values, *self.fitness_function_args) except Exception: raise ValueError("Error in fitness function evaluation. Your fitness function does not seem to be compatible with your individuals.") else: self.fitness = fitness
[docs] def get_genes(self): """ Returns a copy of the genes. Returns ------- numpy.ndarray An array containing copies of the individual's genes. """ return np.array([gene.copy() for gene in self.genes])
[docs] def get_gene_values(self): """ Returns a copy of the gene values. Returns ------- numpy.ndarray An array containing the values of the individual's genes. """ return self.genes_values.copy()
[docs] def get_fitness_function(self): """ Returns the fitness function used by the individual. Returns ------- callable The fitness function. """ return self.fitness_function
[docs] def copy(self): """ Creates and returns a copy of the individual. Returns ------- Individual A new Individual instance with the same genes and fitness. """ return Individual(self.get_genes(), self.fitness_function, self.fitness_function_args, fitness=self.fitness)