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