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.
First, determine whether your algorithm uses ranking. If it does, implement the trait: FrontsAndRankingBasedSurvival. If not, implement: SurvivalOperator
We use NSGA-II as an example, which uses both ranking and survival scoring.
use ndarray::{Array1, Array2};
use crate::{
genetic::{D12, Fronts},
operators::survival::moo::FrontsAndRankingBasedSurvival,
random::RandomGenerator,
};
#[derive(Debug, Clone, Default)]
pub struct Nsga2RankCrowdingSurvival;
impl Nsga2RankCrowdingSurvival {
pub fn new() -> Self {
Self {}
}
}
impl FrontsAndRankingBasedSurvival for Nsga2RankCrowdingSurvival {
fn set_front_survival_score<ConstrDim>(
&self,
fronts: &mut Fronts<ConstrDim>,
_rng: &mut impl RandomGenerator,
) where
ConstrDim: D12,
{
for front in fronts.iter_mut() {
let crowding_distance = crowding_distance(&front.fitness);
front.set_survival_score(crowding_distance);
}
}
}
/// Computes the crowding distance for a given Pareto population_fitness.
///
/// # Parameters:
/// - `population_fitness`: A 2D array where each row represents an individual's fitness values.
///
/// # Returns:
/// - A 1D array of crowding distances for each individual in the population_fitness.
fn crowding_distance(population_fitness: &Array2<f64>) -> Array1<f64> {
// Omitted for simplicity
}
Notes on Fronts and Traits
Frontsis a type alias for aVecof PopulationMOO.PopulationMOOis a container for individuals, includinggenes,fitness,constraints(if any),rank(if any), andsurvival_score(if any).- Fronts are sorted by dominance (0-dominated first, then 1-dominated, etc.).
- D12 is a trait for constraint output dimensions, allowing flexibility between
Array1andArray2.
In the moors framework, implementing a survival strategy for multi-objective optimization algorithms involves defining the trait set_front_survival_score. set_front_survival_score method receives a vector of populations (Vec<PopulationMOO>), where each element represents a Pareto front. Each front contains:
genes: AnArray2<f64>representing the genetic encoding of individuals.fitness: AnArray2<f64>where each row corresponds to the fitness values of an individual.rank: AnArray1<f64>indicating the dominance rank of each individual.
The purpose of set_front_survival_score is to assign a survival score to each individual within a front. In the provided example, this score is computed using the crowding_distance function, which returns an Array1<f64> where each element corresponds to the survival score of an individual.
After survival scores are assigned, the operate method is responsible for selecting the individuals that will survive to the next generation.
The splitting front approach is used for selecting the survivors for the next generation, by default moors will prefer an individual with higher survival score, you can modify this by overwritting the method selection Minimize enum member
fn scoring_comparison(&self) -> SurvivalScoringComparison {
SurvivalScoringComparison::Minimize
}
User-defined survival operators are still in progress: See this issue for more information.