Source code for metaheuristic_designer.operators.operator_functions.mutation

"""
Mutation operator implementations based on probability distributions.
"""

import logging
from typing import Optional
from ...utils import MatrixLike, RNGLike, VectorLike, ScalarLike, check_random_state
import numpy as np
from .probability_distributions_factory import create_prob_distribution

logger = logging.getLogger(__name__)


[docs] def mutate_sample( population_matrix: MatrixLike, fitness_array: VectorLike, distribution: str, N: int, random_state: Optional[RNGLike] = None, **kwargs ) -> MatrixLike: """ Replace `N` components of each individual with random values. The new values are sampled from the probability distribution specified by `distribution`. The remaining components are left unchanged. Parameters ---------- population_matrix : MatrixLike Population of shape ``(N_indiv, N_comp)``. fitness_array : VectorLike Fitness values (unused; kept for interface consistency). distribution : str Key of the distribution to use (see :func:`create_prob_distribution`). N : int Number of components to resample per individual. random_state : RNGLike, optional Random number generator. **kwargs Forwarded to :func:`create_prob_distribution` (e.g. ``loc``, ``scale``). Returns ------- MatrixLike The modified population. """ random_state = check_random_state(random_state) population_size, n_components = population_matrix.shape distribution = create_prob_distribution(distribution, population_matrix, **kwargs) mask_pos = np.tile(np.arange(n_components) < N, (population_size, 1)) mask_pos = random_state.permuted(mask_pos, axis=1) rand_samples = distribution.sample(population_matrix.shape, random_state) population_matrix[mask_pos] = rand_samples[mask_pos] logger.debug("Resampled components of the vector %s, with mask %s", population_matrix[mask_pos], mask_pos.astype(int)) return population_matrix
[docs] def mutate_noise( population_matrix: MatrixLike, fitness_array: VectorLike, distribution: str, F: ScalarLike | VectorLike, N: int, random_state: Optional[RNGLike] = None, **kwargs ) -> MatrixLike: """ Add random noise to `N` components of each individual. The noise is drawn from `distribution`, multiplied by the strength factor `F`, and added to the selected components. Parameters ---------- population_matrix : MatrixLike Population of shape ``(N_indiv, N_comp)``. fitness_array : VectorLike Fitness values (unused). distribution : str Key of the distribution to use. F : ScalarLike | VectorLike Strength factor (scalar or per-individual array). N : int Number of components to mutate per individual. random_state : RNGLike, optional Random number generator. **kwargs Forwarded to :func:`create_prob_distribution`. Returns ------- MatrixLike The mutated population. """ random_state = check_random_state(random_state) population_size, n_components = population_matrix.shape distribution = create_prob_distribution(distribution, population_matrix, **kwargs) mask_pos = np.tile(np.arange(n_components) < N, (population_size, 1)) mask_pos = random_state.permuted(mask_pos, axis=1) rand_samples = distribution.sample(population_matrix.shape, random_state) population_matrix[mask_pos] = population_matrix[mask_pos] + (F * rand_samples)[mask_pos] logger.debug( "Mutated components of the vector:\nvector = %s\nnoise_added = %s\nmask = %s", population_matrix[mask_pos], (F * rand_samples)[mask_pos], mask_pos.astype(int), ) return population_matrix
[docs] def rand_sample( population_matrix: MatrixLike, fitness_array: VectorLike, distribution: str, random_state: Optional[RNGLike] = None, **kwargs ) -> MatrixLike: """ Replace the entire population with new random values. Each element of the genotype matrix is independently resampled from `distribution`. Parameters ---------- population_matrix : MatrixLike Population of shape ``(N_indiv, N_comp)``. Only its shape is used. fitness_array : VectorLike Fitness values (unused). distribution : str Key of the distribution to use. random_state : RNGLike, optional Random number generator. **kwargs Forwarded to :func:`create_prob_distribution`. Returns ------- MatrixLike A new matrix of the same shape filled with random samples. """ random_state = check_random_state(random_state) distribution = create_prob_distribution(distribution, population_matrix, **kwargs) rand_samples = distribution.sample(population_matrix.shape, random_state) logger.debug("Resampled vector %s", rand_samples) return rand_samples
[docs] def rand_noise( population_matrix: MatrixLike, fitness_array: VectorLike, distribution: str, F: ScalarLike, random_state: Optional[RNGLike] = None, **kwargs ) -> MatrixLike: """ Add random noise to the entire population. The noise is drawn from `distribution`, scaled by `F`, and added to every element of the genotype matrix. Parameters ---------- population_matrix : MatrixLike Population of shape ``(N_indiv, N_comp)``. fitness_array : VectorLike Fitness values (unused). distribution : str Key of the distribution to use. F : ScalarLike Strength factor (scalar or per-individual array). random_state : RNGLike, optional Random number generator. **kwargs Forwarded to :func:`create_prob_distribution`. Returns ------- MatrixLike Noisy population of the same shape. """ random_state = check_random_state(random_state) distribution = create_prob_distribution(distribution, population_matrix, **kwargs) rand_samples = distribution.sample(population_matrix.shape, random_state) result = population_matrix + F * rand_samples logger.debug("Added noise to vector %s", result) return result
[docs] def sample_1_sigma( population_matrix: MatrixLike, fitness_array: VectorLike, random_state: Optional[RNGLike] = None, **kwargs ) -> MatrixLike: """ Replace `n` components using a log-normal perturbation with a stored sigma value. This is a self-adaptation helper for Evolution Strategies. The sigma values are expected to be passed in ``kwargs``. Parameters ---------- population_matrix : MatrixLike Population. fitness_array : VectorLike Fitness values (unused). random_state : RNGLike, optional Random number generator. **kwargs Must contain ``epsilon``, ``sigma``, ``tau``, ``n``. Returns ------- MatrixLike The mutated population. """ random_state = check_random_state(random_state) epsilon = kwargs["epsilon"] sigma = kwargs["sigma"] tau = kwargs["tau"] n = kwargs["n"] mask_pos = np.tile(np.arange(population_matrix.shape[1]) < n, (population_matrix.shape[0], 1)) mask_pos = random_state.permuted(mask_pos, axis=1) sampled = np.maximum(epsilon, population_matrix * np.exp(tau * random_state.normal(0, 1, sigma.shape[0]))) population_matrix[mask_pos] = sampled[mask_pos] return population_matrix
[docs] def mutate_1_sigma( population_matrix: MatrixLike, fitness_array: VectorLike, random_state: Optional[RNGLike] = None, **kwargs ) -> MatrixLike: """ Mutate a single sigma value using a log-normal update. The new sigma is ``max(epsilon, old_sigma * exp(tau * N(0,1)))``. Parameters ---------- population_matrix : MatrixLike Current sigma values (one per individual, or per dimension). fitness_array : VectorLike Fitness values (unused). random_state : RNGLike, optional Random number generator. **kwargs Must contain ``epsilon`` and ``tau``. Returns ------- MatrixLike Updated sigma values. """ random_state = check_random_state(random_state) epsilon = kwargs["epsilon"] tau = kwargs["tau"] return np.maximum(epsilon, population_matrix * np.exp(tau * random_state.normal(0, 1, population_matrix.shape[0])[:, None]))
[docs] def mutate_n_sigmas( population_matrix: MatrixLike, fitness_array: VectorLike, random_state: Optional[RNGLike] = None, **kwargs ) -> MatrixLike: """ Mutate multiple sigma values with global and local learning rates. ``max(epsilon, old_sigma * exp(tau*N(0,1) + tau_multiple*N(0,1)))``. Parameters ---------- population_matrix : MatrixLike Current sigma values. fitness_array : VectorLike Fitness values (unused). random_state : RNGLike, optional Random number generator. **kwargs Must contain ``epsilon``, ``tau``, ``tau_multiple``. Returns ------- MatrixLike Updated sigma values. """ random_state = check_random_state(random_state) epsilon = kwargs["epsilon"] tau = kwargs["tau"] tau_multiple = kwargs["tau_multiple"] return np.maximum( epsilon, population_matrix * np.exp( tau * random_state.normal(0, 1, population_matrix.shape[0])[:, None] + tau_multiple * random_state.normal(0, 1, population_matrix.shape[0])[:, None] ), )
[docs] def xor_mask( population_matrix: MatrixLike, fitness_array: VectorLike, N: int, mode: str = "byte", random_state: Optional[RNGLike] = None, **kwargs ) -> MatrixLike: """ Apply bitwise XOR with random masks to `N` components per individual. The mask is drawn as random bytes, integers, or single bits depending on `mode`. Parameters ---------- population_matrix : MatrixLike Population. fitness_array : VectorLike Fitness values (unused). N : int Number of components to mask per individual. mode : str, optional Mask format: ``"bin"``, ``"byte"``, or ``"int"`` (default ``"byte"``). random_state : RNGLike, optional Random number generator. Returns ------- MatrixLike The masked population. """ random_state = check_random_state(random_state) population_size, n_components = population_matrix.shape mask_pos = np.tile(np.arange(n_components) < N, (population_size, 1)) mask_pos = random_state.permuted(mask_pos, axis=1) match mode: case "bin": mask = mask_pos case "byte": mask = random_state.integers(1, 0xFF, size=population_matrix.shape) * mask_pos case "int": mask = random_state.integers(1, 0xFFFF, size=population_matrix.shape) * mask_pos case _: mask = 0 return population_matrix ^ mask