Skip to content

Operators

Crossover

crossover

NullCrossover()

Bases: Crossover[Solution, Solution]

A no-operation crossover operator that simply returns copies of the parents.

This operator is useful as a placeholder when no crossover is desired in an algorithm. It creates deep copies of the parent solutions without performing any genetic recombination. The number of parents and children is fixed at 2.

Example

from jmetal.operator import NullCrossover from jmetal.core.solution import FloatSolution

Create two test solutions

parent1 = FloatSolution([0], [1], 1) parent2 = FloatSolution([0], [1], 1) parent1.variables = [0.5] parent2.variables = [1.5]

Apply null crossover

crossover = NullCrossover() offspring = crossover.execute([parent1, parent2])

Offspring are copies of parents

offspring[0].variables[0] == parent1.variables[0] True offspring[1].variables[0] == parent2.variables[0] True

Initialize the null crossover operator with zero probability.

Source code in src/jmetal/operator/crossover.py
def __init__(self):
    """Initialize the null crossover operator with zero probability."""
    super().__init__(probability=0.0)

execute(parents)

Execute the crossover operation.

Parameters:

Name Type Description Default
parents list[Solution]

A list of exactly two parent solutions.

required

Returns:

Type Description
list[Solution]

A list containing deep copies of the parent solutions.

Raises:

Type Description
Exception

If the number of parents is not exactly two.

Source code in src/jmetal/operator/crossover.py
def execute(self, parents: list[Solution]) -> list[Solution]:
    """Execute the crossover operation.

    Args:
        parents: A list of exactly two parent solutions.

    Returns:
        A list containing deep copies of the parent solutions.

    Raises:
        Exception: If the number of parents is not exactly two.
    """
    if len(parents) != 2:
        raise Exception(f"The number of parents is not two: {len(parents)}")
    # Create copies to avoid modifying the original parents
    return [copy.copy(parent) for parent in parents]

get_number_of_parents()

Get the number of parent solutions required.

Returns:

Name Type Description
int int

Always returns 2, as this operator works with exactly two parents.

Source code in src/jmetal/operator/crossover.py
def get_number_of_parents(self) -> int:
    """Get the number of parent solutions required.

    Returns:
        int: Always returns 2, as this operator works with exactly two parents.
    """
    return 2

get_number_of_children()

Get the number of offspring solutions produced.

Returns:

Name Type Description
int int

Always returns 2, as this operator produces two offspring.

Source code in src/jmetal/operator/crossover.py
def get_number_of_children(self) -> int:
    """Get the number of offspring solutions produced.

    Returns:
        int: Always returns 2, as this operator produces two offspring.
    """
    return 2

get_name()

Get the name of the operator.

Returns:

Name Type Description
str str

"Null crossover"

Source code in src/jmetal/operator/crossover.py
def get_name(self) -> str:
    """Get the name of the operator.

    Returns:
        str: "Null crossover"
    """
    return "Null crossover"

PMXCrossover(probability, rng=None)

Bases: Crossover[PermutationSolution, PermutationSolution]

Partially Mapped Crossover (PMX) for permutation problems.

PMX is a specialized crossover operator designed for permutation-based representations, commonly used in problems like the Traveling Salesman Problem (TSP) and other ordering problems.

The operator works by:

  1. Selecting two random cut points in the parent permutations.
  2. Creating an offspring by copying the segment between the cut points from parent1.
  3. Filling the remaining positions with the relative order of elements from parent2, while avoiding duplicates using a mapping relationship.

Parameters:

Name Type Description Default
probability float

The probability of applying the crossover (0.0 to 1.0). For each pair of parents, this probability determines whether crossover is applied.

required
Example

from jmetal.operator import PMXCrossover from jmetal.core.solution import PermutationSolution

Create two test solutions (permutation of [0,1,2,3,4])

parent1 = PermutationSolution(5, 1) parent2 = PermutationSolution(5, 1) parent1.variables = [0, 1, 2, 3, 4] parent2.variables = [4, 3, 2, 1, 0]

Apply PMX crossover (with probability 1.0 to ensure execution)

crossover = PMXCrossover(probability=1.0) offspring = crossover.execute([parent1, parent2])

The offspring will be a mix of both parents while preserving the permutation property

all(x in offspring[0].variables for x in range(5)) True

Reference

Goldberg, D. E., & Lingle, R. (1985). Alleles, loci, and the traveling salesman problem. In Proceedings of the First International Conference on Genetic Algorithms and their Applications (pp. 154-159).

Initialize the PMX crossover operator.

Parameters:

Name Type Description Default
probability float

Crossover probability between 0.0 and 1.0.

required
rng Generator | None

Optional NumPy Generator for reproducible randomness.

None
Source code in src/jmetal/operator/crossover.py
def __init__(self, probability: float, rng: np.random.Generator | None = None):
    """Initialize the PMX crossover operator.

    Args:
        probability: Crossover probability between 0.0 and 1.0.
        rng: Optional NumPy Generator for reproducible randomness.
    """
    super().__init__(probability=probability)
    self._rng = rng or np.random.default_rng()

execute(parents)

Execute the PMX crossover operation.

Parameters:

Name Type Description Default
parents list[PermutationSolution]

A list of exactly two parent solutions of type PermutationSolution.

required

Returns:

Type Description
list[PermutationSolution]

A list containing two offspring solutions.

Raises:

Type Description
Exception

If the number of parents is not exactly two.

Source code in src/jmetal/operator/crossover.py
def execute(self, parents: list[PermutationSolution]) -> list[PermutationSolution]:
    """Execute the PMX crossover operation.

    Args:
        parents: A list of exactly two parent solutions of type PermutationSolution.

    Returns:
        A list containing two offspring solutions.

    Raises:
        Exception: If the number of parents is not exactly two.
    """
    if len(parents) != 2:
        raise Exception(f"The number of parents is not two: {len(parents)}")

    # Create new PermutationSolution instances with the correct parameters
    parent1, parent2 = parents
    offspring = [
        parent1.__class__(
            number_of_variables=parent1.number_of_variables,
            number_of_objectives=parent1.number_of_objectives,
            number_of_constraints=parent1.number_of_constraints,
        ),
        parent2.__class__(
            number_of_variables=parent2.number_of_variables,
            number_of_objectives=parent2.number_of_objectives,
            number_of_constraints=parent2.number_of_constraints,
        ),
    ]
    # Copy the variables from parents to offspring
    offspring[0].variables = parent1.variables.copy()
    offspring[1].variables = parent2.variables.copy()

    # Only perform crossover with the specified probability
    if self._rng.random() <= self.probability:
        permutation_length = parents[0].number_of_variables

        # Select two distinct random points for crossover
        pts = self._rng.choice(permutation_length, size=2, replace=False)
        point1, point2 = sorted(pts.tolist())

        # Create directional mappings to resolve conflicts without cycles
        mapping_child1 = {}  # parent2 segment value -> parent1 segment value
        mapping_child2 = {}  # parent1 segment value -> parent2 segment value
        for i in range(point1, point2 + 1):
            value_parent1 = parents[0].variables[i]
            value_parent2 = parents[1].variables[i]
            mapping_child1[value_parent2] = value_parent1
            mapping_child2[value_parent1] = value_parent2

        # Apply PMX crossover
        for i in range(permutation_length):
            if i < point1 or i > point2:
                # For positions outside the crossover points
                val1 = parents[0].variables[i]
                val2 = parents[1].variables[i]

                # Resolve mappings with cycle detection
                visited1 = set()
                while val1 in mapping_child1 and val1 not in visited1:
                    visited1.add(val1)
                    val1 = mapping_child1[val1]

                visited2 = set()
                while val2 in mapping_child2 and val2 not in visited2:
                    visited2.add(val2)
                    val2 = mapping_child2[val2]

                offspring[0].variables[i] = val1
                offspring[1].variables[i] = val2
            else:
                # Swap the segment between the points
                offspring[0].variables[i] = parents[1].variables[i]
                offspring[1].variables[i] = parents[0].variables[i]

    return offspring

CXCrossover(probability, rng=None)

Bases: Crossover[PermutationSolution, PermutationSolution]

Cycle Crossover (CX) for permutation-based solutions.

Cycle Crossover is a specialized operator for permutation problems that preserves the absolute positions of elements from both parents. It works by identifying cycles between two parent permutations and creating offspring by alternating between the cycles of the parents.

The algorithm works as follows:

  1. Start with the first parent and identify a cycle of positions where the elements alternate between the two parents.
  2. For the first offspring, take elements from parent 1 at the cycle positions and from parent 2 at all other positions.
  3. For the second offspring, do the opposite (parent 2 at cycle positions, parent 1 elsewhere).

This operator is particularly useful for problems where the absolute position of elements is important, such as the Traveling Salesman Problem (TSP).

Parameters:

Name Type Description Default
probability float

Crossover probability (0.0 to 1.0). The probability that crossover will be applied to a given pair of parents.

required
Example

from jmetal.operator import CXCrossover from jmetal.core.solution import PermutationSolution

Create two parent solutions (permutation of [0,1,2,3,4])

parent1 = PermutationSolution(5, 1) parent2 = PermutationSolution(5, 1) parent1.variables = [0, 1, 2, 3, 4] # Identity permutation parent2.variables = [4, 3, 2, 1, 0] # Reverse permutation

Create CX crossover with probability 1.0

crossover = CXCrossover(probability=1.0) offspring = crossover.execute([parent1, parent2])

The offspring will preserve absolute positions from both parents

all(x in offspring[0].variables for x in range(5)) # Still a valid permutation True

Reference

Oliver, I. M., Smith, D. J., & Holland, J. R. (1987). A study of permutation crossover operators on the traveling salesman problem. In Proceedings of the Second International Conference on Genetic Algorithms on Genetic algorithms and their application (pp. 224-230).

Initialize the Cycle Crossover operator.

Parameters:

Name Type Description Default
probability float

Crossover probability between 0.0 and 1.0.

required
rng Generator | None

Optional random generator. When None, falls back to a fresh np.random.default_rng().

None
Source code in src/jmetal/operator/crossover.py
def __init__(self, probability: float, rng: np.random.Generator | None = None):
    """Initialize the Cycle Crossover operator.

    Args:
        probability: Crossover probability between 0.0 and 1.0.
        rng: Optional random generator. When None, falls back to a fresh
             np.random.default_rng().
    """
    super().__init__(probability=probability)
    self._rng = rng or np.random.default_rng()

execute(parents)

Execute the Cycle Crossover operation.

Parameters:

Name Type Description Default
parents list[PermutationSolution]

A list of exactly two parent solutions of type PermutationSolution. Both parents must have the same length and contain the same elements.

required

Returns:

Type Description
list[PermutationSolution]

A list containing two offspring solutions.

Raises:

Type Description
Exception

If the number of parents is not exactly two.

Source code in src/jmetal/operator/crossover.py
def execute(self, parents: list[PermutationSolution]) -> list[PermutationSolution]:
    """Execute the Cycle Crossover operation.

    Args:
        parents: A list of exactly two parent solutions of type PermutationSolution.
                Both parents must have the same length and contain the same elements.

    Returns:
        A list containing two offspring solutions.

    Raises:
        Exception: If the number of parents is not exactly two.
    """
    if len(parents) != 2:
        raise Exception(f"The number of parents is not two: {len(parents)}")

    # Create copies of parents (swapped) to serve as offspring
    offspring = [copy.copy(parents[1]), copy.copy(parents[0])]

    # Only perform crossover with the specified probability
    if self._rng.random() <= self.probability:
        # Start with a random position
        if hasattr(self._rng, "integers"):
            start_idx = int(self._rng.integers(0, len(parents[0].variables)))
        else:
            start_idx = int(self._rng.randint(0, len(parents[0].variables)))
        curr_idx = start_idx
        cycle = []

        # Find the cycle of positions
        while True:
            cycle.append(curr_idx)
            # Find where parent1's element is in parent2
            curr_idx = parents[0].variables.index(parents[1].variables[curr_idx])
            if curr_idx == start_idx:  # Completed a full cycle
                break

        # Apply the cycle to create offspring
        for j in range(len(parents[0].variables)):
            if j in cycle:
                # Take values from parent1 for cycle positions in offspring1
                # and from parent2 for cycle positions in offspring2
                offspring[0].variables[j] = parents[0].variables[j]
                offspring[1].variables[j] = parents[1].variables[j]

    return offspring

get_number_of_parents()

Get the number of parent solutions required.

Returns:

Name Type Description
int int

Always returns 2, as this operator works with exactly two parents.

Source code in src/jmetal/operator/crossover.py
def get_number_of_parents(self) -> int:
    """Get the number of parent solutions required.

    Returns:
        int: Always returns 2, as this operator works with exactly two parents.
    """
    return 2

get_number_of_children()

Get the number of offspring solutions produced.

Returns:

Name Type Description
int int

Always returns 2, as this operator produces two offspring.

Source code in src/jmetal/operator/crossover.py
def get_number_of_children(self) -> int:
    """Get the number of offspring solutions produced.

    Returns:
        int: Always returns 2, as this operator produces two offspring.
    """
    return 2

get_name()

Get the name of the operator.

Returns:

Name Type Description
str str

"Cycle crossover"

Source code in src/jmetal/operator/crossover.py
def get_name(self) -> str:
    """Get the name of the operator.

    Returns:
        str: "Cycle crossover"
    """
    return "Cycle crossover"

SBXCrossover(probability, distribution_index=20.0, repair_operator=ClampFloatRepair(), rng=None)

Bases: Crossover[FloatSolution, FloatSolution]

Simulated Binary Crossover (SBX) for real-valued solutions.

SBX is a popular crossover operator for real-coded genetic algorithms that simulates the behavior of the single-point crossover operator in binary-coded GAs. It creates offspring solutions based on a probability distribution centered around the parent solutions, with the spread of the distribution controlled by the distribution index.

The operator works by:

  1. For each variable, compute a spread factor beta based on a random number and the distribution index.
  2. Use beta to compute new variable values that are spread around the parent values.
  3. The distribution index controls whether offspring are likely to be near the parents (high values) or more spread out (low values).

Parameters:

Name Type Description Default
probability float

Crossover probability (0.0 to 1.0). The probability that crossover will be applied to a given pair of parents.

required
distribution_index float

Distribution index (must be ≥ 0). Controls the shape of the probability distribution: - High values (>20): Offspring are very close to parents - Medium values (~10-20): Balanced exploration/exploitation - Low values (<5): High exploration, offspring can be far from parents Typical values range from 5 to 30, with 20 being a common default.

20.0

Raises:

Type Description
ValueError

If distribution_index is negative

Example

from jmetal.operator import SBXCrossover from jmetal.core.solution import FloatSolution

Create two parent solutions

parent1 = FloatSolution([0, 0], [1, 1], 1) parent2 = FloatSolution([0, 0], [1, 1], 1) parent1.variables = [0.2, 0.8] parent2.variables = [0.8, 0.2]

Create SBX crossover with probability 0.9 and distribution index 20

crossover = SBXCrossover(probability=0.9, distribution_index=20.0)

Generate offspring

offspring = crossover.execute([parent1, parent2])

Offspring will be similar to parents due to high distribution index

all(0.1 < x < 0.9 for x in offspring[0].variables + offspring[1].variables) True

References

Deb, K., & Agrawal, R. B. (1995). Simulated binary crossover for continuous search space. Complex Systems, 9(2), 115-148.

Deb, K., & Deb, K. (2014). Multi-objective optimization. In Search methodologies (pp. 403-449). Springer, Boston, MA.

Source code in src/jmetal/operator/crossover.py
def __init__(
    self,
    probability: float,
    distribution_index: float = 20.0,
    repair_operator: Callable[[float, float, float], float] | FloatRepairOperator | None = ClampFloatRepair(),
    rng: np.random.Generator | None = None,
):
    super().__init__(probability=probability)
    self.distribution_index = distribution_index
    if distribution_index < 0:
        raise ValueError("The distribution index cannot be negative")
    # Normalize/ensure the repair operator provides scalar and vector APIs
    self.repair_operator = ensure_float_repair(repair_operator)
    self._rng = rng or np.random.default_rng()

SPXCrossover(probability, rng=None)

Bases: Crossover[BinarySolution, BinarySolution]

A high-performance single-point crossover operator for BinarySolution.

This implementation uses NumPy's vectorized operations for better performance when working with BinarySolution solutions. It performs a single-point crossover between two parent solutions to produce two offspring.

The crossover point is selected uniformly at random from all possible bit positions in the solution. The bits after the crossover point are swapped between the two parents to create the offspring.

Parameters:

Name Type Description Default
probability float

The probability of applying the crossover (must be between 0.0 and 1.0)

required

Raises:

Type Description
ValueError

If the probability is not in the range [0.0, 1.0]

Source code in src/jmetal/operator/crossover.py
def __init__(self, probability: float, rng: np.random.Generator | None = None):
    if not (0.0 <= probability <= 1.0):
        raise ValueError(f"Probability must be between 0.0 and 1.0, but was {probability}")
    super().__init__(probability=probability)
    # Prefer a provided Generator or use the modern NumPy default_rng. This
    # makes the operator testable when tests patch `numpy.random.default_rng`.
    self._rng = rng or np.random.default_rng()

execute(parents)

Execute the single-point crossover operation.

Parameters:

Name Type Description Default
parents list[BinarySolution]

A list of exactly two parent solutions of type BinarySolution. Both parents must have the same number of bits.

required

Returns:

Type Description
list[BinarySolution]

List[BinarySolution]: A list containing two offspring solutions.

Note

This method assumes that both parents are valid BinarySolution instances with properly initialized bits attributes.

Source code in src/jmetal/operator/crossover.py
def execute(self, parents: list[BinarySolution]) -> list[BinarySolution]:
    """
    Execute the single-point crossover operation.

    Args:
        parents: A list of exactly two parent solutions of type BinarySolution.
                Both parents must have the same number of bits.

    Returns:
        List[BinarySolution]: A list containing two offspring solutions.

    Note:
        This method assumes that both parents are valid BinarySolution instances
        with properly initialized bits attributes.
    """
    if len(parents) != 2:
        raise ValueError("SPXCrossover requires exactly two parents")

    # Create copies of the parents to avoid modifying the originals
    offspring = [copy.copy(parents[0]), copy.copy(parents[1])]

    # Check if crossover should be performed based on probability
    if self._rng.random() > self.probability:
        return offspring

    # Get the bits from both parents
    bits1 = offspring[0].bits
    bits2 = offspring[1].bits

    # Ensure both parents have the same number of bits
    if len(bits1) != len(bits2):
        raise ValueError("Parents must have the same number of bits")

    num_bits = len(bits1)
    if num_bits > 1:
        # Select a random crossover point (1 to num_bits-1 to ensure crossover happens)
        crossover_point = int(self._rng.integers(1, num_bits))

        # Create new bit arrays for the offspring
        new_bits1 = np.concatenate([bits1[:crossover_point], bits2[crossover_point:]])
        new_bits2 = np.concatenate([bits2[:crossover_point], bits1[crossover_point:]])

        # Update the bits in the offspring
        offspring[0].bits = new_bits1
        offspring[1].bits = new_bits2

    return offspring

get_number_of_parents()

Return the number of parent solutions required by the operator.

Source code in src/jmetal/operator/crossover.py
def get_number_of_parents(self) -> int:
    """Return the number of parent solutions required by the operator."""
    return 2

get_number_of_children()

Return the number of offspring produced by the operator.

Source code in src/jmetal/operator/crossover.py
def get_number_of_children(self) -> int:
    """Return the number of offspring produced by the operator."""
    return 2

get_name()

Return the name of the operator.

Source code in src/jmetal/operator/crossover.py
def get_name(self) -> str:
    """Return the name of the operator."""
    return "Single point crossover"

BLXAlphaCrossover(probability=0.9, alpha=0.5, repair_operator=None, rng=None)

Bases: Crossover[FloatSolution, FloatSolution]

BLX-α (Blend Crossover) for real-valued solutions.

The BLX-α crossover creates offspring within a range that is extended by a factor of α (alpha) beyond the range defined by the parent values. This allows for exploration beyond the region defined by the parents while maintaining a balance between exploration and exploitation.

The crossover works by: 1. For each variable, determine the min and max values from the parents 2. Calculate the range between parents 3. Expand the range by α * range in both directions 4. Sample new values uniformly from this expanded range 5. Apply bounds repair if values fall outside the variable bounds

Parameters:

Name Type Description Default
probability float

Crossover probability (0.0 to 1.0)

0.9
alpha float

Expansion factor (must be ≥ 0). Controls the exploration range: - alpha = 0: Offspring will be in the range defined by parents (no exploration) - alpha > 0: Offspring can be outside parent range (increased exploration) - Typical values: 0.1 to 0.5

0.5
repair_operator Callable[[float, float, float], float] | None

Optional function to repair out-of-bounds values. If None, values are clamped to the variable bounds using min/max. Signature: repair_operator(value: float, lower_bound: float, upper_bound: float) -> float

None

Raises:

Type Description
ValueError

If probability is not in [0,1] or alpha is negative.

Reference

Eshelman, L. J., & Schaffer, J. D. (1993). Real-coded genetic algorithms and interval-schemata. Foundations of genetic algorithms, 2, 187-202.

Source code in src/jmetal/operator/crossover.py
def __init__(
    self,
    probability: float = 0.9,
    alpha: float = 0.5,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")
    if alpha < 0:
        raise ValueError("alpha must be non-negative")

    super().__init__(probability=probability)
    self.alpha = alpha
    # Normalize repair operator to FloatRepairOperator for scalar/vector API
    self.repair_operator = ensure_float_repair(repair_operator)
    self._rng = rng or np.random.default_rng()

doCrossover(probability, parent1, parent2)

Perform the crossover operation.

Parameters:

Name Type Description Default
probability float

Crossover probability

required
parent1 FloatSolution

First parent solution

required
parent2 FloatSolution

Second parent solution

required

Returns:

Type Description
list[FloatSolution]

A list containing two offspring solutions

Source code in src/jmetal/operator/crossover.py
def doCrossover(
    self, probability: float, parent1: FloatSolution, parent2: FloatSolution
) -> list[FloatSolution]:
    """Perform the crossover operation.

    Args:
        probability: Crossover probability
        parent1: First parent solution
        parent2: Second parent solution

    Returns:
        A list containing two offspring solutions
    """
    offspring1 = parent1.__class__(
        parent1.lower_bound,
        parent1.upper_bound,
        len(parent1.objectives),
        len(parent1.constraints) if hasattr(parent1, "constraints") else 0,
    )
    offspring2 = parent2.__class__(
        parent2.lower_bound,
        parent2.upper_bound,
        len(parent2.objectives),
        len(parent2.constraints) if hasattr(parent2, "constraints") else 0,
    )

    if self._rng.random() > probability:
        offspring1.variables = parent1.variables.copy()
        offspring2.variables = parent2.variables.copy()
        return [offspring1, offspring2]

    for i in range(len(parent1.variables)):
        x1, x2 = parent1.variables[i], parent2.variables[i]
        lower_bound = parent1.lower_bound[i]
        upper_bound = parent1.upper_bound[i]

        # Calculate the range between parents
        min_val = min(x1, x2)
        max_val = max(x1, x2)
        range_val = max_val - min_val

        # Expand the range by alpha
        min_range = min_val - range_val * self.alpha
        max_range = max_val + range_val * self.alpha

        # Generate offspring values within the expanded range
        y1 = self._rng.uniform(min_range, max_range)
        y2 = self._rng.uniform(min_range, max_range)

        # Repair out-of-bounds values (use scalar API)
        y1 = self.repair_operator.repair_scalar(y1, lower_bound, upper_bound)
        y2 = self.repair_operator.repair_scalar(y2, lower_bound, upper_bound)

        offspring1._variables[i] = y1
        offspring2._variables[i] = y2

    return [offspring1, offspring2]

BLXAlphaBetaCrossover(probability=0.9, alpha=0.5, beta=0.5, repair_operator=None, rng=None)

Bases: Crossover[FloatSolution, FloatSolution]

BLX-αβ (Blend Crossover with separate alpha and beta) for real-valued solutions.

An extension of BLX-α crossover that uses two different expansion factors (α and β) for the lower and upper bounds respectively. This allows for asymmetric exploration around the parent solutions.

The crossover works by: 1. For each variable, determine the min and max values from the parents 2. Calculate the range between parents (d = max - min) 3. Expand the range by αd below the min and βd above the max 4. Sample new values uniformly from this expanded range 5. Apply bounds repair if values fall outside the variable bounds

Parameters:

Name Type Description Default
probability float

Crossover probability (0.0 to 1.0)

0.9
alpha float

Lower expansion factor (must be ≥ 0). Controls exploration below parents: - alpha = 0: No exploration below the smaller parent value - alpha > 0: Expands range below smaller parent by alpha*d - Typical values: 0.1 to 0.5

0.5
beta float

Upper expansion factor (must be ≥ 0). Controls exploration above parents: - beta = 0: No exploration above the larger parent value - beta > 0: Expands range above larger parent by beta*d - Typical values: 0.1 to 0.5

0.5
repair_operator Callable[[float, float, float], float] | None

Optional function to repair out-of-bounds values. If None, values are clamped to the variable bounds using min/max. Signature: repair_operator(value: float, lower_bound: float, upper_bound: float) -> float

None

Raises:

Type Description
ValueError

If probability is not in [0,1] or alpha/beta are negative.

Reference

Eshelman, L. J., & Schaffer, J. D. (1993). Real-coded genetic algorithms and interval-schemata. Foundations of genetic algorithms, 2, 187-202.

Source code in src/jmetal/operator/crossover.py
def __init__(
    self,
    probability: float = 0.9,
    alpha: float = 0.5,
    beta: float = 0.5,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")
    if alpha < 0:
        raise ValueError("alpha must be non-negative")
    if beta < 0:
        raise ValueError("beta must be non-negative")

    super().__init__(probability=probability)
    self.alpha = alpha
    self.beta = beta
    # Normalize repair operator to FloatRepairOperator for scalar/vector API
    self.repair_operator = ensure_float_repair(repair_operator)
    self._rng = rng or np.random.default_rng()

doCrossover(probability, parent1, parent2)

Perform the crossover operation.

Parameters:

Name Type Description Default
probability float

Crossover probability

required
parent1 FloatSolution

First parent solution

required
parent2 FloatSolution

Second parent solution

required

Returns:

Type Description
list[FloatSolution]

A list containing two offspring solutions

Source code in src/jmetal/operator/crossover.py
def doCrossover(
    self, probability: float, parent1: FloatSolution, parent2: FloatSolution
) -> list[FloatSolution]:
    """Perform the crossover operation.

    Args:
        probability: Crossover probability
        parent1: First parent solution
        parent2: Second parent solution

    Returns:
        A list containing two offspring solutions
    """
    offspring1 = parent1.__class__(
        parent1.lower_bound,
        parent1.upper_bound,
        len(parent1.objectives),
        len(parent1.constraints) if hasattr(parent1, "constraints") else 0,
    )
    offspring2 = parent2.__class__(
        parent2.lower_bound,
        parent2.upper_bound,
        len(parent2.objectives),
        len(parent2.constraints) if hasattr(parent2, "constraints") else 0,
    )

    if self._rng.random() > probability:
        offspring1.variables = parent1.variables.copy()
        offspring2.variables = parent2.variables.copy()
        return [offspring1, offspring2]

    for i in range(len(parent1.variables)):
        x1, x2 = parent1.variables[i], parent2.variables[i]
        lower_bound = parent1.lower_bound[i]
        upper_bound = parent1.upper_bound[i]

        # Ensure x1 <= x2
        if x1 > x2:
            x1, x2 = x2, x1

        # Calculate the range and expanded bounds
        d = x2 - x1
        c_min = x1 - self.alpha * d
        c_max = x2 + self.beta * d

        # Generate offspring values within the expanded range
        y1 = self._rng.uniform(c_min, c_max)
        y2 = self._rng.uniform(c_min, c_max)

        # Repair out-of-bounds values (use scalar API)
        y1 = self.repair_operator.repair_scalar(y1, lower_bound, upper_bound)
        y2 = self.repair_operator.repair_scalar(y2, lower_bound, upper_bound)

        offspring1._variables[i] = y1
        offspring2._variables[i] = y2

    return [offspring1, offspring2]

ArithmeticCrossover(probability=0.9, repair_operator=None, rng=None)

Bases: Crossover[FloatSolution, FloatSolution]

Arithmetic Crossover for real-valued solutions.

This operator performs an arithmetic combination of two parent solutions to produce two offspring. For each variable, a random weight (alpha) is used to compute a weighted average of the parent values.

The crossover works by:

  1. For each variable, generate a random weight alpha in [0, 1].
  2. Calculate new values as child1 = alpha * parent1 + (1 - alpha) * parent2 and child2 = (1 - alpha) * parent1 + alpha * parent2.
  3. Apply bounds repair if values fall outside the variable bounds.

Parameters:

Name Type Description Default
probability float

Crossover probability (0.0 to 1.0)

0.9
repair_operator Callable[[float, float, float], float] | None

Optional function to repair out-of-bounds values. If None, values are clamped to the variable bounds using min/max. Signature: repair_operator(value: float, lower_bound: float, upper_bound: float) -> float

None

Raises:

Type Description
ValueError

If probability is not in [0,1]

Reference

Michalewicz, Z. (1996). Genetic Algorithms + Data Structures = Evolution Programs. Springer-Verlag, Berlin.

Source code in src/jmetal/operator/crossover.py
def __init__(
    self,
    probability: float = 0.9,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")

    super().__init__(probability=probability)
    # Normalize repair operator to FloatRepairOperator for scalar/vector API
    self.repair_operator = ensure_float_repair(repair_operator)
    self._rng = rng or np.random.default_rng()

doCrossover(probability, parent1, parent2)

Perform the arithmetic crossover operation.

Parameters:

Name Type Description Default
probability float

Crossover probability

required
parent1 FloatSolution

First parent solution

required
parent2 FloatSolution

Second parent solution

required

Returns:

Type Description
list[FloatSolution]

A list containing two offspring solutions

Source code in src/jmetal/operator/crossover.py
def doCrossover(
    self, probability: float, parent1: FloatSolution, parent2: FloatSolution
) -> list[FloatSolution]:
    """Perform the arithmetic crossover operation.

    Args:
        probability: Crossover probability
        parent1: First parent solution
        parent2: Second parent solution

    Returns:
        A list containing two offspring solutions
    """
    # Create copies of the parents as the base for the offspring
    offspring1 = parent1.__class__(
        parent1.lower_bound,
        parent1.upper_bound,
        len(parent1.objectives),
        len(parent1.constraints) if hasattr(parent1, "constraints") else 0,
    )
    offspring2 = parent2.__class__(
        parent2.lower_bound,
        parent2.upper_bound,
        len(parent2.objectives),
        len(parent2.constraints) if hasattr(parent2, "constraints") else 0,
    )

    # If crossover doesn't happen, return copies of the parents
    if self._rng.random() >= probability:
        offspring1.variables = parent1.variables.copy()
        offspring2.variables = parent2.variables.copy()
        return [offspring1, offspring2]

    # Generate a single alpha for all variables in this crossover
    alpha = self._rng.random()

    # Initialize variables for both offspring with the correct length
    num_variables = len(parent1.variables)
    # Initialize variables as lists
    vars1 = [0.0] * num_variables
    vars2 = [0.0] * num_variables

    # Perform arithmetic crossover on each variable
    for i in range(num_variables):
        p1 = parent1.variables[i]
        p2 = parent2.variables[i]

        # Calculate new values using the same alpha for all variables
        value1 = alpha * p1 + (1 - alpha) * p2
        value2 = (1 - alpha) * p1 + alpha * p2

        # Apply bounds repair if needed
        lower_bound = parent1.lower_bound[i]
        upper_bound = parent1.upper_bound[i]

        repaired1 = self.repair_operator.repair_scalar(value1, lower_bound, upper_bound)
        repaired2 = self.repair_operator.repair_scalar(value2, lower_bound, upper_bound)

        vars1[i] = repaired1
        vars2[i] = repaired2

    # Set the variables after all calculations are done
    offspring1._variables = vars1
    offspring2._variables = vars2

    return [offspring1, offspring2]

UnimodalNormalDistributionCrossover(probability=0.9, zeta=0.5, eta=0.35, repair_operator=None, rng=None)

Bases: Crossover[FloatSolution, FloatSolution]

Unimodal Normal Distribution Crossover (UNDX) for real-valued solutions.

UNDX is a multi-parent crossover operator that generates offspring based on the normal distribution defined by three parent solutions. It is particularly effective for continuous optimization problems as it preserves the statistics of the population.

Reference

Onikura, T., & Kobayashi, S. (1999). Extended UNIMODAL DISTRIBUTION CROSSOVER for REAL-CODED GENETIC ALGORITHMS. In Proceedings of the 1999 Congress on Evolutionary Computation-CEC99 (Cat. No. 99TH8406) (Vol. 2, pp. 1581-1588). IEEE.

Parameters:

Name Type Description Default
probability float

Crossover probability (0.0 to 1.0)

0.9
zeta float

Controls the spread along the line connecting parents (typically in [0.1, 1.0], where smaller values produce offspring closer to the parents)

0.5
eta float

Controls the spread in the orthogonal direction (typically in [0.1, 0.5], where smaller values produce more concentrated distributions)

0.35
repair_operator Callable[[float, float, float], float] | None

Optional function to repair out-of-bounds values. If None, values are clamped to the variable bounds using min/max. Signature: repair_operator(value: float, lower_bound: float, upper_bound: float) -> float

None

Raises:

Type Description
ValueError

If probability is not in [0,1] or if zeta or eta are negative

Source code in src/jmetal/operator/crossover.py
def __init__(
    self,
    probability: float = 0.9,
    zeta: float = 0.5,
    eta: float = 0.35,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")
    if zeta < 0:
        raise ValueError("zeta must be non-negative")
    if eta < 0:
        raise ValueError("eta must be non-negative")

    super().__init__(probability=probability)
    self.zeta = zeta
    self.eta = eta
    # Normalize repair operator to FloatRepairOperator for scalar/vector API
    self.repair_operator = ensure_float_repair(repair_operator)
    self._rng = rng or np.random.default_rng()

doCrossover(probability, parent1, parent2, parent3)

Perform the UNDX crossover operation.

Parameters:

Name Type Description Default
probability float

Crossover probability

required
parent1 FloatSolution

First parent solution

required
parent2 FloatSolution

Second parent solution

required
parent3 FloatSolution

Third parent solution (used to determine the orthogonal direction)

required

Returns:

Type Description
list[FloatSolution]

A list containing two offspring solutions

Source code in src/jmetal/operator/crossover.py
def doCrossover(
    self,
    probability: float,
    parent1: FloatSolution,
    parent2: FloatSolution,
    parent3: FloatSolution,
) -> list[FloatSolution]:
    """Perform the UNDX crossover operation.

    Args:
        probability: Crossover probability
        parent1: First parent solution
        parent2: Second parent solution
        parent3: Third parent solution (used to determine the orthogonal direction)

    Returns:
        A list containing two offspring solutions
    """
    # Create offspring as copies of parents initially
    offspring1 = parent1.__class__(
        parent1.lower_bound,
        parent1.upper_bound,
        len(parent1.objectives),
        len(parent1.constraints) if hasattr(parent1, "constraints") else 0,
    )
    offspring2 = parent2.__class__(
        parent2.lower_bound,
        parent2.upper_bound,
        len(parent2.objectives),
        len(parent2.constraints) if hasattr(parent2, "constraints") else 0,
    )

    # If crossover doesn't happen, return copies of the parents
    if self._rng.random() >= probability:
        offspring1.variables = parent1.variables.copy()
        offspring2.variables = parent2.variables.copy()
        return [offspring1, offspring2]

    number_of_variables = len(parent1.variables)

    # Calculate the center of mass between parent1 and parent2
    center = [(p1 + p2) / 2.0 for p1, p2 in zip(parent1.variables, parent2.variables)]

    # Calculate the difference vector between parent1 and parent2
    diff = [p2 - p1 for p1, p2 in zip(parent1.variables, parent2.variables)]
    distance = math.sqrt(sum(d * d for d in diff))

    # If parents are too close, return exact copies to avoid division by zero
    if distance < 1e-10:
        offspring1.variables = parent1.variables.copy()
        offspring2.variables = parent2.variables.copy()
        return [offspring1, offspring2]

    # Generate offspring
    for i in range(number_of_variables):
        # Generate values along the line connecting the parents
        alpha = self._rng.uniform(-self.zeta * distance, self.zeta * distance)

        # Generate values in the orthogonal direction
        # Calculate beta as the sum of two random values centered around 0
        beta = (self._rng.random() - 0.5) * self.eta * distance + (
            self._rng.random() - 0.5
        ) * self.eta * distance

        # Calculate the orthogonal component from parent3
        orthogonal = (parent3.variables[i] - center[i]) / distance if distance > 0 else 0.0

        # Create the new values
        value1 = center[i] + alpha * diff[i] / distance + beta * orthogonal
        value2 = center[i] - alpha * diff[i] / distance - beta * orthogonal

        # Apply bounds repair if needed (use scalar API)
        lower_bound = parent1.lower_bound[i]
        upper_bound = parent1.upper_bound[i]

        offspring1._variables[i] = self.repair_operator.repair_scalar(
            value1, lower_bound, upper_bound
        )
        offspring2._variables[i] = self.repair_operator.repair_scalar(
            value2, lower_bound, upper_bound
        )

    return [offspring1, offspring2]

DifferentialEvolutionCrossover(CR, F, K=0.5, rng=None)

Bases: Crossover[FloatSolution, FloatSolution]

Differential Evolution (DE) crossover operator for real-valued solutions.

This operator implements the standard DE crossover used in the DE/rand/1/bin and DE/best/1/bin variants. It creates a trial vector by combining the target vector with a difference vector, then performs binomial crossover between the target and trial vectors.

The operator requires three parents and three mutation factors (F, CR, and K). The first parent is the target vector, while the other two are used to compute the difference vector.

Parameters:

Name Type Description Default
CR float

Crossover probability (0.0 to 1.0). Controls the probability of each variable being taken from the trial vector versus the target vector.

required
F float

Differential weight (mutation factor) for the difference vector. Typically in [0, 2].

required
K float

Scaling factor for the difference vector. Typically in [0, 1].

0.5

Raises:

Type Description
ValueError

If CR is not in [0,1] or F/K are negative.

Reference

Storn, R., & Price, K. (1997). Differential evolution - a simple and efficient heuristic for global optimization over continuous spaces. Journal of global optimization, 11(4), 341-359.

Source code in src/jmetal/operator/crossover.py
def __init__(
    self, CR: float, F: float, K: float = 0.5, rng: np.random.Generator | None = None
):
    super().__init__(probability=1.0)
    self.CR = CR
    self.F = F
    self.K = K
    self._rng = rng or np.random.default_rng()

    self.current_individual: FloatSolution | None = None

execute(parents)

Execute the differential evolution crossover ('best/1/bin' variant in jMetal).

Source code in src/jmetal/operator/crossover.py
def execute(self, parents: list[FloatSolution]) -> list[FloatSolution]:
    """Execute the differential evolution crossover ('best/1/bin' variant in jMetal)."""
    if len(parents) != self.get_number_of_parents():
        raise Exception(
            f"The number of parents is not {self.get_number_of_parents()}: {len(parents)}"
        )

    # Ensure current_individual has been set before using it
    # Ensure current_individual has been set before using it
    assert self.current_individual is not None, (
        "current_individual must be set before calling execute"
    )
    # Copy the current individual using __copy__
    child = copy.copy(self.current_individual)

    number_of_variables = len(parents[0].variables)
    if hasattr(self._rng, "integers"):
        rand = int(self._rng.integers(0, number_of_variables))
    else:
        rand = int(self._rng.randint(0, number_of_variables))

    for i in range(number_of_variables):
        if self._rng.random() < self.CR or i == rand:
            value = parents[2].variables[i] + self.F * (
                parents[0].variables[i] - parents[1].variables[i]
            )

            if value < child.lower_bound[i]:
                value = child.lower_bound[i]
            if value > child.upper_bound[i]:
                value = child.upper_bound[i]
        else:
            value = child.variables[i]

        child._variables[i] = value

    return [child]

Mutation

mutation

NullMutation()

Bases: Mutation[Solution]

Source code in src/jmetal/operator/mutation.py
def __init__(self):
    super().__init__(probability=0)

get_name()

Get the name of the operator.

Returns:

Name Type Description
str str

A string containing the operator name and mutation probability.

Source code in src/jmetal/operator/mutation.py
def get_name(self) -> str:
    """Get the name of the operator.

    Returns:
        str: A string containing the operator name and mutation probability.
    """
    return f"Null mutation (p={self.probability})"

BitFlipMutation(probability, rng=None)

Bases: Mutation[BinarySolution]

NumPy-optimized bit flip mutation for BinarySolution.

This implementation uses NumPy's vectorized operations for better performance when working with BinarySolution solutions. It flips each bit with a given probability, but does so using efficient array operations.

Parameters:

Name Type Description Default
probability float

The probability of flipping each bit (0.0 to 1.0)

required

Raises:

Type Description
ValueError

If probability is not in range [0.0, 1.0]

Source code in src/jmetal/operator/mutation.py
def __init__(self, probability: float, rng: np.random.Generator | None = None):
    if not (0.0 <= probability <= 1.0):
        raise ValueError(f"Probability must be in range [0.0, 1.0], got {probability}")
    super().__init__(probability=probability)
    self.rng = rng if rng is not None else np.random.default_rng()

execute(solution)

Execute the bit flip mutation operation.

Parameters:

Name Type Description Default
solution BinarySolution

The solution to be mutated. Must be a BinarySolution with a 'bits' attribute.

required

Returns:

Type Description
BinarySolution

The mutated solution (modified in-place)

Raises:

Type Description
TypeError

If solution is not a BinarySolution or doesn't have a 'bits' attribute

ValueError

If the solution has no variables or invalid bit values

Note

The input solution is modified in-place and also returned.

Source code in src/jmetal/operator/mutation.py
def execute(self, solution: BinarySolution) -> BinarySolution:
    """
    Execute the bit flip mutation operation.

    Args:
        solution: The solution to be mutated. Must be a BinarySolution with a 'bits' attribute.

    Returns:
        The mutated solution (modified in-place)

    Raises:
        TypeError: If solution is not a BinarySolution or doesn't have a 'bits' attribute
        ValueError: If the solution has no variables or invalid bit values

    Note:
        The input solution is modified in-place and also returned.
    """
    # Input validation
    if not isinstance(solution, BinarySolution):
        raise TypeError(f"Expected BinarySolution, got {type(solution).__name__}")

    if not hasattr(solution, "bits") or not isinstance(solution.bits, np.ndarray):
        raise AttributeError("Solution must have a 'bits' attribute of type numpy.ndarray")

    if solution.number_of_variables <= 0:
        raise ValueError("Solution must have at least one variable")

    if len(solution.bits) == 0:
        return solution  # Nothing to mutate

    try:
        # Generate random numbers for each bit
        rand_values = self.rng.random(solution.number_of_variables)

        # Create a mask of bits to flip
        flip_mask = rand_values < self.probability

        # Ensure the mask has the same shape as solution.bits
        if flip_mask.shape != solution.bits.shape:
            flip_mask = np.resize(flip_mask, solution.bits.shape)

        # Flip the bits where the mask is True
        solution.bits ^= flip_mask.astype(bool)

        return solution

    except Exception as e:
        raise RuntimeError(f"Error during bit flip mutation: {str(e)}") from e

get_name()

Return the name of the operator.

Returns:

Name Type Description
str str

A string representing the name of the operator

Source code in src/jmetal/operator/mutation.py
def get_name(self) -> str:
    """
    Return the name of the operator.

    Returns:
        str: A string representing the name of the operator
    """
    return "Bit flip mutation"

PolynomialMutation(probability=0.01, distribution_index=20.0, repair_operator=None, rng=None)

Bases: Mutation[FloatSolution]

Implementation of a polynomial mutation operator for real-valued solutions.

The polynomial mutation is based on a polynomial probability distribution that perturbs solutions in a way that favors small changes while still allowing occasional larger jumps. This provides a good balance between exploration and exploitation in evolutionary algorithms.

The mutation follows a polynomial probability distribution centered on the parent value, with the spread controlled by the distribution index.

Parameters:

Name Type Description Default
probability float

The probability of mutating each variable (0 ≤ p ≤ 1).

0.01
distribution_index float

Controls the perturbation magnitude (must be ≥ 0): - Lower values (e.g., 5-20): More exploratory, larger mutations - Medium values (e.g., 20-100): Balanced exploration/exploitation - Higher values (e.g., >100): More exploitative, smaller mutations

20.0
repair_operator Callable[[float, float, float], float] | None

Optional function to repair out-of-bounds values. If None, values are clamped to the variable bounds.

None

Raises:

Type Description
ValueError

If probability is not in [0,1] or distribution_index is negative.

Source code in src/jmetal/operator/mutation.py
def __init__(
    self,
    probability: float = 0.01,
    distribution_index: float = 20.0,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")
    if distribution_index < 0:
        raise ValueError("distribution_index must be non-negative")

    super().__init__(probability=probability)
    self.distribution_index = distribution_index
    # Normalize repair operator to a FloatRepairOperator instance
    self.repair_operator = ensure_float_repair(repair_operator)
    # RNG generator (np.random.Generator) for reproducibility
    self.rng = rng if rng is not None else np.random.default_rng()

IntegerPolynomialMutation(probability, distribution_index=20.0, repair_operator=None, rng=None)

Bases: Mutation[IntegerSolution]

Polynomial mutation operator for integer-valued decision variables.

This operator adapts the polynomial mutation for integer solutions by rounding the continuous values to the nearest integer. It's particularly useful for problems where variables must take discrete integer values.

The mutation works by: 1. Applying polynomial mutation to the integer variable (treated as float) 2. Rounding the result to the nearest integer 3. Clamping the value to the variable's bounds

Parameters:

Name Type Description Default
probability float

The probability of mutating each variable (0 ≤ p ≤ 1).

required
distribution_index float

Controls the perturbation magnitude (must be ≥ 0): - Lower values (e.g., 5-20): More exploratory, larger mutations - Medium values (e.g., 20-100): Balanced exploration/exploitation - Higher values (e.g., >100): More exploitative, smaller mutations

20.0
Example

from jmetal.operator import IntegerPolynomialMutation from jmetal.core.solution import IntegerSolution

Create an integer solution with bounds [0, 10] for all variables

solution = IntegerSolution(3, 1, 0) # 3 variables, 1 objective, 0 constraints solution.variables = [5, 5, 5] solution.lower_bound = [0] * 3 solution.upper_bound = [10] * 3

Apply polynomial mutation with 100% probability

mutation = IntegerPolynomialMutation(probability=1.0, distribution_index=20.0) mutated = mutation.execute(solution)

Variables will be mutated with integer values within [0, 10]

Source code in src/jmetal/operator/mutation.py
def __init__(
    self,
    probability: float,
    distribution_index: float = 20.0,
    repair_operator: Callable[[float, int, int], int] | None = None,
    rng: np.random.Generator | None = None,
):
    super().__init__(probability=probability)
    self.distribution_index = distribution_index
    # Normalize integer repair operator
    self.repair_operator = ensure_integer_repair(repair_operator)
    self.rng = rng if rng is not None else np.random.default_rng()

get_name()

Get the name of the operator.

Returns:

Name Type Description
str str

A string containing the operator name and distribution index.

Source code in src/jmetal/operator/mutation.py
def get_name(self) -> str:
    """Get the name of the operator.

    Returns:
        str: A string containing the operator name and distribution index.
    """
    return f"Polynomial mutation (int, η={self.distribution_index})"

SimpleRandomMutation(probability, rng=None)

Bases: Mutation[FloatSolution]

Implementation of a simple random mutation operator for real-valued solutions.

This operator replaces the value of a decision variable with a random value uniformly distributed between the lower and upper bounds of that variable. This is one of the simplest mutation operators but can be effective for exploration, especially in the early stages of optimization.

The mutation works by:

  1. For each variable, with probability probability, replace its value with a random value from a uniform distribution between the variable's lower and upper bounds.
  2. Leave the variable unchanged otherwise.

Parameters:

Name Type Description Default
probability float

The probability of mutating each variable (0 ≤ p ≤ 1). Higher values increase exploration but may disrupt good solutions.

required
Example

from jmetal.operator import SimpleRandomMutation from jmetal.core.solution import FloatSolution

Create a solution with bounds [0, 10] for all variables

solution = FloatSolution([0, 0], [10, 10], 1) # 2 variables, 1 objective solution.variables = [5.0, 5.0] # Initial values

Apply random mutation with 50% probability

mutation = SimpleRandomMutation(probability=0.5) mutated = mutation.execute(solution)

Each variable has a 50% chance to be replaced with a random value in [0, 10]

Parameters:

Name Type Description Default
probability float

The probability of mutating each variable (0 ≤ p ≤ 1).

required

Raises:

Type Description
ValueError

If probability is not in [0,1].

Source code in src/jmetal/operator/mutation.py
def __init__(self, probability: float, rng: np.random.Generator | None = None):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")
    super().__init__(probability=probability)
    self.rng = rng if rng is not None else np.random.default_rng()

UniformMutation(probability, perturbation=0.5, repair_operator=None, rng=None)

Bases: Mutation[FloatSolution]

Implementation of a uniform mutation operator for real-valued solutions.

This operator adds a random perturbation uniformly distributed in [-perturbation/2, perturbation/2] to each variable with a given probability. The perturbation is scaled by the variable's range, making the operator scale-invariant to the problem's bounds.

Parameters:

Name Type Description Default
probability float

The probability of mutating each variable (0 ≤ p ≤ 1).

required
perturbation float

Controls the maximum relative perturbation size (must be > 0). - Smaller values (e.g., 0.1-0.5): Small, local perturbations - Larger values (e.g., 1.0-2.0): Larger, more exploratory perturbations

0.5

Raises:

Type Description
ValueError

If probability is not in [0,1] or perturbation is not positive.

Source code in src/jmetal/operator/mutation.py
def __init__(
    self,
    probability: float,
    perturbation: float = 0.5,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")
    if perturbation <= 0:
        raise ValueError("perturbation must be positive")

    super().__init__(probability=probability)
    self.perturbation = perturbation
    # Normalize repair operator and store RNG generator (np.random.Generator)
    self.repair_operator = ensure_float_repair(repair_operator)
    self.rng = rng if rng is not None else np.random.default_rng()

NonUniformMutation(probability, perturbation=0.5, max_iterations=1000, repair_operator=None, rng=None)

Bases: Mutation[FloatSolution]

Implementation of a non-uniform mutation operator for real-valued solutions.

This operator perturbs solutions in a way that the mutation strength decreases over time, allowing for more exploration in early generations and more exploitation in later generations. The mutation strength is controlled by the current iteration number relative to the maximum number of iterations.

The mutation follows the formula::

delta(t, y) = y * (r * (1 - t/T)^b - 1)  if r <= 0.5
delta(t, y) = y * (1 - r * (1 - t/T)^b)  if r > 0.5

where t is the current iteration, T is max_iterations, b is the perturbation index, r is a random number in [0, 1], and y is the variable's range.

The operator is particularly useful for: - Fine-tuning solutions in later generations - Problems requiring adaptive exploration/exploitation balance - Situations where solution precision increases over time

Parameters:

Name Type Description Default
probability float

The probability of mutating each variable (0 ≤ p ≤ 1).

required
perturbation float

Controls the perturbation strength (must be > 0). - Lower values (e.g., 1-5): Smoother decrease in mutation strength - Higher values (e.g., 5-20): Faster transition to smaller mutations

0.5
max_iterations int

The maximum number of iterations/generations (must be > 0). This is used to calculate the current progress (t/T).

1000
Example

from jmetal.operator import NonUniformMutation from jmetal.core.solution import FloatSolution

Create a solution with bounds [0, 10] for all variables

solution = FloatSolution([0, 0], [10, 10], 1) # 2 variables, 1 objective solution.variables = [5.0, 5.0] # Initial values

Create a non-uniform mutation operator

With 30% mutation probability, medium perturbation (5.0), and 1000 max iterations

mutation = NonUniformMutation(probability=0.3, perturbation=5.0, max_iterations=1000)

In early generations (e.g., iteration 10 of 1000)

mutation.current_iteration = 10 mutated_early = mutation.execute(solution)

In later generations (e.g., iteration 900 of 1000)

mutation.current_iteration = 900 mutated_late = mutation.execute(solution)

Later mutations will be much smaller in magnitude

Note

Remember to update current_iteration before each generation to ensure proper adaptation of the mutation strength.

Raises:

Type Description
ValueError

If probability is not in [0,1] or parameters are not positive.

Source code in src/jmetal/operator/mutation.py
def __init__(
    self,
    probability: float,
    perturbation: float = 0.5,
    max_iterations: int = 1000,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")
    if perturbation <= 0:
        raise ValueError("perturbation must be positive")
    if max_iterations <= 0:
        raise ValueError("max_iterations must be positive")

    super().__init__(probability=probability)
    self.perturbation = perturbation
    self.max_iterations = max_iterations
    self.current_iteration = 0
    # Normalize repair operator
    self.repair_operator = ensure_float_repair(repair_operator)
    self.rng = rng if rng is not None else np.random.default_rng()

execute(solution)

Execute the non-uniform mutation on a solution.

Parameters:

Name Type Description Default
solution FloatSolution

The solution to be mutated.

required

Returns:

Type Description
FloatSolution

The mutated solution.

Source code in src/jmetal/operator/mutation.py
def execute(self, solution: FloatSolution) -> FloatSolution:
    """Execute the non-uniform mutation on a solution.

    Args:
        solution: The solution to be mutated.

    Returns:
        The mutated solution.
    """
    Check.that(issubclass(type(solution), FloatSolution), "Solution type invalid")

    for i in range(len(solution.variables)):
        if self.rng.random() <= self.probability:
            current_value = solution.variables[i]

            # Calculate delta based on direction
            if self.rng.random() <= 0.5:
                delta = self.__delta(
                    solution.upper_bound[i] - current_value,
                    self.perturbation,
                )
            else:
                delta = self.__delta(
                    solution.lower_bound[i] - current_value,
                    self.perturbation,
                )

            # Apply mutation and repair
            new_value = current_value + delta
            new_value = self.repair_operator.repair_scalar(
                new_value, solution.lower_bound[i], solution.upper_bound[i]
            )
            solution._variables[i] = new_value

    return solution

set_current_iteration(current_iteration)

Set the current iteration number for controlling mutation strength.

Parameters:

Name Type Description Default
current_iteration int

The current iteration number (must be ≥ 0).

required
Source code in src/jmetal/operator/mutation.py
def set_current_iteration(self, current_iteration: int) -> None:
    """Set the current iteration number for controlling mutation strength.

    Args:
        current_iteration: The current iteration number (must be ≥ 0).
    """
    if current_iteration < 0:
        raise ValueError("current_iteration must be non-negative")
    self.current_iteration = current_iteration

get_name()

Get the name of the operator.

Returns:

Type Description
str

A string containing the operator name and parameters.

Source code in src/jmetal/operator/mutation.py
def get_name(self) -> str:
    """Get the name of the operator.

    Returns:
        A string containing the operator name and parameters.
    """
    return (
        f"Non-Uniform mutation (perturbation={self.perturbation}, "
        f"max_iter={self.max_iterations})"
    )

PermutationSwapMutation(probability, rng=None)

Bases: Mutation[PermutationSolution]

Implementation of a swap mutation operator for permutation solutions.

This operator randomly selects two distinct positions in the permutation and swaps their values. It is commonly used for permutation-based optimization problems like the Traveling Salesman Problem (TSP).

The mutation works by: 1. Randomly selecting two distinct positions in the permutation 2. Swapping the values at these positions 3. Only performing the swap with a given probability

Parameters:

Name Type Description Default
probability float

The probability of applying the mutation to a solution (0 ≤ p ≤ 1). If the probability is 1.0, the mutation is always applied.

required
Example

from jmetal.operator import PermutationSwapMutation from jmetal.core.solution import PermutationSolution

Create a permutation solution [0, 1, 2, 3, 4]

solution = PermutationSolution(5, 1) # 5 variables, 1 objective solution.variables = [0, 1, 2, 3, 4]

Apply swap mutation with 100% probability

mutation = PermutationSwapMutation(probability=1.0) mutated = mutation.execute(solution)

Two random positions will be swapped, e.g., [2, 1, 0, 3, 4]

Source code in src/jmetal/operator/mutation.py
def __init__(self, probability: float, rng: np.random.Generator | None = None):
    super().__init__(probability=probability)
    self.rng = rng if rng is not None else np.random.default_rng()

CompositeMutation(mutation_operator_list)

Bases: Mutation[Solution]

A composite mutation operator that applies different mutation operators to different solution components.

This operator is particularly useful for composite solutions where each component may require a different mutation strategy. It maintains a list of mutation operators, one for each component of the composite solution.

The mutation works by: 1. Taking a composite solution as input 2. Applying each mutation operator to the corresponding solution component 3. Combining the results into a new composite solution

Parameters:

Name Type Description Default
mutation_operator_list list[Mutation]

A list of mutation operators, one for each component of the composite solution. The length of this list must match the number of variables in the composite solution.

required

Raises:

Type Description
ValueError

If the mutation_operator_list is empty or None.

TypeError

If any element in mutation_operator_list is not a subclass of Mutation.

Example

from jmetal.operator import CompositeMutation, BitFlipMutation, PolynomialMutation from jmetal.core.solution import CompositeSolution, BinarySolution, FloatSolution

Create a composite solution with binary and float components

binary_solution = BinarySolution(5, 1) # 5 bits, 1 objective float_solution = FloatSolution([0]3, [1]3, 1) # 3 variables, 1 objective composite = CompositeSolution([binary_solution, float_solution])

Create a composite mutation with appropriate operators for each component

mutation = CompositeMutation([ ... BitFlipMutation(0.1), # For binary component ... PolynomialMutation(0.1, 20) # For float component ... ])

Apply the composite mutation

mutated = mutation.execute(composite)

Source code in src/jmetal/operator/mutation.py
def __init__(self, mutation_operator_list: list[Mutation]):
    super().__init__(probability=1.0)

    Check.is_not_none(mutation_operator_list)
    Check.collection_is_not_empty(mutation_operator_list)

    self.mutation_operators_list = []
    for operator in mutation_operator_list:
        Check.that(
            issubclass(operator.__class__, Mutation), "Object is not a subclass of Mutation"
        )
        self.mutation_operators_list.append(operator)

get_name()

Get the name of the operator.

Returns:

Name Type Description
str str

A string containing the operator name and the names of the component operators.

Source code in src/jmetal/operator/mutation.py
def get_name(self) -> str:
    """Get the name of the operator.

    Returns:
        str: A string containing the operator name and the names of the component operators.
    """
    operator_names = [op.get_name() for op in self.mutation_operators_list]
    return f"Composite mutation ({', '.join(operator_names)})"

ScrambleMutation(probability, rng=None)

Bases: Mutation[PermutationSolution]

Implementation of a scramble mutation operator for permutation solutions.

This operator selects a random subsequence of the permutation and randomly reorders (scrambles) the elements within that subsequence. It is particularly useful for permutation problems where the relative ordering of elements is important.

The mutation works by: 1. Randomly selecting a subsequence of the permutation (limited to max 20 elements) 2. Randomly shuffling the elements within this subsequence 3. Only performing the scramble with a given probability

Parameters:

Name Type Description Default
probability float

The probability of applying the mutation to a solution (0 ≤ p ≤ 1). If the probability is 1.0, the mutation is always applied.

required
Example

from jmetal.operator import ScrambleMutation from jmetal.core.solution import PermutationSolution

Create a permutation solution [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

solution = PermutationSolution(10, 1) # 10 variables, 1 objective solution.variables = list(range(10))

Apply scramble mutation with 100% probability

mutation = ScrambleMutation(probability=1.0) mutated = mutation.execute(solution)

A random subsequence will be scrambled, e.g., [0, 1, 4, 3, 2, 5, 6, 7, 8, 9]

Source code in src/jmetal/operator/mutation.py
def __init__(self, probability: float, rng: np.random.Generator | None = None):
    super().__init__(probability=probability)
    self.rng = rng if rng is not None else np.random.default_rng()

get_name()

Get the name of the operator.

Returns:

Name Type Description
str str

A string containing the operator name.

Source code in src/jmetal/operator/mutation.py
def get_name(self) -> str:
    """Get the name of the operator.

    Returns:
        str: A string containing the operator name.
    """
    return "Scramble mutation"

LevyFlightMutation(mutation_probability=0.01, beta=1.5, step_size=0.01, repair_operator=None, rng=None)

Bases: Mutation[FloatSolution]

Implementation of a Lévy flight mutation operator for real-valued solutions.

Lévy flights are characterized by heavy-tailed distributions with infinite variance, producing mostly small steps with occasional very large jumps. This behavior is beneficial for global optimization as it provides both local search capabilities and the ability to escape local optima through large jumps.

The implementation uses the Mantegna algorithm to generate Lévy-distributed steps:

  1. Generate u from a normal distribution scaled by a factor derived from the beta parameter (via the gamma function).
  2. Generate v from a standard normal distribution.
  3. Compute the Lévy step as u divided by the absolute value of v raised to the power 1 / beta.

Parameters:

Name Type Description Default
mutation_probability float

The probability of mutating each variable (0 ≤ p ≤ 1).

0.01
beta float

The Lévy index parameter (1 < β ≤ 2). Controls the tail heaviness: - Values closer to 1.0 produce heavier tails with more frequent large jumps - Values around 1.5 provide balanced exploration (default) - Values closer to 2.0 approach Gaussian behavior with fewer large jumps

1.5
step_size float

The scaling factor for Lévy steps (must be > 0). Typical values: - 0.001-0.01: Fine-grained local search - 0.01-0.05: Balance of local and global search (default: 0.01) - 0.05-0.1: Emphasize global exploration

0.01
repair_operator Callable[[float, float, float], float] | None

Optional function to repair out-of-bounds values. If None, values are clamped to the variable bounds.

None

Raises:

Type Description
ValueError

If parameters are outside their valid ranges.

Source code in src/jmetal/operator/mutation.py
def __init__(
    self,
    mutation_probability: float = 0.01,
    beta: float = 1.5,
    step_size: float = 0.01,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= mutation_probability <= 1:
        raise ValueError("mutation_probability must be in [0, 1]")
    if not 1 < beta <= 2:
        raise ValueError("beta must be in (1, 2]")
    if step_size <= 0:
        raise ValueError("step_size must be positive")

    super().__init__(probability=mutation_probability)
    self.beta = beta
    self.step_size = step_size
    # Normalize repair operator to a FloatRepairOperator instance
    self.repair_operator = ensure_float_repair(repair_operator)
    self.rng = rng if rng is not None else np.random.default_rng()

PowerLawMutation(probability=0.01, delta=1.0, repair_operator=None, rng=None)

Bases: Mutation[FloatSolution]

Implementation of a power-law mutation operator for real-valued solutions.

The power-law distribution produces heavy-tailed perturbations that can occasionally create large jumps while favoring smaller perturbations, which is beneficial for both exploration and exploitation in optimization.

The mutation follows the formula::

temp_delta = rnd^(-delta)
deltaq = 0.5 * (rnd - 0.5) * (1 - temp_delta)
new_value = old_value + deltaq * (upper_bound - lower_bound)

Parameters:

Name Type Description Default
probability float

The probability of mutating each variable (0 ≤ p ≤ 1).

0.01
delta float

The power-law exponent parameter (must be > 0). Controls distribution shape: values below 1.0 give more uniform distributions with moderate perturbations, values near 1.0 balance exploration and exploitation (the default), and values above 1.0 give heavy-tailed distributions favoring small perturbations with occasional large jumps.

1.0
repair_operator Callable[[float, float, float], float] | None

Optional function to repair out-of-bounds values. If None, values are clamped to the variable bounds.

None

Raises:

Type Description
ValueError

If probability is not in [0,1] or delta is not positive.

Source code in src/jmetal/operator/mutation.py
def __init__(
    self,
    probability: float = 0.01,
    delta: float = 1.0,
    repair_operator: Callable[[float, float, float], float] | None = None,
    rng: np.random.Generator | None = None,
):
    if not 0 <= probability <= 1:
        raise ValueError("probability must be in [0, 1]")
    if delta <= 0:
        raise ValueError("delta must be positive")

    super().__init__(probability=probability)
    self.delta = delta
    # Normalize repair operator to a FloatRepairOperator instance
    self.repair_operator = ensure_float_repair(
        repair_operator if repair_operator is not None else None
    )
    self.rng = rng if rng is not None else np.random.default_rng()

Selection

selection

S = TypeVar('S', bound=Solution) module-attribute

.. module:: selection :platform: Unix, Windows :synopsis: Module implementing selection operators.

.. moduleauthor:: Antonio J. Nebro antonio@lcc.uma.es, Antonio Benítez-Hidalgo antonio.b@uma.es

RouletteWheelSelection(objective_index=0, rng=None)

Bases: Selection[list[S], S]

Performs roulette wheel selection.

This selection operator selects solutions based on their fitness values using a roulette wheel mechanism. It can handle both single and multi-objective optimization by using the first objective value for selection. For multi-objective optimization, consider using a proper fitness assignment strategy first.

Note: This implementation assumes all objective values are non-negative. If negative values are present, a proper normalization should be applied first.

Initialize the roulette wheel selection operator.

Parameters:

Name Type Description Default
objective_index int

Index of the objective to use for selection (default: 0). Only used if no fitness value is present in the solution attributes.

0
rng Generator | None

Optional random generator. When None, falls back to the global random/ numpy.random modules, as before this parameter existed.

None
Source code in src/jmetal/operator/selection.py
def __init__(self, objective_index: int = 0, rng: np.random.Generator | None = None):
    """Initialize the roulette wheel selection operator.

    Args:
        objective_index: Index of the objective to use for selection (default: 0).
                        Only used if no fitness value is present in the solution attributes.
        rng: Optional random generator. When None, falls back to the global `random`/
             `numpy.random` modules, as before this parameter existed.
    """
    super().__init__()
    self.objective_index = objective_index
    self.rng = rng

execute(front)

Select a solution using roulette wheel selection.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
S

The selected solution.

Raises:

Type Description
ValueError

If the front is None, empty, or contains invalid fitness values.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> S:
    """Select a solution using roulette wheel selection.

    Args:
        front: List of solutions to select from.

    Returns:
        The selected solution.

    Raises:
        ValueError: If the front is None, empty, or contains invalid fitness values.
    """
    if not front:
        raise ValueError("The front is empty")

    # Calculate fitness values (using first objective if no fitness attribute)
    fitness_values = []
    for solution in front:
        if hasattr(solution, "fitness") and solution.fitness is not None:
            fitness_values.append(solution.fitness)
        else:
            # Fallback to using the specified objective
            fitness_values.append(solution.objectives[self.objective_index])

    # Convert to numpy array for efficient operations
    fitness_values = np.array(fitness_values, dtype=float)

    # Check for invalid fitness values
    if np.any(fitness_values < 0):
        raise ValueError(
            "Negative fitness values are not supported. "
            "Consider normalizing the fitness values first."
        )

    # If all values are zero, return a random solution
    total_fitness = np.sum(fitness_values)
    if total_fitness <= 0:
        if self.rng is not None:
            return front[int(self.rng.integers(0, len(front)))]
        return random.choice(front)

    # Calculate selection probabilities
    probabilities = fitness_values / total_fitness

    # Select a solution based on the probabilities
    if self.rng is not None:
        selected_index = self.rng.choice(len(front), p=probabilities)
    else:
        selected_index = np.random.choice(len(front), p=probabilities)
    return front[selected_index]

TournamentSelection(tournament_size=2, comparator=DominanceComparator(), rng=None)

Bases: Selection[list[S], S]

Performs k-ary tournament selection.

This selection operator randomly selects k solutions from the population and returns the best one according to the provided comparator. It's a generalization of binary tournament selection that allows controlling selection pressure through the tournament size.

A larger tournament size (k) increases selection pressure, favoring better solutions more strongly. A smaller k provides more diversity but slower convergence.

Parameters:

Name Type Description Default
tournament_size int

Number of solutions to participate in each tournament (default: 2). Must be at least 2.

2
comparator Comparator

Comparator used to compare solutions (default: DominanceComparator).

DominanceComparator()
rng Generator | None

Optional random generator. When None, falls back to the global random module (random.sample/random.random), as before this parameter existed.

None
Example

from jmetal.operator import TournamentSelection from jmetal.util.comparator import DominanceComparator

Create a tournament selection with size 5

selector = TournamentSelection(tournament_size=5)

Select from a population

winner = selector.execute(population)

Source code in src/jmetal/operator/selection.py
def __init__(
    self,
    tournament_size: int = 2,
    comparator: Comparator = DominanceComparator(),
    rng: np.random.Generator | None = None,
):
    super().__init__()
    if tournament_size < 2:
        raise ValueError(f"Tournament size must be at least 2, got {tournament_size}")
    self.tournament_size = tournament_size
    self.comparator = comparator
    self.rng = rng

execute(front)

Execute the k-ary tournament selection.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
S

The best solution among the tournament participants.

Raises:

Type Description
ValueError

If front is None, empty, or smaller than tournament size.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> S:
    """Execute the k-ary tournament selection.

    Args:
        front: List of solutions to select from.

    Returns:
        The best solution among the tournament participants.

    Raises:
        ValueError: If front is None, empty, or smaller than tournament size.
    """
    if not front:
        raise ValueError("The front is empty")

    if len(front) == 1:
        return front[0]

    # Adjust tournament size if population is smaller
    effective_size = min(self.tournament_size, len(front))

    # Sample k solutions without replacement
    if self.rng is not None:
        tournament_indices = self.rng.choice(len(front), size=effective_size, replace=False)
    else:
        tournament_indices = random.sample(range(len(front)), effective_size)
    tournament_solutions = [front[i] for i in tournament_indices]

    # Find the best solution in the tournament
    winner = tournament_solutions[0]
    for i in range(1, len(tournament_solutions)):
        candidate = tournament_solutions[i]
        comparison = self.comparator.compare(candidate, winner)
        if comparison < 0:  # candidate is better
            winner = candidate
        elif comparison == 0:  # tie - randomly decide
            tie_break = self.rng.random() if self.rng is not None else random.random()
            if tie_break < 0.5:
                winner = candidate

    return winner

BinaryTournamentSelection(comparator=DominanceComparator(), rng=None)

Bases: TournamentSelection

Performs binary tournament selection between two random solutions.

This is a specialization of TournamentSelection with tournament_size=2. It randomly selects two solutions from the population and returns the better one according to the provided comparator. If the comparator returns 0 (tie), a random solution is chosen.

This class is provided for convenience and backward compatibility. For more control over tournament size, use TournamentSelection directly.

Parameters:

Name Type Description Default
comparator Comparator

Comparator used to compare solutions (default: DominanceComparator).

DominanceComparator()
rng Generator | None

Optional random generator. When None, falls back to the global random module, as before this parameter existed.

None
Example

from jmetal.operator import BinaryTournamentSelection from jmetal.util.comparator import DominanceComparator

Create binary tournament selection

selector = BinaryTournamentSelection()

Or with a custom comparator

selector = BinaryTournamentSelection(comparator=DominanceComparator())

Select from a population

winner = selector.execute(population)

Source code in src/jmetal/operator/selection.py
def __init__(
    self,
    comparator: Comparator = DominanceComparator(),
    rng: np.random.Generator | None = None,
):
    super().__init__(tournament_size=2, comparator=comparator, rng=rng)

BestSolutionSelection()

Bases: Selection[list[S], S]

Selects the best solution from a population based on dominance comparison.

This selection operator returns the non-dominated solution from the population. If multiple solutions are non-dominated with respect to each other, it returns the first one encountered in the front.

The comparison is done using the DominanceComparator, which follows these rules:

  • Solution A dominates solution B if A is not worse than B in all objectives and A is strictly better than B in at least one objective.
  • If neither solution dominates the other, they are considered non-dominated.
Example

from jmetal.operator import BestSolutionSelection from jmetal.core.solution import FloatSolution

Create a population of solutions

solution1 = FloatSolution([0], [1], 2) # 2 objectives solution1.objectives = [0.5, 0.8] solution2 = FloatSolution([0], [1], 2) solution2.objectives = [0.3, 0.9] population = [solution1, solution2]

Select the best solution

selector = BestSolutionSelection() best_solution = selector.execute(population)

Initialize the best solution selector.

Source code in src/jmetal/operator/selection.py
def __init__(self):
    """Initialize the best solution selector."""
    super().__init__()

execute(front)

Select the best solution from the front.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
S

The best solution in the front according to dominance comparison.

Raises:

Type Description
ValueError

If front is None or empty.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> S:
    """Select the best solution from the front.

    Args:
        front: List of solutions to select from.

    Returns:
        The best solution in the front according to dominance comparison.

    Raises:
        ValueError: If front is None or empty.
    """
    if front is None:
        raise ValueError("The front is None")
    if not front:
        raise ValueError("The front is empty")

    result = front[0]

    for solution in front[1:]:
        if DominanceComparator().compare(solution, result) < 0:
            result = solution

    return result

get_name()

Get the name of the selection operator.

Returns:

Type Description
str

A string representing the name of the selection operator.

Source code in src/jmetal/operator/selection.py
def get_name(self) -> str:
    """Get the name of the selection operator.

    Returns:
        A string representing the name of the selection operator.
    """
    return "Best solution selection"

NaryRandomSolutionSelection(number_of_solutions_to_be_returned=1, rng=None)

Bases: Selection[list[S], list[S]]

Performs random selection of multiple solutions from a population.

This selection operator randomly selects a specified number of distinct solutions from the population with uniform probability. The selection is done without replacement, meaning each solution can be selected at most once.

Parameters:

Name Type Description Default
number_of_solutions_to_be_returned int

Number of distinct solutions to select (default: 1). Must be a positive integer.

1
rng Generator | None

Optional random generator. When None, falls back to the global random module (random.choice/random.sample), as before this parameter existed.

None
Example

from jmetal.operator import NaryRandomSolutionSelection

Select 3 random solutions

selector = NaryRandomSolutionSelection(number_of_solutions_to_be_returned=3) selected = selector.execute(population) # Returns List[S] with 3 solutions

Source code in src/jmetal/operator/selection.py
def __init__(
    self,
    number_of_solutions_to_be_returned: int = 1,
    rng: np.random.Generator | None = None,
):
    super().__init__()
    if number_of_solutions_to_be_returned < 1:
        raise ValueError(
            f"The number of solutions to be returned must be a positive integer, got {number_of_solutions_to_be_returned}"
        )

    self.number_of_solutions_to_be_returned = number_of_solutions_to_be_returned
    self.rng = rng

execute(front)

Randomly select multiple solutions from the front.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
list[S]

A list of randomly selected solutions from the front.

Raises:

Type Description
ValueError

If front is None, empty, or has fewer solutions than requested.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> list[S]:
    """Randomly select multiple solutions from the front.

    Args:
        front: List of solutions to select from.

    Returns:
        A list of randomly selected solutions from the front.

    Raises:
        ValueError: If front is None, empty, or has fewer solutions than requested.
    """
    if front is None:
        raise ValueError("The front is None")
    if not front:
        raise ValueError("The front is empty")
    if len(front) < self.number_of_solutions_to_be_returned:
        raise ValueError(
            f"The front size ({len(front)}) is smaller than the number of requested solutions: {self.number_of_solutions_to_be_returned}"
        )

    if self.rng is not None:
        indexes = self.rng.choice(
            len(front), size=self.number_of_solutions_to_be_returned, replace=False
        )
        return [front[i] for i in indexes]

    # Optimization: use random.choice for single selection
    if self.number_of_solutions_to_be_returned == 1:
        return [random.choice(front)]

    return random.sample(front, self.number_of_solutions_to_be_returned)

get_name()

Get the name of the selection operator.

Returns:

Type Description
str

A string representing the name of the selection operator.

Source code in src/jmetal/operator/selection.py
def get_name(self) -> str:
    """Get the name of the selection operator.

    Returns:
        A string representing the name of the selection operator.
    """
    return "N-ary random solution selection"

DifferentialEvolutionSelection(index_to_exclude=None, rng=None)

Bases: Selection[list[S], list[S]]

Performs selection for differential evolution algorithms.

This selection operator is specifically designed for differential evolution algorithms. It selects three distinct solutions from the population, with an optional index to exclude (typically the current solution's index to avoid self-selection).

Parameters:

Name Type Description Default
index_to_exclude int

Optional index of a solution to exclude from selection. This is useful to avoid selecting the same solution as the base vector.

None
rng Generator | None

Optional random generator. When None, falls back to the global random module (random.sample), as before this parameter existed.

None
Source code in src/jmetal/operator/selection.py
def __init__(self, index_to_exclude: int = None, rng: np.random.Generator | None = None):
    super().__init__()
    self.index_to_exclude = index_to_exclude
    self.rng = rng

execute(front)

Select three distinct solutions for differential evolution.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
list[S]

A list containing three distinct solutions from the front.

Raises:

Type Description
ValueError

If front is None, empty, or has fewer than 4 solutions.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> list[S]:
    """Select three distinct solutions for differential evolution.

    Args:
        front: List of solutions to select from.

    Returns:
        A list containing three distinct solutions from the front.

    Raises:
        ValueError: If front is None, empty, or has fewer than 4 solutions.
    """
    if front is None:
        raise ValueError("The front is null")
    elif len(front) == 0:
        raise ValueError("The front is empty")
    elif len(front) < 4:
        raise ValueError(
            f"Differential evolution selection requires at least 4 solutions, got {len(front)}"
        )

    # If there's an index to exclude, create a new list without it
    if self.index_to_exclude is not None and 0 <= self.index_to_exclude < len(front):
        candidates = [sol for i, sol in enumerate(front) if i != self.index_to_exclude]
    else:
        candidates = list(front)

    # Check if we have enough candidates after exclusion
    if len(candidates) < 3:
        raise ValueError(
            f"Not enough candidates to select from (need 3, have {len(candidates)} after exclusion)"
        )

    # Randomly select 3 distinct solutions from the remaining candidates
    if self.rng is not None:
        indexes = self.rng.choice(len(candidates), size=3, replace=False)
        selected = [candidates[i] for i in indexes]
    else:
        selected = random.sample(candidates, 3)

    return selected

set_index_to_exclude(index)

Set the index of the solution to exclude from selection.

Parameters:

Name Type Description Default
index int

Index of the solution to exclude. Can be None to disable exclusion.

required
Source code in src/jmetal/operator/selection.py
def set_index_to_exclude(self, index: int) -> None:
    """Set the index of the solution to exclude from selection.

    Args:
        index: Index of the solution to exclude. Can be None to disable exclusion.
    """
    self.index_to_exclude = index

get_name()

Get the name of the selection operator.

Returns:

Type Description
str

A string representing the name of the selection operator.

Source code in src/jmetal/operator/selection.py
def get_name(self) -> str:
    """Get the name of the selection operator.

    Returns:
        A string representing the name of the selection operator.
    """
    return "Differential evolution selection"

RandomSelection(rng=None)

Bases: Selection[list[S], S]

Performs random selection of a solution from a population.

This selection operator randomly selects a single solution from the provided population with uniform probability. It's a simple selection method that doesn't consider solution quality.

Parameters:

Name Type Description Default
rng Generator | None

Optional random generator. When None, falls back to the global random module (random.choice), as before this parameter existed.

None
Source code in src/jmetal/operator/selection.py
def __init__(self, rng: np.random.Generator | None = None):
    super().__init__()
    self.rng = rng

execute(front)

Randomly select a solution from the front.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
S

A randomly selected solution from the front.

Raises:

Type Description
ValueError

If front is None or empty.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> S:
    """Randomly select a solution from the front.

    Args:
        front: List of solutions to select from.

    Returns:
        A randomly selected solution from the front.

    Raises:
        ValueError: If front is None or empty.
    """
    if front is None:
        raise ValueError("The front is None")
    elif len(front) == 0:
        raise ValueError("The front is empty")

    if not isinstance(front, list):
        raise ValueError("The front must be a list")

    # Check if all elements are instances of the same type as the first element
    if front and not all(isinstance(solution, front[0].__class__) for solution in front):
        raise ValueError("All elements in the front must be of the same type")

    if self.rng is not None:
        return front[int(self.rng.integers(0, len(front)))]
    return random.choice(front)

RankingAndCrowdingDistanceSelection(max_population_size, dominance_comparator=DominanceComparator())

Bases: Selection[list[S], list[S]]

Performs selection based on non-dominated ranking and crowding distance.

This selection operator first ranks the solutions using non-dominated sorting and then applies crowding distance to maintain diversity within each rank. It's commonly used in NSGA-II and other multi-objective evolutionary algorithms.

Parameters:

Name Type Description Default
max_population_size int

Maximum number of solutions to select.

required
dominance_comparator Comparator

Comparator used for non-dominated sorting. Defaults to DominanceComparator().

DominanceComparator()
Source code in src/jmetal/operator/selection.py
def __init__(
    self, max_population_size: int, dominance_comparator: Comparator = DominanceComparator()
):
    super().__init__()
    self.max_population_size = max_population_size
    self.dominance_comparator = dominance_comparator

execute(front)

Select solutions using non-dominated ranking and crowding distance.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
list[S]

A list of selected solutions, with size up to max_population_size.

Raises:

Type Description
ValueError

If front is None, empty, or max_population_size is invalid.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> list[S]:
    """Select solutions using non-dominated ranking and crowding distance.

    Args:
        front: List of solutions to select from.

    Returns:
        A list of selected solutions, with size up to max_population_size.

    Raises:
        ValueError: If front is None, empty, or max_population_size is invalid.
    """
    if front is None:
        raise ValueError("The front is None")
    if not front:
        raise ValueError("The front is empty")
    if not isinstance(self.max_population_size, int) or self.max_population_size <= 0:
        raise ValueError("max_population_size must be a positive integer")

    # If the front is smaller than max_population_size, return the entire front
    if len(front) <= self.max_population_size:
        return front.copy()

    ranking: FastNonDominatedRanking[S] = FastNonDominatedRanking(self.dominance_comparator)
    crowding_distance: CrowdingDistanceDensityEstimator[S] = CrowdingDistanceDensityEstimator()
    ranking.compute_ranking(front)

    ranking_index = 0
    new_solution_list: list[S] = []
    number_of_subfronts = ranking.get_number_of_subfronts()

    while (
        len(new_solution_list) < self.max_population_size
        and ranking_index < number_of_subfronts
    ):
        subfront = ranking.get_subfront(ranking_index)

        # If adding the entire subfront doesn't exceed max_population_size, add it all
        if len(new_solution_list) + len(subfront) <= self.max_population_size:
            new_solution_list.extend(subfront)
        else:
            # Otherwise, sort by crowding distance and add the best remaining solutions
            crowding_distance.compute_density_estimator(subfront)
            # Sort by crowding distance in descending order
            sorted_subfront = sorted(
                subfront, key=lambda x: x.attributes.get("crowding_distance", 0.0), reverse=True
            )
            # Take only as many as needed to fill the population
            remaining = self.max_population_size - len(new_solution_list)
            new_solution_list.extend(sorted_subfront[:remaining])

        ranking_index += 1

    return new_solution_list

get_name()

Get the name of the selection operator.

Returns:

Type Description
str

A string representing the name of the selection operator.

Source code in src/jmetal/operator/selection.py
def get_name(self) -> str:
    """Get the name of the selection operator.

    Returns:
        A string representing the name of the selection operator.
    """
    return "Ranking and crowding distance selection"

RankingAndFitnessSelection(max_population_size, reference_point, dominance_comparator=DominanceComparator())

Bases: Selection[list[S], list[S]]

Performs selection based on non-dominated ranking and hypervolume contribution.

This selection operator first ranks the solutions using non-dominated sorting and then applies hypervolume contribution to maintain diversity within each rank. It's commonly used in multi-objective evolutionary algorithms that aim to maximize the hypervolume indicator.

Parameters:

Name Type Description Default
max_population_size int

Maximum number of solutions to select.

required
reference_point S

Reference point used for hypervolume calculation. Should be dominated by all solutions.

required
dominance_comparator Comparator

Comparator used for non-dominated sorting. Defaults to DominanceComparator().

DominanceComparator()
Source code in src/jmetal/operator/selection.py
def __init__(
    self,
    max_population_size: int,
    reference_point: S,
    dominance_comparator: Comparator = DominanceComparator(),
):
    super().__init__()
    self.max_population_size = max_population_size
    self.dominance_comparator = dominance_comparator
    self.reference_point = reference_point

hypesub(l, A, actDim, bounds, pvec, alpha, k)

Recursively compute hypervolume contributions.

This is a helper method for hypervolume calculation. It's an implementation of the Hype algorithm for hypervolume approximation.

Parameters:

Name Type Description Default
l int

Number of points.

required
A list[list[float]]

List of objective vectors.

required
actDim int

Current dimension being processed.

required
bounds list[float]

Reference point coordinates.

required
pvec list[int]

Indices of points in A.

required
alpha list[float]

Weighting factors for hypervolume contribution.

required
k int

Number of points to consider.

required

Returns:

Type Description
list[float]

List of hypervolume contributions for each point.

Source code in src/jmetal/operator/selection.py
def hypesub(
    self,
    l: int,
    A: list[list[float]],
    actDim: int,
    bounds: list[float],
    pvec: list[int],
    alpha: list[float],
    k: int,
) -> list[float]:
    """Recursively compute hypervolume contributions.

    This is a helper method for hypervolume calculation. It's an implementation
    of the Hype algorithm for hypervolume approximation.

    Args:
        l: Number of points.
        A: List of objective vectors.
        actDim: Current dimension being processed.
        bounds: Reference point coordinates.
        pvec: Indices of points in A.
        alpha: Weighting factors for hypervolume contribution.
        k: Number of points to consider.

    Returns:
        List of hypervolume contributions for each point.
    """
    h = [0 for _ in range(l)]
    Adim = [a[actDim - 1] for a in A]
    indices_sort = sorted(range(len(Adim)), key=Adim.__getitem__)
    S = [A[j] for j in indices_sort]
    pvec = [pvec[j] for j in indices_sort]

    for i in range(1, len(S) + 1):
        if i < len(S):
            extrusion = S[i][actDim - 1] - S[i - 1][actDim - 1]
        else:
            extrusion = bounds[actDim - 1] - S[i - 1][actDim - 1]

        if actDim == 1:
            if i > k:
                break
            if all(alpha) >= 0:
                for p in pvec[0:i]:
                    h[p] = h[p] + extrusion * alpha[i - 1]
        else:
            if extrusion > 0:
                h = [
                    h[j]
                    + extrusion
                    * self.hypesub(l, S[0:i], actDim - 1, bounds, pvec[0:i], alpha, k)[j]
                    for j in range(l)
                ]

    return h

compute_hypervol_fitness_values(population, reference_point, k)

Compute hypervolume-based fitness values for a population.

This method computes the hypervolume contribution of each solution in the population and stores it in the solution's attributes as 'fitness'.

Parameters:

Name Type Description Default
population list[S]

List of solutions to evaluate.

required
reference_point S

Reference point for hypervolume calculation.

required
k int

Number of points to consider for hypervolume approximation. If negative, uses the entire population size.

required

Returns:

Type Description
list[S]

The input population with updated fitness values in their attributes.

Source code in src/jmetal/operator/selection.py
def compute_hypervol_fitness_values(
    self, population: list[S], reference_point: S, k: int
) -> list[S]:
    """Compute hypervolume-based fitness values for a population.

    This method computes the hypervolume contribution of each solution in the
    population and stores it in the solution's attributes as 'fitness'.

    Args:
        population: List of solutions to evaluate.
        reference_point: Reference point for hypervolume calculation.
        k: Number of points to consider for hypervolume approximation.
            If negative, uses the entire population size.

    Returns:
        The input population with updated fitness values in their attributes.
    """
    points = [ind.objectives for ind in population]
    bounds = reference_point.objectives
    population_size = len(points)

    if k < 0:
        k = population_size

    actDim = len(bounds)
    pvec = range(population_size)
    alpha = []

    # Calculate alpha values for weighted hypervolume contribution
    for i in range(1, k + 1):
        alpha.append(np.prod([float(k - j) / (population_size - j) for j in range(1, i)]) / i)

    # Compute hypervolume contributions
    f = self.hypesub(population_size, points, actDim, bounds, pvec, alpha, k)

    # Store fitness values in solution attributes
    for i in range(len(population)):
        if not hasattr(population[i], "attributes") or population[i].attributes is None:
            population[i].attributes = {}
        population[i].attributes["fitness"] = f[i]

    return population

execute(front)

Select solutions using non-dominated ranking and hypervolume contribution.

This method first performs non-dominated sorting of the input front. It then fills the new population with solutions from the best ranks, using hypervolume contribution to select solutions when a rank needs to be split.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
list[S]

A list of selected solutions, with size equal to max_population_size.

Raises:

Type Description
ValueError

If front is None or empty.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> list[S]:
    """Select solutions using non-dominated ranking and hypervolume contribution.

    This method first performs non-dominated sorting of the input front.
    It then fills the new population with solutions from the best ranks,
    using hypervolume contribution to select solutions when a rank needs to be split.

    Args:
        front: List of solutions to select from.

    Returns:
        A list of selected solutions, with size equal to max_population_size.

    Raises:
        ValueError: If front is None or empty.
    """
    if front is None:
        raise ValueError("The front is None")
    elif len(front) == 0:
        raise ValueError("The front is empty")

    # Perform non-dominated sorting
    ranking: FastNonDominatedRanking[S] = FastNonDominatedRanking(self.dominance_comparator)
    ranking.compute_ranking(front)

    ranking_index = 0
    new_solution_list: list[S] = []

    # Fill the new population with solutions from the best ranks
    while len(new_solution_list) < self.max_population_size:
        current_rank = ranking.get_subfront(ranking_index)

        # If we can take all solutions from this rank without exceeding max_population_size
        if len(current_rank) <= self.max_population_size - len(new_solution_list):
            new_solution_list.extend(current_rank)
            ranking_index += 1
        else:
            # Need to select a subset of this rank using hypervolume contribution
            remaining_slots = self.max_population_size - len(new_solution_list)
            parameter_K = len(current_rank) - remaining_slots

            # Remove the worst solutions based on hypervolume contribution
            while parameter_K > 0:
                current_rank = self.compute_hypervol_fitness_values(
                    current_rank, self.reference_point, parameter_K
                )
                # Sort by fitness (hypervolume contribution) in descending order
                current_rank = sorted(
                    current_rank, key=lambda x: x.attributes.get("fitness", 0), reverse=True
                )
                # Remove the solution with the lowest contribution
                current_rank = current_rank[:-1]
                parameter_K -= 1

            new_solution_list.extend(current_rank)

    return new_solution_list

get_name()

Get the name of the selection operator.

Returns:

Type Description
str

A string representing the name of the selection operator.

Source code in src/jmetal/operator/selection.py
def get_name(self) -> str:
    """Get the name of the selection operator.

    Returns:
        A string representing the name of the selection operator.
    """
    return "Ranking and hypervolume-based selection"

BinaryTournament2Selection(comparator_list, rng=None)

Bases: Selection[list[S], S]

Performs binary tournament selection with multiple comparators.

This selection operator uses a list of comparators in sequence to determine the winner between two randomly selected solutions. The first comparator that can determine a winner is used. If all comparators result in a tie, a random solution is chosen.

Parameters:

Name Type Description Default
comparator_list list[Comparator]

List of comparators to use in sequence.

required
rng Generator | None

Optional random generator. When None, falls back to the global random module (random.sample/random.randint), as before this parameter existed.

None
Source code in src/jmetal/operator/selection.py
def __init__(
    self, comparator_list: list[Comparator], rng: np.random.Generator | None = None
):
    super().__init__()
    if not comparator_list:
        raise ValueError("The comparator list cannot be empty")
    self.comparator_list = comparator_list
    self.rng = rng

execute(front)

Execute the binary tournament selection with multiple comparators.

Parameters:

Name Type Description Default
front list[S]

List of solutions to select from.

required

Returns:

Type Description
S

The selected solution.

Raises:

Type Description
ValueError

If front is None, empty, or contains only one solution.

Source code in src/jmetal/operator/selection.py
def execute(self, front: list[S]) -> S:
    """Execute the binary tournament selection with multiple comparators.

    Args:
        front: List of solutions to select from.

    Returns:
        The selected solution.

    Raises:
        ValueError: If front is None, empty, or contains only one solution.
    """
    if front is None:
        raise ValueError("The front is None")

    if not front:
        raise ValueError("The front is empty")

    if len(front) == 1:
        return front[0]

    # Use the first comparator to get initial winner
    result = self.__winner(front, self.comparator_list[0])

    # If first comparator couldn't decide, try the rest
    if result is None and len(self.comparator_list) > 1:
        for comparator in self.comparator_list[1:]:
            result = self.__winner(front, comparator)
            if result is not None:
                break

    # If no comparator could decide, choose randomly
    if result is None:
        idx = (
            int(self.rng.integers(0, len(front)))
            if self.rng is not None
            else random.randint(0, len(front) - 1)
        )
        result = front[idx]

    return result

Repair

repair

FloatRepairOperator

Base interface for repair operators that work on continuous (float) variables.

Implementations should provide two APIs: - repair_scalar(value, lb, ub) repairs a single float value. - repair_vector(values, lbs, ubs) repairs arrays element-wise and returns a numpy array.

A callable adapter (ensure_float_repair) is provided so that existing code that passes simple scalar callables continues to work. The default repair_vector implementation applies repair_scalar element-wise.

ClampFloatRepair

Bases: FloatRepairOperator

Default clamp-to-bounds repair for float variables.

This implementation reproduces the common min(max(x, lb), ub) behavior and provides an optimized repair_vector using numpy.clip.

IntegerRepairOperator

Repair operator for integer-valued variables.

Default behavior: round to the nearest integer and clamp to bounds.

NoOpRepair

No-op repair operator: returns inputs unchanged.

Useful for solution types that do not require repair (binary, permutation), or as a placeholder in tests.

RandomUniformRepair(rng=None)

Bases: FloatRepairOperator

Repair operator that replaces out-of-bounds values by a uniform sample inside the provided bounds. Uses a NumPy Generator for reproducibility.

Source code in src/jmetal/operator/repair.py
def __init__(self, rng: np.random.Generator | None = None):
    self._rng = rng or np.random.default_rng()

ReflectiveRepair

Bases: FloatRepairOperator

Reflective (mirror) repair: values outside bounds are reflected back into the interval. Repeated reflections handled via modulo arithmetic.

BoundSwapRepair

Bases: FloatRepairOperator

If value exceeds upper bound assign lower bound, and viceversa.

This is an aggressive repair that 'jumps' the value to the opposite bound.

ensure_float_repair(repair)

Normalize a repair argument into a FloatRepairOperator instance.

Rules: - If repair is None: return ClampFloatRepair() (default clamp behavior). - If repair is already a FloatRepairOperator: return it unchanged. - If repair is a callable: wrap it in an adapter that implements repair_scalar and inherits the default repair_vector behavior.

Source code in src/jmetal/operator/repair.py
def ensure_float_repair(
    repair: FloatRepairOperator | Callable | None,
) -> FloatRepairOperator:
    """Normalize a `repair` argument into a `FloatRepairOperator` instance.

    Rules:
    - If `repair` is None: return `ClampFloatRepair()` (default clamp behavior).
    - If `repair` is already a `FloatRepairOperator`: return it unchanged.
    - If `repair` is a callable: wrap it in an adapter that implements
      `repair_scalar` and inherits the default `repair_vector` behavior.
    """
    if repair is None:
        return ClampFloatRepair()

    if isinstance(repair, FloatRepairOperator):
        return repair

    if callable(repair):
        func = repair

        class _CallableRepair(FloatRepairOperator):
            def repair_scalar(self, value: float, lower_bound: float, upper_bound: float) -> float:
                return float(func(value, lower_bound, upper_bound))

            # inherits repair_vector which calls repair_scalar element-wise

        return _CallableRepair()

    raise TypeError("repair must be None, callable or FloatRepairOperator")

Replacement

replacement

Replacement

Bases: ABC, Generic[S]

Base class for population replacement strategies.

A replacement strategy decides which solutions from a parent population and an offspring population survive into the next generation. Concrete strategies (ranking-based, crowding-distance-based, hypervolume-based, ...) differ enough in their selection logic that this base class only fixes the shared contract, not any implementation.

replace(solution_list, offspring_list) abstractmethod

Combine a parent and an offspring population and select the survivors.

Parameters:

Name Type Description Default
solution_list list[S]

The parent population.

required
offspring_list list[S]

The offspring population.

required

Returns:

Type Description
list[S]

The population that survives into the next generation.

Source code in src/jmetal/operator/replacement.py
@abstractmethod
def replace(self, solution_list: list[S], offspring_list: list[S]) -> list[S]:
    """Combine a parent and an offspring population and select the survivors.

    Args:
        solution_list: The parent population.
        offspring_list: The offspring population.

    Returns:
        The population that survives into the next generation.
    """
    pass

RemovalPolicyType

Bases: Enum

Defines the policy for removing solutions in replacement strategies.

Attributes:

Name Type Description
SEQUENTIAL

Remove solutions one by one, updating density estimates after each removal. This is more computationally expensive but can lead to better diversity.

ONE_SHOT

Remove all solutions at once based on initial density estimates. This is faster but may be less accurate in maintaining diversity.

RankingAndDensityEstimatorReplacement(ranking, density_estimator, removal_policy=RemovalPolicyType.ONE_SHOT)

Bases: Replacement[S]

A replacement strategy that combines non-dominated ranking with density estimation.

This replacement strategy is commonly used in multi-objective evolutionary algorithms to maintain a good balance between convergence and diversity in the population. It first ranks solutions using non-dominated sorting and then applies a density estimator to select solutions within each front.

The replacement process works as follows: 1. Combine parent and offspring populations 2. Rank all solutions using non-dominated sorting 3. Fill the new population with solutions from the best fronts 4. When a front needs to be split, use the density estimator to select the most diverse solutions

Parameters:

Name Type Description Default
ranking Ranking

The ranking strategy to use (e.g., FastNonDominatedRanking)

required
density_estimator DensityEstimator

The density estimator to use (e.g., CrowdingDistance)

required
removal_policy RemovalPolicyType

The policy for removing solutions (SEQUENTIAL or ONE_SHOT)

ONE_SHOT
Example

from jmetal.operator import RankingAndDensityEstimatorReplacement from jmetal.util.ranking import FastNonDominatedRanking from jmetal.util.density_estimator import CrowdingDistance

Create a replacement operator with crowding distance

replacement = RankingAndDensityEstimatorReplacement( ... ranking=FastNonDominatedRanking(), ... density_estimator=CrowdingDistance(), ... removal_policy=RemovalPolicyType.SEQUENTIAL ... )

Apply replacement to combine parent and offspring populations

new_population = replacement.replace(parents, offspring)

Source code in src/jmetal/operator/replacement.py
def __init__(
    self,
    ranking: Ranking,
    density_estimator: DensityEstimator,
    removal_policy: RemovalPolicyType = RemovalPolicyType.ONE_SHOT,
):
    self.ranking = ranking
    self.density_estimator = density_estimator
    self.removal_policy = removal_policy

replace(solution_list, offspring_list)

Combine parent and offspring populations and select the best solutions.

This method combines the parent and offspring populations, ranks all solutions using non-dominated sorting, and then applies the specified removal policy to select the best solutions.

Parameters:

Name Type Description Default
solution_list list[S]

The parent population (list of solutions).

required
offspring_list list[S]

The offspring population (list of solutions).

required

Returns:

Type Description
list[S]

A new population with the same size as solution_list containing the

list[S]

best solutions from the combined population.

Note

The size of the returned population will be equal to the size of solution_list, not the combined size of both populations.

Source code in src/jmetal/operator/replacement.py
def replace(self, solution_list: list[S], offspring_list: list[S]) -> list[S]:
    """Combine parent and offspring populations and select the best solutions.

    This method combines the parent and offspring populations, ranks all solutions
    using non-dominated sorting, and then applies the specified removal policy
    to select the best solutions.

    Args:
        solution_list: The parent population (list of solutions).
        offspring_list: The offspring population (list of solutions).

    Returns:
        A new population with the same size as solution_list containing the
        best solutions from the combined population.

    Note:
        The size of the returned population will be equal to the size of
        solution_list, not the combined size of both populations.
    """
    join_population = solution_list + offspring_list
    self.ranking.compute_ranking(join_population)

    if self.removal_policy is RemovalPolicyType.SEQUENTIAL:
        result_list: list[S] = self.sequential_truncation(0, len(solution_list))
    else:
        result_list = self.one_shot_truncation(0, len(solution_list))

    return result_list

sequential_truncation(ranking_id, size_of_the_result_list)

Select solutions using sequential truncation based on density estimation.

This method is called recursively to fill the new population with solutions from the best non-dominated fronts. When a front needs to be split, it uses the density estimator to select the most diverse solutions.

Parameters:

Name Type Description Default
ranking_id int

The current front index to process.

required
size_of_the_result_list int

Number of solutions still needed to fill the population.

required

Returns:

Type Description
list[S]

A list of selected solutions from the current and subsequent fronts.

Note

This method is typically called internally by the replace() method and should not be called directly in most cases.

Source code in src/jmetal/operator/replacement.py
def sequential_truncation(self, ranking_id: int, size_of_the_result_list: int) -> list[S]:
    """Select solutions using sequential truncation based on density estimation.

    This method is called recursively to fill the new population with solutions
    from the best non-dominated fronts. When a front needs to be split, it uses
    the density estimator to select the most diverse solutions.

    Args:
        ranking_id: The current front index to process.
        size_of_the_result_list: Number of solutions still needed to fill the population.

    Returns:
        A list of selected solutions from the current and subsequent fronts.

    Note:
        This method is typically called internally by the replace() method and
        should not be called directly in most cases.
    """
    current_ranked_solutions = self.ranking.get_subfront(ranking_id)
    self.density_estimator.compute_density_estimator(current_ranked_solutions)

    result_list: list[S] = []

    if len(current_ranked_solutions) < size_of_the_result_list:
        # If the entire front fits, add all solutions and move to the next front
        result_list.extend(self.ranking.get_subfront(ranking_id))
        result_list.extend(
            self.sequential_truncation(
                ranking_id + 1, size_of_the_result_list - len(current_ranked_solutions)
            )
        )
    else:
        # If we need to split the front, use density estimator to select solutions
        for solution in current_ranked_solutions:
            result_list.append(solution)

        # Remove solutions with worst density values until we reach the desired size
        while len(result_list) > size_of_the_result_list:
            self.density_estimator.sort(result_list)

            del result_list[-1]
            self.density_estimator.compute_density_estimator(result_list)

    return result_list

one_shot_truncation(ranking_id, size_of_the_result_list)

Select solutions using one-shot truncation based on density estimation.

This method is similar to sequential_truncation but is more efficient as it doesn't recompute density estimates after each removal. It's faster but may be less accurate in maintaining diversity compared to sequential truncation.

Parameters:

Name Type Description Default
ranking_id int

The current front index to process.

required
size_of_the_result_list int

Number of solutions still needed to fill the population.

required

Returns:

Type Description
list[S]

A list of selected solutions from the current and subsequent fronts.

Note

This method is typically called internally by the replace() method when the removal policy is set to ONE_SHOT.

Source code in src/jmetal/operator/replacement.py
def one_shot_truncation(self, ranking_id: int, size_of_the_result_list: int) -> list[S]:
    """Select solutions using one-shot truncation based on density estimation.

    This method is similar to sequential_truncation but is more efficient as it
    doesn't recompute density estimates after each removal. It's faster but may
    be less accurate in maintaining diversity compared to sequential truncation.

    Args:
        ranking_id: The current front index to process.
        size_of_the_result_list: Number of solutions still needed to fill the population.

    Returns:
        A list of selected solutions from the current and subsequent fronts.

    Note:
        This method is typically called internally by the replace() method when
        the removal policy is set to ONE_SHOT.
    """
    current_ranked_solutions = self.ranking.get_subfront(ranking_id)
    self.density_estimator.compute_density_estimator(current_ranked_solutions)

    result_list: list[S] = []

    if len(current_ranked_solutions) < size_of_the_result_list:
        # If the entire front fits, add all solutions and move to the next front
        result_list.extend(self.ranking.get_subfront(ranking_id))
        result_list.extend(
            self.one_shot_truncation(
                ranking_id + 1, size_of_the_result_list - len(current_ranked_solutions)
            )
        )
    else:
        # Sort solutions by density and take the best ones
        self.density_estimator.sort(current_ranked_solutions)
        i = 0
        while len(result_list) < size_of_the_result_list:
            result_list.append(current_ranked_solutions[i])
            i += 1

    return result_list

RankingAndCrowdingDistanceReplacement(ranking=None, density_estimator=None)

Bases: Replacement[S]

Replacement operator based on non-dominated ranking and crowding distance.

This operator combines the parent and offspring populations, ranks them using non-dominated sorting, and selects the best solutions based on crowding distance. It's a specialized version of RankingAndDensityEstimatorReplacement that's specifically designed for NSGA-II and similar algorithms.

The replacement process works as follows: 1. Combine parent and offspring populations 2. Rank all solutions using non-dominated sorting 3. Fill the new population with solutions from the best fronts 4. When a front needs to be split, use crowding distance to select the most diverse solutions

Parameters:

Name Type Description Default
ranking Ranking

The ranking strategy to use (default: FastNonDominatedRanking)

None
density_estimator DensityEstimator

The density estimator to use (default: CrowdingDistance)

None
Example

from jmetal.operator import RankingAndCrowdingDistanceReplacement from jmetal.core.solution import FloatSolution

Create a replacement operator

replacement = RankingAndCrowdingDistanceReplacement()

Apply replacement to combine parent and offspring populations

new_population = replacement.replace(parents, offspring)

Source code in src/jmetal/operator/replacement.py
def __init__(self, ranking: Ranking = None, density_estimator: DensityEstimator = None):
    self.ranking = ranking if ranking is not None else FastNonDominatedRanking()
    self.density_estimator = (
        density_estimator if density_estimator is not None else CrowdingDistanceDensityEstimator()
    )

replace(solution_list, offspring_list)

Replace solutions in the population with offspring solutions.

This method combines the parent and offspring populations, ranks them using non-dominated sorting, and selects the best solutions based on crowding distance.

Parameters:

Name Type Description Default
solution_list list[S]

The parent population (list of solutions).

required
offspring_list list[S]

The offspring population (list of solutions).

required

Returns:

Type Description
list[S]

A new population with the same size as solution_list containing the

list[S]

best solutions from the combined population.

Note

The size of the returned population will be equal to the size of solution_list, not the combined size of both populations.

Source code in src/jmetal/operator/replacement.py
def replace(self, solution_list: list[S], offspring_list: list[S]) -> list[S]:
    """Replace solutions in the population with offspring solutions.

    This method combines the parent and offspring populations, ranks them using
    non-dominated sorting, and selects the best solutions based on crowding distance.

    Args:
        solution_list: The parent population (list of solutions).
        offspring_list: The offspring population (list of solutions).

    Returns:
        A new population with the same size as solution_list containing the
        best solutions from the combined population.

    Note:
        The size of the returned population will be equal to the size of
        solution_list, not the combined size of both populations.
    """
    join_population = solution_list + offspring_list

    # Compute ranking of the combined population
    self.ranking.compute_ranking(join_population)

    # Initialize result list
    result_list: list[S] = []

    # Fill the result list with solutions from the best fronts
    front_index = 0
    while len(result_list) < len(solution_list):
        # Get the current front
        current_front = self.ranking.get_subfront(front_index)

        # If adding the entire front won't exceed the population size, add all solutions
        if len(result_list) + len(current_front) <= len(solution_list):
            result_list.extend(current_front)
            front_index += 1
        else:
            # If we can't add the entire front, use crowding distance to select the best solutions
            self.density_estimator.compute_density_estimator(current_front)
            current_front.sort(key=lambda x: x.attributes["crowding_distance"], reverse=True)

            # Add solutions until we reach the desired population size
            remaining = len(solution_list) - len(result_list)
            result_list.extend(current_front[:remaining])

    return result_list

get_name()

Get the name of the replacement operator.

Returns:

Type Description
str

A string representing the name of this replacement operator.

Source code in src/jmetal/operator/replacement.py
def get_name(self) -> str:
    """Get the name of the replacement operator.

    Returns:
        A string representing the name of this replacement operator.
    """
    return "Ranking and crowding distance replacement"

SMSEMOAReplacement(ranking=None)

Bases: Replacement[S]

Replacement operator for the SMS-EMOA (S-Metric Selection Evolutionary Multiobjective Algorithm).

This replacement operator combines the parent and offspring populations, ranks them using non-dominated sorting, keeps every front except the last one whole, and prunes the last front down to size by sorting it by hypervolume contribution and dropping the worst-contributing solutions -- the same logic as jmetal.algorithm.multiobjective.smsemoa.SMSEMOA.replacement, generalized from "exactly one excess solution" (true when offspring_population_size=1, SMS-EMOA's usual steady-state configuration) to any number of excess solutions.

The hypervolume contribution of a solution is the hypervolume that would be lost if that solution was removed from the front. Pruning by it, front by front, is what keeps a good spread of solutions along the Pareto front.

The reference point is not fixed at construction time: it is recomputed on every replace() call as the merged population's worst objective values plus an offset of 1.0, matching the classic SMSEMOA's formula.

Parameters:

Name Type Description Default
ranking Ranking

The ranking strategy to use (default: FastNonDominatedRanking).

None
Example

from jmetal.operator import SMSEMOAReplacement

replacement = SMSEMOAReplacement() new_population = replacement.replace(parents, offspring)

Initialize the SMS-EMOA replacement operator.

Parameters:

Name Type Description Default
ranking Ranking

The ranking strategy to use. Defaults to FastNonDominatedRanking.

None
Source code in src/jmetal/operator/replacement.py
def __init__(self, ranking: Ranking = None):
    """Initialize the SMS-EMOA replacement operator.

    Args:
        ranking: The ranking strategy to use. Defaults to `FastNonDominatedRanking`.
    """
    self.ranking = ranking if ranking is not None else FastNonDominatedRanking()

replace(solution_list, offspring_list)

Replace solutions in the population with offspring solutions.

This method combines the parent and offspring populations, ranks them using non-dominated sorting, keeps every front but the last whole, and -- if the last front doesn't fit entirely -- sorts it by hypervolume contribution (descending) and keeps only as many of its best-contributing solutions as needed to reach the size of solution_list. Contributions are computed once, over the whole overflowing front, exactly as jmetal.algorithm.multiobjective.smsemoa.SMSEMOA's own replacement does for its single-excess-solution case -- this is that same computation generalized to however many solutions are in excess.

Parameters:

Name Type Description Default
solution_list list[S]

The parent population (list of solutions).

required
offspring_list list[S]

The offspring population (list of solutions).

required

Returns:

Type Description
list[S]

A new population of the same size as solution_list containing the

list[S]

best solutions from the combined population.

Source code in src/jmetal/operator/replacement.py
def replace(self, solution_list: list[S], offspring_list: list[S]) -> list[S]:
    """Replace solutions in the population with offspring solutions.

    This method combines the parent and offspring populations, ranks them using
    non-dominated sorting, keeps every front but the last whole, and -- if the last
    front doesn't fit entirely -- sorts it by hypervolume contribution (descending)
    and keeps only as many of its best-contributing solutions as needed to reach the
    size of `solution_list`. Contributions are computed once, over the whole
    overflowing front, exactly as `jmetal.algorithm.multiobjective.smsemoa.SMSEMOA`'s
    own replacement does for its single-excess-solution case -- this is that same
    computation generalized to however many solutions are in excess.

    Args:
        solution_list: The parent population (list of solutions).
        offspring_list: The offspring population (list of solutions).

    Returns:
        A new population of the same size as solution_list containing the
        best solutions from the combined population.
    """
    joint_population = solution_list + offspring_list
    self.ranking.compute_ranking(joint_population)

    num_subfronts = self.ranking.get_number_of_subfronts()
    result: list[S] = []
    for i in range(num_subfronts - 1):
        result.extend(self.ranking.get_subfront(i))

    last_front = list(self.ranking.get_subfront(num_subfronts - 1))
    target_size = len(solution_list)

    if len(result) + len(last_front) <= target_size:
        result.extend(last_front)
        return result

    reference_point = (
        np.max([s.objectives for s in joint_population], axis=0) + 1.0
    ).tolist()
    hv_estimator: HypervolumeContributionDensityEstimator[S] = (
        HypervolumeContributionDensityEstimator(reference_point=reference_point)
    )
    hv_estimator.compute_density_estimator(last_front)
    last_front.sort(key=lambda s: s.attributes["hv_contribution"], reverse=True)

    remaining_slots = target_size - len(result)
    result.extend(last_front[:remaining_slots])

    return result