Source code for saealib.algorithms.ga

"""Genetic Algorithm module."""

from __future__ import annotations

import math
from collections.abc import Callable
from typing import TYPE_CHECKING

import numpy as np

from saealib.algorithms.base import Algorithm
from saealib.callback import PostAskEvent, PostCrossoverEvent, PostMutationEvent
from saealib.context import OptimizationContext
from saealib.operators.repair import repair_clipping
from saealib.population import Archive, Population, PopulationAttribute
from saealib.problem import Problem

if TYPE_CHECKING:
    from saealib.operators.crossover import Crossover
    from saealib.operators.mutation import Mutation
    from saealib.operators.selection import ParentSelection, SurvivorSelection
    from saealib.optimizer import ComponentProvider

RepairFunc = Callable[[np.ndarray, tuple[np.ndarray, np.ndarray]], np.ndarray]


[docs] class GA(Algorithm): """ Genetic Algorithm class. Attributes ---------- crossover : Crossover Crossover operator. mutation : Mutation Mutation operator. parent_selection : ParentSelection Parent selection operator. survivor_selection : SurvivorSelection Survivor selection operator. """
[docs] def __init__( self, crossover: Crossover, mutation: Mutation, parent_selection: ParentSelection, survivor_selection: SurvivorSelection, repair: RepairFunc | None = repair_clipping, ): """ Initialize GA (Genetic Algorithm) class. Parameters ---------- crossover : Crossover Crossover operator. mutation : Mutation Mutation operator. parent_selection : ParentSelection Parent selection operator. survivor_selection : SurvivorSelection Survivor selection operator. repair : RepairFunc or None, optional Repair function applied after crossover and after mutation. Defaults to ``repair_clipping``. Pass ``None`` to disable repair. """ super().__init__() self.crossover = crossover self.mutation = mutation self.parent_selection = parent_selection self.survivor_selection = survivor_selection self.repair = repair
[docs] def get_required_attrs(self, problem: Problem) -> list[PopulationAttribute]: """Return algorithm-specific attributes (GA needs none beyond the defaults).""" return []
@property def population_class(self): """Return the population class.""" return Population @property def archive_class(self): """Return the archive class.""" return Archive
[docs] def ask( self, ctx: OptimizationContext, provider: ComponentProvider, n_offspring: int | None = None, ) -> Population: """ Generate offspring via crossover and mutation. Parameters ---------- ctx : OptimizationContext Current optimization context. provider : ComponentProvider Component provider. n_offspring : int or None, optional Number of offspring. Defaults to the current population size. Returns ------- Population """ cand = np.empty((0, ctx.dim)) pop = ctx.population.get_array("x") popsize = len(pop) target = n_offspring if n_offspring is not None else popsize lb = ctx.problem.lb ub = ctx.problem.ub n_pair = math.ceil(target / 2) parent_idx_m = ( self.parent_selection.select( ctx, ctx.population, n_pair=n_pair, n_parents=self.crossover.n_parents, rng=ctx.rng, ) % popsize ) for i in range(n_pair): parent = pop[parent_idx_m[i]] if ctx.rng.random() < self.crossover.crossover_rate: c = self.crossover.crossover(parent, rng=ctx.rng) else: c = parent.copy() cand = np.vstack((cand, c)) if self.repair is not None: cand = self.repair(cand, (lb, ub)) post_co = PostCrossoverEvent(ctx=ctx, provider=provider, candidates=cand) provider.dispatch(post_co) cand = post_co.candidates cand_len = len(cand) for i in range(cand_len): cand[i] = self.mutation.mutate(cand[i], (lb, ub), rng=ctx.rng) if self.repair is not None: cand = self.repair(cand, (lb, ub)) post_mut = PostMutationEvent(ctx=ctx, provider=provider, candidates=cand) provider.dispatch(post_mut) cand = post_mut.candidates post_ask = PostAskEvent(ctx=ctx, provider=provider, candidates=cand) provider.dispatch(post_ask) cand = post_ask.candidates cand_pop = ctx.population.empty_like(capacity=target) cand_pop.extend({"x": cand[:target]}) return cand_pop
[docs] def tell( self, ctx: OptimizationContext, provider: ComponentProvider, offspring: Population, ): """ Update the population using (μ+λ) survivor selection. Parameters ---------- ctx : OptimizationContext Current optimization context. provider : ComponentProvider Component provider. offspring : Population Offspring population. """ popsize = len(ctx.population) pool = ctx.population.empty_like(capacity=popsize + len(offspring)) pool.extend(ctx.population) pool.extend(offspring) survivor_idx = self.survivor_selection.select(ctx, pool, popsize) ctx.population.clear() ctx.population.extend(pool.extract(survivor_idx))