Genetic Operators
Genetic operators are the core of any genetic algorithm; they are responsible for defining how individuals evolve across generations. They are formal mathematical operators defined on populations of candidate solutions, used to generate new individuals by recombining or perturbing existing ones.
Mutation
Mutation is a unary genetic operator that introduces random perturbations into an individual’s representation. By randomly flipping, tweaking or replacing genes, mutation maintains population diversity and enables exploration of new regions in the search space.
A mutation operator in moors
is any type that implements the MutationOperator trait. For example:
use ndarray::ArrayViewMut1;
use crate::{operators::MutationOperator, random::RandomGenerator};
#[derive(Debug, Clone)]
/// Mutation operator that flips bits in a binary individual with a specified mutation rate.
pub struct BitFlipMutation {
pub gene_mutation_rate: f64,
}
impl BitFlipMutation {
pub fn new(gene_mutation_rate: f64) -> Self {
Self { gene_mutation_rate }
}
}
impl MutationOperator for BitFlipMutation {
fn mutate<'a>(&self, mut individual: ArrayViewMut1<'a, f64>, rng: &mut impl RandomGenerator) {
for gene in individual.iter_mut() {
if rng.gen_bool(self.gene_mutation_rate) {
*gene = if *gene == 0.0 { 1.0 } else { 0.0 };
}
}
}
}
The main method to implement is mutate
, which operates at the individual level using an ndarray::ArrayViewMut1
. Predefined mutation operators include:
Mutation Operator | Description |
---|---|
BitFlipMutation | Randomly flips one or more bits in the binary representation, introducing small variations. |
GaussianMutation | Adds Gaussian noise to each real-valued gene to locally explore the continuous solution space. |
ScrambleMutation | Selects a subsequence and randomly shuffles it, preserving the original elements but altering their order. |
SwapMutation | Swaps the positions of two randomly chosen genes to explore neighboring permutations. |
DisplacementMutation | Extracts a block of the permutation and inserts it at another position, preserving the block’s relative order. |
UniformRealMutation | Resets a real-valued gene based on a uniform distribution. |
UniformBinaryMutation | Resets a bit to a random 0 or 1 . |
A mutation operator in pymoors
is just a class that defines the operate
method:
from pymoors.typing import TwoDArray
class BitFlipMutation:
def __init__(self, gene_mutation_rate: float = 0.5):
self.gene_mutation_rate = gene_mutation_rate
def operate(
self,
population: TwoDArray,
) -> TwoDArray:
mask = np.random.random(population.shape) < self.gene_mutation_rate
population[mask] = 1.0 - population[mask]
return population
operate
acts at poblational level, as usual it means that it takes a 2D numpy array and returns 2D array too, where each row is the evaluation of a single individual.
There are many built-in mutation operators backed at the rust side
Mutation Operator | Description |
---|---|
pymoors.BitFlipMutation |
Randomly flips one or more bits in the binary representation, introducing small variations. |
pymoors.GaussianMutation |
Adds Gaussian noise to each real-valued gene to locally explore the continuous solution space. |
pymoors.ScrambleMutation |
Selects a subsequence and randomly shuffles it, preserving the original elements but altering their order. |
pymoors.SwapMutation |
Swaps the positions of two randomly chosen genes to explore neighboring permutations. |
pymoors.DisplacementMutation |
Extracts a block of the permutation and inserts it at another position, preserving the block’s relative order. |
pymoors.UniformRealMutation |
Resets a real-valued gene based on a uniform distribution. |
pymoors.UniformBinaryMutation |
Resets a bit to a random 0 or 1 . |
operate
at poblational level
in moors
we allow the user to define the crossover at individual or poblational level, but in pymoors
we force to
work with poblational level. Technical reason is that in each user defined operate
call we have to adquire python GIL
in the rust side, poblational call requires just 1 call to the GIL.
Crossover
Crossover is a binary genetic operator that combines genetic material from two parent individuals by exchanging segments of their representations, producing offspring that inherit traits from both parents. It promotes the exploration of new solution combinations while preserving useful building blocks.
A crossover operator in moors
is any type that implements the CrossoverOperator trait. For example:
use ndarray::{Array1, Axis, concatenate, s};
use crate::operators::CrossoverOperator;
use crate::random::RandomGenerator;
#[derive(Debug, Clone)]
/// Single-point crossover operator for binary-encoded individuals.
pub struct SinglePointBinaryCrossover;
impl CrossoverOperator for SinglePointBinaryCrossover {
fn crossover(
&self,
parent_a: &Array1<f64>,
parent_b: &Array1<f64>,
rng: &mut impl RandomGenerator,
) -> (Array1<f64>, Array1<f64>) {
let num_genes = parent_a.len();
// Select a crossover point between 1 and num_genes - 1
let crossover_point = rng.gen_range_usize(1, num_genes);
// Split parents at the crossover point and create offspring
let offspring_a = concatenate![
Axis(0),
parent_a.slice(s![..crossover_point]),
parent_b.slice(s![crossover_point..])
];
let offspring_b = concatenate![
Axis(0),
parent_b.slice(s![..crossover_point]),
parent_a.slice(s![crossover_point..])
];
(offspring_a, offspring_b)
}
}
The main method to implement is crossover
, which takes two parents (ndarray::Array1
) and produces two offspring. Predefined crossover operators include:
Crossover Operator | Description |
---|---|
ExponentialCrossover | For Differential Evolution: starts at a random index and copies consecutive genes from the mutant vector while a uniform random number is below the crossover rate, then fills remaining positions from the target vector. |
OrderCrossover | For permutations: copies a segment between two cut points from one parent, then fills the rest of the child with the remaining genes in the order they appear in the other parent. |
SimulatedBinaryCrossover | For real-valued vectors: generates offspring by sampling each gene from a distribution centered on parent values, mimicking the spread of single-point binary crossover in continuous space. |
SinglePointBinaryCrossover | Selects one crossover point and swaps the tails of two parents at that point to produce two offspring. |
UniformBinaryCrossover | For each bit position, randomly chooses which parent to inherit from (with a given probability), resulting in highly mixed offspring. |
TwoPointBinaryCrossover | Exchanges segments between two parents at two randomly chosen points to create offspring. |
ArithmeticCrossover | Exchanges segments between two parents at two randomly chosen points to create offspring. |
A crossover operator in pymoors
is just a class that defines the operate
method:
from pymoors.typing import TwoDArray
class SinglePointBinaryCrossover:
def operate(
self,
parents_a: TwoDArray,
parents_b: TwoDArray,
) -> TwoDArray:
n_pairs, n_genes = parents_a.shape
offsprings = np.empty((2 * n_pairs, n_genes), dtype=parents_a.dtype)
for i in range(n_pairs):
a = parents_a[i]
b = parents_b[i]
point = np.random.randint(1, n_genes)
c1 = np.concatenate((a[:point], b[point:]))
c2 = np.concatenate((b[:point], a[point:]))
offsprings[2 * i] = c1
offsprings[2 * i + 1] = c2
return offsprings
operate
acts at poblational level, as usual it means that it takes two parents as 2D numpy arrays and returns a single 2D array of length twice the number of crossovers (two children per crossover).
There are many built-in crossover operators backed at the rust side
Crossover Operator | Description |
---|---|
pymoors.ExponentialCrossover |
For Differential Evolution: starts at a random index and copies consecutive genes from the mutant vector while a uniform random number is below the crossover rate, then fills remaining positions from the target vector. |
pymoors.OrderCrossover |
For permutations: copies a segment between two cut points from one parent, then fills the rest of the child with the remaining genes in the order they appear in the other parent. |
pymoors.SimulatedBinaryCrossover |
For real-valued vectors: generates offspring by sampling each gene from a distribution centered on parent values, mimicking the spread of single-point binary crossover in continuous space. |
pymoors.SinglePointBinaryCrossover |
Selects one crossover point and swaps the tails of two parents at that point to produce two offspring. |
pymoors.UniformBinaryCrossover |
For each bit position, randomly chooses which parent to inherit from (with a given probability), resulting in highly mixed offspring. |
pymoors.TwoPointBinaryCrossover |
Exchanges segments between two parents at two randomly chosen points to create offspring. |
pymoors.ArithmeticCrossover |
Generates offspring by computing a weighted average of two parent solutions (for each gene, child = α·parent₁ + (1−α)·parent₂). |
operate
at poblational level
in moors
we allow the user to define the crossover at individual or poblational level, but in pymoors
we force to
work with poblational level. Technical reason is that in each user defined operate
call we have to adquire python GIL
in the rust side, poblational call requires just 1 call to the GIL.
Sampling
Sampling is a genetic operator that generates new individuals by drawing samples from a defined distribution or the existing population.
A sampling operator in moors
is any type that implements the SamplingOperator trait. For example:
use ndarray::Array1;
use crate::{operators::SamplingOperator, random::RandomGenerator};
/// Sampling operator for binary variables.
#[derive(Debug, Clone)]
pub struct RandomSamplingBinary;
impl SamplingOperator for RandomSamplingBinary {
fn sample_individual(&self, num_vars: usize, rng: &mut impl RandomGenerator) -> Array1<f64> {
(0..num_vars)
.map(|_| if rng.gen_bool(0.5) { 1.0 } else { 0.0 })
.collect()
}
}
The main method to implement is sample_individual
, which produces an individual as an ndarray::Array1
. Predefined sampling operators include:
Sampling Operator | Description |
---|---|
RandomSamplingBinary | Generates a vector of random bits, sampling each position independently with equal probability. |
RandomSamplingFloat | Creates a real-valued vector by sampling each gene uniformly within specified bounds. |
RandomSamplingInt | Produces an integer vector by sampling each gene uniformly from a given range. |
PermutationSampling | Generates a random permutation by uniformly shuffling all indices. |
A sampling operator in pymoors
is just a class that defines the operate
method:
from pymoors.typing import TwoDArray
class RandomSamplingBinary:
def operate(self, population: TwoDArray) -> TwoDArray:
mask = np.random.random(population.shape) < 0.5
return mask.astype(np.float64)
operate
acts at poblational level, as usual it means that it takes a 2D numpy array and returns 2D array too, where each row is a sampled individual.
Sampling Operator | Description |
---|---|
pymoors.RandomSamplingBinary |
Generates a vector of random bits, sampling each position independently with equal probability. |
pymoors.RandomSamplingFloat |
Creates a real-valued vector by sampling each gene uniformly within specified bounds. |
pymoors.RandomSamplingInt |
Produces an integer vector by sampling each gene uniformly from a given range. |
pymoors.PermutationSampling |
Generates a random permutation by uniformly shuffling all indices. |
Selection
Selection is a genetic operator that chooses individuals from the current population based on their fitness, favoring higher-quality solutions for reproduction.
The selection operator is a bit more restrictive, in that each pre‑defined algorithm in moors
defines exactly one selection operator. For example, the NSGA-II
algorithm uses a ranking‑by‑crowding‑distance selection operator, while NSGA-III
uses a random selection operator. The user can only provide their own selection operator to a custom algorithm—not to the algorithms that come pre‑defined in moors.
A selection operator in moors
is any type that implements the SelectionOperator trait. For example:
use crate::genetic::{D01, IndividualMOO};
use crate::operators::selection::{DuelResult, SelectionOperator};
use crate::random::RandomGenerator;
#[derive(Debug, Clone)]
pub struct RandomSelection;
impl SelectionOperator for RandomSelection {
type FDim = ndarray::Ix2;
fn tournament_duel<'a, ConstrDim>(
&self,
p1: &IndividualMOO<'a, ConstrDim>,
p2: &IndividualMOO<'a, ConstrDim>,
rng: &mut impl RandomGenerator,
) -> DuelResult
where
ConstrDim: D01,
{
if let result @ DuelResult::LeftWins | result @ DuelResult::RightWins =
Self::feasibility_dominates(p1, p2)
{
return result;
}
// Otherwise, both are feasible or both are infeasible => random winner.
if rng.gen_bool(0.5) {
DuelResult::LeftWins
} else {
DuelResult::RightWins
}
}
}
Note that we have defined an associated type type FDim = ndarray::Ix2
, this is because, in this example, this operator will be used for a multi‑objective algorithm. The selection operators defined in pymoors must specify the fitness dimension. Note that this is the selection operator used by the NSGA‑III algorithm: it performs a random selection that gives priority to feasibility, which is why we use the trait’s static method Self::feasibility_dominates
.
User-defined selection operators are still in progress: See this issue for more information.
Survival
Survival is a genetic operator that determines which individuals are carried over to the next generation based on a general quality criterion.
The survival operator follows the same logic than selection operator, in that each pre‑defined algorithm in moors
defines exactly one selection operator. For example, the NSGA-II
algorithm uses a ranking‑by‑crowding‑distance survival operator, while NSGA-III
uses a reference points based operator. The user can only provide their own survival operator to a custom algorithm—not to the algorithms that come pre‑defined in moors.
A survival operator in moors
is any type that implements the SurvivalOperator trait. For example:
use crate::genetic::{D01, IndividualMOO};
use crate::operators::selection::{DuelResult, SelectionOperator};
use crate::random::RandomGenerator;
#[derive(Debug, Clone)]
pub struct RandomSelection;
impl SelectionOperator for RandomSelection {
type FDim = ndarray::Ix2;
fn tournament_duel<'a, ConstrDim>(
&self,
p1: &IndividualMOO<'a, ConstrDim>,
p2: &IndividualMOO<'a, ConstrDim>,
rng: &mut impl RandomGenerator,
) -> DuelResult
where
ConstrDim: D01,
{
if let result @ DuelResult::LeftWins | result @ DuelResult::RightWins =
Self::feasibility_dominates(p1, p2)
{
return result;
}
// Otherwise, both are feasible or both are infeasible => random winner.
if rng.gen_bool(0.5) {
DuelResult::LeftWins
} else {
DuelResult::RightWins
}
}
}
Note that we have defined an associated type type FDim = ndarray::Ix2
, this is because, in this example, this operator will be used for a multi‑objective algorithm. The selection operators defined in moors must specify the fitness dimension. Note that this is the selection operator used by the NSGA‑III algorithm: it performs a random selection that gives priority to feasibility, which is why we use the trait’s static method Self::feasibility_dominates
.
User-defined survival operators are still in progress: See this issue for more information.