Source code for jmetal.operator.crossover

import copy
import math
import random
from typing import List, Callable, Optional

import numpy as np

from jmetal.core.operator import Crossover
from jmetal.core.solution import (
    BinarySolution,
    CompositeSolution,
    FloatSolution,
    IntegerSolution,
    PermutationSolution,
    Solution,
)
from jmetal.util.ckecking import Check

# Type variables for generic type hints

"""
.. module:: crossover
   :platform: Unix, Windows
   :synopsis: Module implementing crossover operators.

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


[docs] class NullCrossover(Crossover[Solution, Solution]): def __init__(self): super(NullCrossover, self).__init__(probability=0.0)
[docs] def execute(self, parents: List[Solution]) -> List[Solution]: if len(parents) != 2: raise Exception("The number of parents is not two: {}".format(len(parents))) # Create deep copies to avoid modifying the original parents return [copy.deepcopy(parent) for parent in parents]
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self): return "Null crossover"
[docs] class PMXCrossover(Crossover[PermutationSolution, PermutationSolution]): def __init__(self, probability: float): super(PMXCrossover, self).__init__(probability=probability)
[docs] def execute(self, parents: List[PermutationSolution]) -> List[PermutationSolution]: if len(parents) != 2: raise Exception("The number of parents is not two: {}".format(len(parents))) offspring = copy.deepcopy(parents) permutation_length = len(offspring[0].variables) rand = random.random() if rand <= self.probability: cross_points = sorted([random.randint(0, permutation_length) for _ in range(2)]) def _repeated(element, collection): c = 0 for e in collection: if e == element: c += 1 return c > 1 def _swap(data_a, data_b, cross_points): c1, c2 = cross_points new_a = data_a[:c1] + data_b[c1:c2] + data_a[c2:] new_b = data_b[:c1] + data_a[c1:c2] + data_b[c2:] return new_a, new_b def _map(swapped, cross_points): n = len(swapped[0]) c1, c2 = cross_points s1, s2 = swapped map_ = s1[c1:c2], s2[c1:c2] for i_chromosome in range(n): if not c1 < i_chromosome < c2: for i_son in range(2): while _repeated(swapped[i_son][i_chromosome], swapped[i_son]): map_index = map_[i_son].index(swapped[i_son][i_chromosome]) swapped[i_son][i_chromosome] = map_[1 - i_son][map_index] return s1, s2 swapped = _swap(offspring[0].variables, offspring[1].variables, cross_points) mapped = _map(swapped, cross_points) offspring[0].variables, offspring[1].variables = mapped return offspring
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self): return "Partially Matched crossover"
[docs] class CXCrossover(Crossover[PermutationSolution, PermutationSolution]): def __init__(self, probability: float): super(CXCrossover, self).__init__(probability=probability)
[docs] def execute(self, parents: List[PermutationSolution]) -> List[PermutationSolution]: if len(parents) != 2: raise Exception("The number of parents is not two: {}".format(len(parents))) offspring = copy.deepcopy(parents[::-1]) rand = random.random() if rand <= self.probability: idx = random.randint(0, len(parents[0].variables) - 1) curr_idx = idx cycle = [] while True: cycle.append(curr_idx) curr_idx = parents[0].variables.index(parents[1].variables[curr_idx]) if curr_idx == idx: break for j in range(len(parents[0].variables)): if j in cycle: offspring[0].variables[j] = parents[0].variables[j] offspring[1].variables[j] = parents[1].variables[j] return offspring
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self): return "Cycle crossover"
[docs] class SBXCrossover(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. Args: probability: Crossover probability (0.0 to 1.0) distribution_index: Distribution index (must be ≥ 0). Higher values produce offspring closer to parents. Typical values range from 5 to 30, with 20 being a common default. Raises: Exception: If distribution_index is negative """ __EPS = 1.0e-14 # Small constant to prevent division by zero def __init__(self, probability: float, distribution_index: float = 20.0): super(SBXCrossover, self).__init__(probability=probability) self.distribution_index = distribution_index if distribution_index < 0: raise ValueError("The distribution index cannot be negative")
[docs] def execute(self, parents: List[FloatSolution]) -> List[FloatSolution]: Check.that(issubclass(type(parents[0]), FloatSolution), "Solution type invalid: " + str(type(parents[0]))) Check.that(issubclass(type(parents[1]), FloatSolution), "Solution type invalid") Check.that(len(parents) == 2, "The number of parents is not two: {}".format(len(parents))) offspring = copy.deepcopy(parents) rand = random.random() if rand <= self.probability: for i in range(len(parents[0].variables)): value_x1, value_x2 = parents[0].variables[i], parents[1].variables[i] if random.random() <= 0.5: if abs(value_x1 - value_x2) > self.__EPS: if value_x1 < value_x2: y1, y2 = value_x1, value_x2 else: y1, y2 = value_x2, value_x1 # Calculate beta and handle potential complex numbers try: # First offspring (based on first parent's bounds) lb1, ub1 = parents[0].lower_bound[i], parents[0].upper_bound[i] beta1 = 1.0 + (2.0 * (y1 - lb1) / (y2 - y1)) alpha1 = 2.0 - pow(beta1, -(self.distribution_index + 1.0)) rand_val = random.random() if rand_val <= (1.0 / alpha1): betaq1 = pow(rand_val * alpha1, (1.0 / (self.distribution_index + 1.0))) else: betaq1 = pow(1.0 / (2.0 - rand_val * alpha1), 1.0 / (self.distribution_index + 1.0)) c1 = 0.5 * (y1 + y2 - betaq1 * (y2 - y1)) # Ensure c1 is a real number and within bounds if isinstance(c1, complex): c1 = y1 if c1.real < lb1 else (y2 if c1.real > ub1 else c1.real) # Second offspring (based on second parent's bounds) lb2, ub2 = parents[1].lower_bound[i], parents[1].upper_bound[i] beta2 = 1.0 + (2.0 * (ub2 - y2) / (y2 - y1)) alpha2 = 2.0 - pow(beta2, -(self.distribution_index + 1.0)) if rand_val <= (1.0 / alpha2): betaq2 = pow((rand_val * alpha2), (1.0 / (self.distribution_index + 1.0))) else: betaq2 = pow(1.0 / (2.0 - rand_val * alpha2), 1.0 / (self.distribution_index + 1.0)) c2 = 0.5 * (y1 + y2 + betaq2 * (y2 - y1)) # Ensure c2 is a real number and within bounds if isinstance(c2, complex): c2 = y1 if c2.real < lb2 else (y2 if c2.real > ub2 else c2.real) except (ValueError, ZeroDivisionError): # Fallback to parent values if any numerical issues occur c1, c2 = y1, y2 # Apply bounds checking using the correct bounds for each offspring if c1 < lb1: c1 = lb1 if c2 < lb2: c2 = lb2 if c1 > ub1: c1 = ub1 if c2 > ub2: c2 = ub2 if random.random() <= 0.5: offspring[0]._variables[i] = c2 offspring[1]._variables[i] = c1 else: offspring[0]._variables[i] = c1 offspring[1]._variables[i] = c2 else: offspring[0]._variables[i] = value_x1 offspring[1]._variables[i] = value_x2 else: offspring[0]._variables[i] = value_x1 offspring[1]._variables[i] = value_x2 return offspring
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self) -> str: return "SBX crossover"
[docs] class IntegerSBXCrossover(Crossover[IntegerSolution, IntegerSolution]): __EPS = 1.0e-14 def __init__(self, probability: float, distribution_index: float = 20.0): super(IntegerSBXCrossover, self).__init__(probability=probability) self.distribution_index = distribution_index
[docs] def execute(self, parents: List[IntegerSolution]) -> List[IntegerSolution]: Check.that(issubclass(type(parents[0]), IntegerSolution), "Solution type invalid") Check.that(issubclass(type(parents[1]), IntegerSolution), "Solution type invalid") Check.that(len(parents) == 2, "The number of parents is not two: {}".format(len(parents))) offspring = copy.deepcopy(parents) rand = random.random() if rand <= self.probability: for i in range(len(parents[0].variables)): value_x1, value_x2 = parents[0].variables[i], parents[1].variables[i] if random.random() <= 0.5: if abs(value_x1 - value_x2) > self.__EPS: if value_x1 < value_x2: y1, y2 = value_x1, value_x2 else: y1, y2 = value_x2, value_x1 # Calculate beta and handle potential complex numbers try: # First offspring (based on first parent's bounds) lb1, ub1 = parents[0].lower_bound[i], parents[0].upper_bound[i] beta1 = 1.0 + (2.0 * (y1 - lb1) / (y2 - y1)) alpha1 = 2.0 - pow(beta1, -(self.distribution_index + 1.0)) rand_val = random.random() if rand_val <= (1.0 / alpha1): betaq1 = pow(rand_val * alpha1, (1.0 / (self.distribution_index + 1.0))) else: betaq1 = pow(1.0 / (2.0 - rand_val * alpha1), 1.0 / (self.distribution_index + 1.0)) c1 = 0.5 * (y1 + y2 - betaq1 * (y2 - y1)) # Ensure c1 is a real number and within bounds if isinstance(c1, complex): c1 = y1 if c1.real < lb1 else (y2 if c1.real > ub1 else c1.real) # Second offspring (based on second parent's bounds) lb2, ub2 = parents[1].lower_bound[i], parents[1].upper_bound[i] beta2 = 1.0 + (2.0 * (ub2 - y2) / (y2 - y1)) alpha2 = 2.0 - pow(beta2, -(self.distribution_index + 1.0)) if rand_val <= (1.0 / alpha2): betaq2 = pow((rand_val * alpha2), (1.0 / (self.distribution_index + 1.0))) else: betaq2 = pow(1.0 / (2.0 - rand_val * alpha2), 1.0 / (self.distribution_index + 1.0)) c2 = 0.5 * (y1 + y2 + betaq2 * (y2 - y1)) # Ensure c2 is a real number and within bounds if isinstance(c2, complex): c2 = y1 if c2.real < lb2 else (y2 if c2.real > ub2 else c2.real) except (ValueError, ZeroDivisionError): # Fallback to parent values if any numerical issues occur c1, c2 = y1, y2 # Apply bounds checking using the correct bounds for each offspring if c1 < lb1: c1 = lb1 if c2 < lb2: c2 = lb2 if c1 > ub1: c1 = ub1 if c2 > ub2: c2 = ub2 if random.random() <= 0.5: offspring[0]._variables[i] = int(c2) offspring[1]._variables[i] = int(c1) else: offspring[0]._variables[i] = int(c1) offspring[1]._variables[i] = int(c2) else: offspring[0]._variables[i] = value_x1 offspring[1]._variables[i] = value_x2 else: offspring[0]._variables[i] = value_x1 offspring[1]._variables[i] = value_x2 return offspring
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self) -> str: return "Integer SBX crossover"
[docs] class SPXCrossover(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. Args: probability: The probability of applying the crossover (must be between 0.0 and 1.0) Raises: ValueError: If the probability is not in the range [0.0, 1.0] """ def __init__(self, probability: float): if not (0.0 <= probability <= 1.0): raise ValueError(f"Probability must be between 0.0 and 1.0, but was {probability}") super(SPXCrossover, self).__init__(probability=probability) self._rng = np.random.default_rng()
[docs] 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 deep copies of the parents to avoid modifying the originals offspring = [copy.deepcopy(parents[0]), copy.deepcopy(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 = 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
[docs] def get_number_of_parents(self) -> int: """Return the number of parent solutions required by the operator.""" return 2
[docs] def get_number_of_children(self) -> int: """Return the number of offspring produced by the operator.""" return 2
[docs] def get_name(self) -> str: """Return the name of the operator.""" return "Single point crossover"
[docs] class BLXAlphaCrossover(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 Args: probability: Crossover probability (0.0 to 1.0) alpha: 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 repair_operator: 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 Raises: 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. """ def __init__( self, probability: float = 0.9, alpha: float = 0.5, repair_operator: Optional[Callable[[float, float, float], float]] = 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 self.repair_operator = repair_operator if repair_operator else self._default_repair
[docs] def execute(self, parents: List[FloatSolution]) -> List[FloatSolution]: Check.that(len(parents) == 2, "BLXAlphaCrossover requires exactly two parents") return self.doCrossover(self.probability, parents[0], parents[1])
[docs] 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 random.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 = random.uniform(min_range, max_range) y2 = random.uniform(min_range, max_range) # Repair out-of-bounds values y1 = self.repair_operator(y1, lower_bound, upper_bound) y2 = self.repair_operator(y2, lower_bound, upper_bound) offspring1._variables[i] = y1 offspring2._variables[i] = y2 return [offspring1, offspring2]
def _default_repair(self, value: float, lower_bound: float, upper_bound: float) -> float: """Default repair method that clamps values to bounds.""" return max(lower_bound, min(upper_bound, value))
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self) -> str: return f"BLX-alpha crossover"
[docs] class BLXAlphaBetaCrossover(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 Args: probability: Crossover probability (0.0 to 1.0) alpha: 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 beta: 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 repair_operator: 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 Raises: 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. """ def __init__( self, probability: float = 0.9, alpha: float = 0.5, beta: float = 0.5, repair_operator: Optional[Callable[[float, float, float], float]] = 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 self.repair_operator = repair_operator if repair_operator else self._default_repair
[docs] def execute(self, parents: List[FloatSolution]) -> List[FloatSolution]: Check.that(len(parents) == 2, "BLXAlphaBetaCrossover requires exactly two parents") return self.doCrossover(self.probability, parents[0], parents[1])
[docs] 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 random.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 = random.uniform(c_min, c_max) y2 = random.uniform(c_min, c_max) # Repair out-of-bounds values y1 = self.repair_operator(y1, lower_bound, upper_bound) y2 = self.repair_operator(y2, lower_bound, upper_bound) offspring1._variables[i] = y1 offspring2._variables[i] = y2 return [offspring1, offspring2]
def _default_repair(self, value: float, lower_bound: float, upper_bound: float) -> float: """Default repair method that clamps values to bounds.""" return max(lower_bound, min(upper_bound, value))
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self) -> str: return f"BLX-alpha-beta crossover (α={self.alpha}, β={self.beta})"
[docs] class ArithmeticCrossover(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 - child2 = (1 - alpha) * parent1 + alpha * parent2 3. Apply bounds repair if values fall outside the variable bounds Args: probability: Crossover probability (0.0 to 1.0) repair_operator: 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 Raises: ValueError: If probability is not in [0,1] Reference: Michalewicz, Z. (1996). Genetic Algorithms + Data Structures = Evolution Programs. Springer-Verlag, Berlin. """ def __init__( self, probability: float = 0.9, repair_operator: Optional[Callable[[float, float, float], float]] = None, ): if not 0 <= probability <= 1: raise ValueError("probability must be in [0, 1]") super().__init__(probability=probability) self.repair_operator = repair_operator if repair_operator else self._default_repair
[docs] def execute(self, parents: List[FloatSolution]) -> List[FloatSolution]: Check.that(len(parents) == 2, "Arithmetic Crossover requires exactly two parents") return self.doCrossover(self.probability, parents[0], parents[1])
[docs] 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 random.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 = random.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(value1, lower_bound, upper_bound) repaired2 = self.repair_operator(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]
def _default_repair(self, value: float, lower_bound: float, upper_bound: float) -> float: """Default repair method that clamps values to bounds.""" return max(lower_bound, min(upper_bound, value))
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self) -> str: return "Arithmetic Crossover"
[docs] class UnimodalNormalDistributionCrossover(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. Args: probability: Crossover probability (0.0 to 1.0) zeta: Controls the spread along the line connecting parents (typically in [0.1, 1.0], where smaller values produce offspring closer to the parents) eta: Controls the spread in the orthogonal direction (typically in [0.1, 0.5], where smaller values produce more concentrated distributions) repair_operator: 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 Raises: ValueError: If probability is not in [0,1] or if zeta or eta are negative """ def __init__( self, probability: float = 0.9, zeta: float = 0.5, eta: float = 0.35, repair_operator: Optional[Callable[[float, float, float], float]] = 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 self.repair_operator = repair_operator if repair_operator else self._default_repair
[docs] def execute(self, parents: List[FloatSolution]) -> List[FloatSolution]: Check.that(len(parents) >= 3, "UNDX requires at least three parents") return self.doCrossover(self.probability, parents[0], parents[1], parents[2])
[docs] 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 random.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 = random.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 = (random.random() - 0.5) * self.eta * distance + \ (random.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 lower_bound = parent1.lower_bound[i] upper_bound = parent1.upper_bound[i] offspring1._variables[i] = self.repair_operator(value1, lower_bound, upper_bound) offspring2._variables[i] = self.repair_operator(value2, lower_bound, upper_bound) return [offspring1, offspring2]
def _default_repair(self, value: float, lower_bound: float, upper_bound: float) -> float: """Default repair method that clamps values to bounds.""" return max(lower_bound, min(upper_bound, value))
[docs] def get_number_of_parents(self) -> int: return 3 # UNDX requires exactly 3 parents
[docs] def get_number_of_children(self) -> int: return 2 # UNDX generates 2 offspring
[docs] def get_name(self) -> str: return f"Unimodal Normal Distribution Crossover (ζ={self.zeta}, η={self.eta})"
[docs] class DifferentialEvolutionCrossover(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. Args: cr: Crossover probability (0.0 to 1.0). Controls the probability of each variable being taken from the trial vector versus the target vector. f: Differential weight (mutation factor) for the difference vector. Typically in [0, 2]. k: Scaling factor for the difference vector. Typically in [0, 1]. Raises: 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. """ def __init__(self, CR: float, F: float, K: float = 0.5): super(DifferentialEvolutionCrossover, self).__init__(probability=1.0) self.CR = CR self.F = F self.K = K self.current_individual: FloatSolution = None
[docs] 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("The number of parents is not {}: {}".format(self.get_number_of_parents(), len(parents))) child = copy.deepcopy(self.current_individual) number_of_variables = len(parents[0].variables) rand = random.randint(0, number_of_variables - 1) for i in range(number_of_variables): if random.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]
[docs] def get_number_of_parents(self) -> int: return 3
[docs] def get_number_of_children(self) -> int: return 1
[docs] def get_name(self) -> str: return "Differential Evolution crossover"
[docs] class CompositeCrossover(Crossover[CompositeSolution, CompositeSolution]): __EPS = 1.0e-14 def __init__(self, crossover_operator_list: List[Crossover]): super(CompositeCrossover, self).__init__(probability=1.0) Check.is_not_none(crossover_operator_list) Check.collection_is_not_empty(crossover_operator_list) self.crossover_operators_list = [] for operator in crossover_operator_list: Check.that(issubclass(operator.__class__, Crossover), "Object is not a subclass of Crossover") self.crossover_operators_list.append(operator)
[docs] def execute(self, solutions: List[CompositeSolution]) -> List[CompositeSolution]: Check.is_not_none(solutions) Check.that(len(solutions) == 2, "The number of parents is not two: " + str(len(solutions))) offspring1 = [] offspring2 = [] number_of_solutions_in_composite_solution = len(solutions[0].variables) for i in range(number_of_solutions_in_composite_solution): parents = [solutions[0].variables[i], solutions[1].variables[i]] children = self.crossover_operators_list[i].execute(parents) offspring1.append(children[0]) offspring2.append(children[1]) return [CompositeSolution(offspring1), CompositeSolution(offspring2)]
[docs] def get_number_of_parents(self) -> int: return 2
[docs] def get_number_of_children(self) -> int: return 2
[docs] def get_name(self) -> str: return "Composite crossover"