Skip to content

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

  • Fronts is a type alias for a Vec of PopulationMOO.
  • PopulationMOO is a container for individuals, including genes, fitness, constraints (if any), rank (if any), and survival_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 Array1 and Array2.

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: An Array2<f64> representing the genetic encoding of individuals.
  • fitness: An Array2<f64> where each row corresponds to the fitness values of an individual.
  • rank: An Array1<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.