"""
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