Source code for metaheuristic_designer.operators.branch_operator

"""
Operator that randomly applies one operator from a list to each individual.
"""

from __future__ import annotations
from typing import Iterable, Optional
import enum
from enum import Enum
from copy import copy
import numpy as np

from ..encoding import Encoding
from ..initializer import Initializer
from ..population import Population
from ..utils import RNGLike
from ..operator import Operator


[docs] class BranchOpMethods(Enum): RANDOM = enum.auto() PICK = enum.auto()
[docs] @staticmethod def from_str(str_input: str) -> BranchOpMethods: str_input = str_input.lower() if str_input not in branch_ops_map: raise ValueError(f'Operator on operators "{str_input}" not defined') return branch_ops_map[str_input]
branch_ops_map = {"random": BranchOpMethods.RANDOM, "rand": BranchOpMethods.RANDOM, "pick": BranchOpMethods.PICK, "choose": BranchOpMethods.PICK}
[docs] class BranchOperator(Operator): """Operator that stochastically selects among several operators. For each individual, one operator from `op_list` is chosen according to the configured method (random with given probability, or manually picked). This allows e.g. applying mutation with a certain probability while leaving the rest untouched. Parameters ---------- op_list : list of Operator The candidate operators. method : str, optional Branching method, ``"random"`` or ``"pick"`` (default ``"random"``). name : str, optional Display name; defaults to ``"method(op_names)"``. encoding : Encoding, optional Encoding applied to the genotype. random_state : RNGLike, optional Random number generator. idx : int, optional Index of the operator to use when method is ``"pick"`` (default -1). p : float, optional Probability of selecting the first operator (default 0.5). The second operator (usually :class:`NullOperator`) gets probability ``1 - p``. **kwargs Additional keyword arguments stored as schedulable parameters. """ def __init__( self, op_list: Iterable[Operator], method: str = None, name: str = None, encoding: Optional[Encoding] = None, random_state: Optional[RNGLike] = None, idx: int = -1, p: float = 0.5, **kwargs, ): self.op_list = op_list if name is None: op_names = [] for op in op_list: if not isinstance(op, Operator): op_names.append("lambda_func") else: op_names.append(op.name) joined_names = ", ".join(op_names) name = f"{method}({joined_names})" super().__init__(name=name, encoding=encoding, random_state=random_state, p=p, **kwargs) if method is None: self.method = BranchOpMethods.RANDOM else: self.method = BranchOpMethods.from_str(method) self.chosen_idx = idx self.weights = np.array([self.params.p, 1 - self.params.p])
[docs] def gather_params(self) -> dict: """Collect parameters from this operator and all sub-operators. Returns ------- dict Flat dictionary with dotted keys. """ all_params = self.get_params() for op in self.op_list: all_params.update(op.gather_params()) return all_params
[docs] def evolve(self, population: Population, initializer: Optional[Initializer] = None) -> Population: """Apply a random operator to each individual according to the branch method. Parameters ---------- population : Population The current population. initializer : Initializer, optional The population initializer. Returns ------- Population The modified population. """ new_population = copy(population) if self.method == BranchOpMethods.RANDOM: self.chosen_idx = self.random_state.choice(range(len(self.op_list)), size=(population.population_size,), replace=True, p=self.weights) if isinstance(self.chosen_idx, np.ndarray) and self.chosen_idx.ndim > 0: chosen_idx = self.chosen_idx else: chosen_idx = np.asarray([self.chosen_idx] * len(population)) for idx, op in enumerate(self.op_list): split_mask = chosen_idx == idx if np.any(split_mask): split_population = population.take_selection(split_mask) split_population = op.evolve(split_population, initializer) new_population = new_population.apply_selection(split_population, split_mask) return new_population
[docs] def choose_index(self, idx: int): """ Manually chooses the operator to use next Parameters ---------- idx : int Index of the operator in the list. """ self.chosen_idx = idx
[docs] def step(self, progress: float): """Update schedulable parameters and propagate to sub-operators. Parameters ---------- progress : float Current progress of the algorithm (0-1). """ super().step(progress) for op in self.op_list: if isinstance(op, Operator): op.step(progress) self.weights = np.array([self.params.p, 1 - self.params.p])
[docs] def get_state(self) -> dict: data = super().get_state() data["op_list"] = [] for op in self.op_list: if isinstance(op, Operator): data["op_list"].append(op.get_state()) else: data["op_list"].append("lambda_func") return data