Source code for sigmaepsilon.math.optimize.bitchromosome

"""Shared base class for bit-chromosome-encoded genetic algorithms."""

import numpy as np
from numpy import ndarray

from .ga import GeneticAlgorithm

__all__ = ["BitChromosomeGeneticAlgorithm"]


[docs] class BitChromosomeGeneticAlgorithm(GeneticAlgorithm): """Shared base for genetic algorithms whose genotype is a flat 0/1 bit chromosome. The chromosome has length ``dim * length``, decoded into ``dim`` scalar phenotypes by linearly scaling each variable's ``length``-bit segment into its ``ranges[d]`` bounds. This class provides the representation-specific machinery (:func:`populate`, :func:`crossover`, :func:`mutate`, :func:`decode`) for any bit-chromosome GA. What varies between concrete subclasses is only how the linearly-decoded scalar is interpreted, which is controlled by the :func:`_postprocess_phenotypes` hook: - :class:`~sigmaepsilon.math.optimize.bga.BinaryGeneticAlgorithm` keeps the continuous, floating point value (suitable for real-valued, box-constrained optimization). - :class:`~sigmaepsilon.math.optimize.iga.IntegerGeneticAlgorithm` rounds it to the nearest integer (suitable for boolean or small-integer-domain decision variables). .. note:: This class is not meant to be instantiated directly; use one of its concrete subclasses instead. """ __slots__ = () def populate(self, genotypes: ndarray | None = None) -> ndarray: """Populate the model and return the array of genotypes.""" nPop = self.nPop if genotypes is None: poolshape = (int(nPop / 2), self.dim * self.length) genotypes = self.rng.integers(2, size=poolshape) else: poolshape = genotypes.shape nParent = poolshape[0] if nParent < nPop: offspring = [] g = self.random_parents_generator(genotypes) try: while (len(offspring) + nParent) < nPop: parent1, parent2 = next(g) offspring.extend(self.crossover(parent1, parent2)) genotypes = np.vstack([genotypes, offspring]) except Exception: # pragma: no cover raise RuntimeError return genotypes def decode(self, genotypes: ndarray) -> ndarray: """Decode the genotypes to phenotypes and return them as an array.""" span = 2**self.length - 2**0 genotypes = genotypes.reshape((self.nPop, self.dim, self.length)) precisions = [ (self.ranges[d, -1] - self.ranges[d, 0]) / span for d in range(self.dim) ] phenotypes = np.sum( [genotypes[:, :, i] * 2**i for i in range(self.length)], axis=0 ).astype(float) for d in range(self.dim): phenotypes[:, d] *= precisions[d] phenotypes[:, d] += self.ranges[d, 0] return self._postprocess_phenotypes(phenotypes) def _postprocess_phenotypes(self, phenotypes: ndarray) -> ndarray: """Transform the linearly-decoded phenotypes for subclasses. This is a hook that transforms the continuous phenotypes into the representation subclasses actually want to expose (e.g. rounded to the nearest integer). The default implementation is the identity (continuous). """ return phenotypes def crossover( self, parent1: ndarray, parent2: ndarray, nCut: int | None = None ) -> tuple[ndarray, ndarray]: """Perform crossover on the parents. Crosses `parent1` and `parent2` using an `nCut` number of cuts and returns two children. """ if self.rng.random() > self.p_c: # pragma: no cover return parent1, parent2 if nCut is None: nCut = self.rng.integers(1, self.dim * self.length - 1) cuts = [0, self.dim * self.length] p = self.rng.choice( np.arange(1, self.length * self.dim - 1), nCut, replace=False ) cuts.extend(p) cuts = np.sort(cuts) child1 = np.zeros(self.dim * self.length, dtype=int) child2 = np.zeros(self.dim * self.length, dtype=int) randBool = self.rng.random() > 0.5 for i in range(nCut + 1): if (i % 2 == 0) == randBool: child1[cuts[i] : cuts[i + 1]] = parent1[cuts[i] : cuts[i + 1]] child2[cuts[i] : cuts[i + 1]] = parent2[cuts[i] : cuts[i + 1]] else: child1[cuts[i] : cuts[i + 1]] = parent2[cuts[i] : cuts[i + 1]] child2[cuts[i] : cuts[i + 1]] = parent1[cuts[i] : cuts[i + 1]] return self.mutate(child1), self.mutate(child2) def mutate(self, child: ndarray) -> ndarray: """Return a mutated genotype. Children come in, mutants go out.""" p = self.rng.random(self.dim * self.length) return np.where(p > self.p_m, child, 1 - child)