Source code for pymoo.core.survival

"""Survival selection strategies for filtering populations."""

from abc import abstractmethod
from typing import Any, Optional, Tuple

import numpy as np

from pymoo.core.population import Population
from pymoo.util import default_random_state


[docs] class Survival: """Base class for survival selection operators.""" def __init__(self, filter_infeasible: bool = True) -> None: """Initialize survival operator. Args: filter_infeasible: Whether to separate feasible from infeasible solutions. """ super().__init__() self.filter_infeasible = filter_infeasible @default_random_state def do( self, problem: Any, pop: Any, *args: Any, n_survive: Optional[int] = None, random_state: Any = None, return_indices: bool = False, **kwargs: Any, ) -> Any: """Select survivors from population. Args: problem: Problem instance. pop: Population to filter. n_survive: Number of survivors to select. random_state: Random state for reproducibility. return_indices: Whether to return indices instead of individuals. args: Additional arguments. kwargs: Additional keyword arguments. Returns: Surviving individuals or their indices. """ # make sure the population has at least one individual if len(pop) == 0: return pop if n_survive is None: n_survive = len(pop) n_survive = min(n_survive, len(pop)) # if the split should be done beforehand if self.filter_infeasible and problem.has_constraints(): # split feasible and infeasible solutions feas, infeas = split_by_feasibility(pop, sort_infeas_by_cv=True) if len(feas) == 0: survivors = Population() else: survivors = self._do( problem, pop[feas], *args, n_survive=min(len(feas), n_survive), random_state=random_state, **kwargs, ) # calculate how many individuals are still remaining to be filled up with infeasible ones n_remaining = n_survive - len(survivors) # if infeasible solutions needs to be added if n_remaining > 0: survivors = Population.merge(survivors, pop[infeas[:n_remaining]]) else: survivors = self._do( problem, pop, *args, n_survive=n_survive, random_state=random_state, **kwargs, ) if return_indices: h = {} # noqa: I for k, ind in enumerate(pop): h[ind] = k return [h[survivor] for survivor in survivors] else: return survivors @abstractmethod def _do( self, problem: Any, pop: Any, *args: Any, n_survive: Optional[int] = None, random_state: Any = None, **kwargs: Any, ) -> Any: """Perform survival selection (to be implemented by subclasses)."""
class ToReplacement(Survival): """Survival operator that replaces individuals with better offspring.""" def __init__(self, survival: Survival) -> None: """Initialize replacement operator. Args: survival: Survival operator to use for ranking. """ super().__init__(False) self.survival = survival def _do( # type: ignore[override] self, problem: Any, pop: Any, off: Any, random_state: Any = None, **kwargs: Any, ) -> Any: """Perform replacement.""" merged = Population.merge(pop, off) indices = self.survival.do( problem, merged, n_survive=len(merged), return_indices=True, random_state=random_state, **kwargs, ) merged.set("__rank__", indices) for k in range(len(pop)): if off[k].get("__rank__") < pop[k].get("__rank__"): pop[k] = off[k] return pop def split_by_feasibility( pop: Any, sort_infeas_by_cv: bool = True, sort_feas_by_obj: bool = False, return_pop: bool = False, ) -> Tuple[Any, ...]: """Split population into feasible and infeasible individuals. Args: pop: Population to split. sort_infeas_by_cv: Whether to sort infeasible by constraint violation. sort_feas_by_obj: Whether to sort feasible by objective. return_pop: Whether to return populations instead of indices. Returns: Feasible and infeasible indices/populations. """ F, CV, b = pop.get("F", "CV", "FEAS") feasible = np.where(b)[0] infeasible = np.where(~b)[0] if sort_infeas_by_cv: infeasible = infeasible[np.argsort(CV[infeasible, 0])] if sort_feas_by_obj: feasible = feasible[np.argsort(F[feasible, 0])] if not return_pop: return feasible, infeasible else: return feasible, infeasible, pop[feasible], pop[infeasible]