Skip to content

Index

This module provides various common operators to be used within evolutionary algorithms.

Each operator is provided as a separate class, which is to be instantiated in this form:

op = OperatorName(
    problem,  # where problem is a Problem instance
    hyperparameter1=...,
    hyperparameter2=...,
    ...
)

Each operator has its __call__(...) method overriden so that it can be used like a function. For example, if the operator op instantiated above were a mutation operator, it would be used like this:

# Apply mutation on a SolutionBatch
mutated_solution = op(my_solution_batch)

Please see the documentations of the provided operator classes for details about how to instantiate them, and how to call them.

A common usage for the operators provided here is to use them with a genetic algorithm. More specifically, the SteadyStateGA algorithm provided within the namespace evotorch.algorithms needs to be configured so that it knows which cross-over operator and which mutation operator it should apply on the solutions. The way this is done is as follows:

import evotorch.algorithms as ea
import evotorch.operators as eo

problem = ...   # initialize the Problem

ga = ea.SteadyStateGA(problem, popsize=...)

# Configure the genetic algorithm to use
# simulated binary cross-over
ga.use(
    eo.SimulatedBinaryCrossOver(
        problem,
        tournament_size=...,
        cross_over_rate=...,
        eta=...
    )
)

# Configure the genetic algorithm to use
# Gaussian mutation
ga.use(
    eo.GaussianMutation(
        problem,
        stdev=...
    )
)

base

Base classes for various operators

CopyingOperator (Operator)

Base class for operators which do not do in-place modifications.

This class does not add any functionality to the Operator class. Instead, the annotations of the __call__(...) method is updated so that it makes it clear that a new SolutionBatch is returned.

One is expected to override the definition of the method _do(...) in an inheriting subclass to define a custom CopyingOperator.

From outside, a subclass of CopyingOperator is meant to be called like a function, as follows:

my_new_batch = my_copying_operator_instance(my_batch)
Source code in evotorch/operators/base.py
class CopyingOperator(Operator):
    """
    Base class for operators which do not do in-place modifications.

    This class does not add any functionality to the Operator class.
    Instead, the annotations of the `__call__(...)` method is
    updated so that it makes it clear that a new SolutionBatch is
    returned.

    One is expected to override the definition of the method `_do(...)`
    in an inheriting subclass to define a custom `CopyingOperator`.

    From outside, a subclass of `CopyingOperator` is meant to be called like
    a function, as follows:

        my_new_batch = my_copying_operator_instance(my_batch)
    """

    def __init__(self, problem: Problem):
        """
        `__init__(...)`: Initialize the CopyingOperator.

        Args:
            problem: The problem object which is being worked on.
        """
        super().__init__(problem)

    def __call__(self, batch: SolutionBatch) -> SolutionBatch:
        return self._do(batch)

    def _do(self, batch: SolutionBatch) -> SolutionBatch:
        """The actual definition of the operation on the batch.
        Expected to be overriden by a subclass.
        """
        raise NotImplementedError

__init__(self, problem) special

__init__(...): Initialize the CopyingOperator.

Parameters:

Name Type Description Default
problem Problem

The problem object which is being worked on.

required
Source code in evotorch/operators/base.py
def __init__(self, problem: Problem):
    """
    `__init__(...)`: Initialize the CopyingOperator.

    Args:
        problem: The problem object which is being worked on.
    """
    super().__init__(problem)

CrossOver (CopyingOperator)

Base class for any CrossOver operator.

One is expected to override the definition of the method _do_cross_over(...) in an inheriting subclass to define a custom CrossOver.

From outside, a CrossOver instance is meant to be called like this:

child_solution_batch = my_cross_over_instance(population_batch)

which causes the CrossOver instance to select parents from the population_batch, recombine their values according to what is instructed in _do_cross_over(...), and return the newly made solutions in a SolutionBatch.

Source code in evotorch/operators/base.py
class CrossOver(CopyingOperator):
    """
    Base class for any CrossOver operator.

    One is expected to override the definition of the method
    `_do_cross_over(...)` in an inheriting subclass to define a
    custom `CrossOver`.

    From outside, a `CrossOver` instance is meant to be called like this:

        child_solution_batch = my_cross_over_instance(population_batch)

    which causes the `CrossOver` instance to select parents from the
    `population_batch`, recombine their values according to what is
    instructed in `_do_cross_over(...)`, and return the newly made solutions
    in a `SolutionBatch`.
    """

    def __init__(
        self,
        problem: Problem,
        *,
        tournament_size: int,
        obj_index: Optional[int] = None,
        num_children: Optional[int] = None,
        cross_over_rate: Optional[float] = None,
    ):
        """
        `__init__(...)`: Initialize the CrossOver.

        Args:
            problem: The problem object which is being worked on.
            tournament_size: Size of the tournament which will be used for
                doing selection.
            obj_index: Index of the objective according to which the selection
                will be done.
                If `obj_index` is None and the problem is single-objective,
                then the selection will be done according to that single
                objective.
                If `obj_index` is None and the problem is multi-objective,
                then the selection will be done according to pareto-dominance
                and crowding criteria, as done in NSGA-II.
                If `obj_index` is an integer `i`, then the selection will be
                done according to the i-th objective only, even when the
                problem is multi-objective.
            num_children: How many children to generate.
                Expected as an even number.
                Cannot be used together with `cross_over_rate`.
            cross_over_rate: Rate of the cross-over operations in comparison
                with the population size.
                1.0 means that the number of generated children will be equal
                to the original population size.
                Cannot be used together with `num_children`.
        """
        super().__init__(problem)

        self._obj_index = None if obj_index is None else problem.normalize_obj_index(obj_index)
        self._tournament_size = int(tournament_size)

        if num_children is not None and cross_over_rate is not None:
            raise ValueError(
                "Received both `num_children` and `cross_over_rate` as values other than None."
                " It was expected to receive both of them as None, or one of them as None,"
                " but not both of them as values other than None."
            )

        self._num_children = None if num_children is None else int(num_children)
        self._cross_over_rate = None if cross_over_rate is None else float(cross_over_rate)

    def _compute_num_tournaments(self, batch: SolutionBatch) -> int:
        if self._num_children is None and self._cross_over_rate is None:
            # return len(batch) * 2
            result = len(batch)
            if (result % 2) != 0:
                result += 1
            return result
        elif self._num_children is not None:
            if (self._num_children % 2) != 0:
                raise ValueError(
                    f"The initialization argument `num_children` was expected as an even number."
                    f" However, it was found as an odd number: {self._num_children}"
                )
            return self._num_children
        elif self._cross_over_rate is not None:
            f = len(batch) * self._cross_over_rate
            result1 = math.ceil(f)
            result2 = math.floor(f)
            if result1 == result2:
                result = result1
                if (result % 2) != 0:
                    result += 1
            else:
                if (result1 % 2) == 0:
                    result = result1
                else:
                    result = result2

            return result
        else:
            assert False, "Exection should not have reached this point"

    @property
    def obj_index(self) -> Optional[int]:
        """The objective index according to which the selection will be done"""
        return self._obj_index

    @torch.no_grad()
    def _do_tournament(self, batch: SolutionBatch) -> tuple:
        # Compute the required number of tournaments
        num_tournaments = self._compute_num_tournaments(batch)

        if self._problem.is_multi_objective and self._obj_index is None:
            # If the problem is multi-objective, and an objective index is not specified,
            # then we do a multi-objective-specific cross-over

            # At first, pareto-sort the solutions
            fronts, ranks = batch.arg_pareto_sort()

            # In NSGA-II-inspired pareto-sorting, smallest rank means the best front.
            # Right now, we want the opposite: we want the solutions in the best front
            # to have rank values which are numerically highest.
            # The following line re-arranges the rank values such that the solutions
            # in the best front have their ranks equal to len(fronts), and the ones
            # in the worst front have their ranks equal to 1.
            ranks = torch.as_tensor(len(fronts) - ranks, dtype=self._problem.eval_dtype, device=batch.device)

            # Because the ranks are computed front the fronts indices, we expect many
            # solutions to end up with the same rank values.
            # To ensure that a randomized selection will be made when comparing two
            # solutions with the same rank, we add random noise to the ranks
            # (between 0.0 and 0.1).
            ranks += self._problem.make_uniform(len(batch), dtype=self._problem.eval_dtype, device=batch.device) * 0.1
        else:
            # Rank the solutions. Worst gets -0.5, best gets 0.5
            ranks = batch.utility(self._obj_index, ranking_method="centered")

        # Get the internal values tensor of the solution batch
        indata = batch._data

        # Get a tensor of random integers in the shape (num_tournaments, tournament_size)
        tournament_indices = self.problem.make_randint(
            (num_tournaments, self._tournament_size), n=len(batch), device=indata.device
        )
        tournament_ranks = ranks[tournament_indices]

        # Imagine tournament size is 2, and the solutions are [ worst, bad, best, good ].
        # So, what we have is (0.2s are actually 0.166666...):
        #
        #    ranks = [ -0.5, -0.2, 0.5, 0.2 ]
        #
        #    tournament     tournament
        #     indices         ranks
        #
        #       0, 1        -0.5, -0.2
        #       2, 3         0.5,  0.2
        #       1, 0        -0.2, -0.5
        #       3, 2         0.2,  0.5
        #       1, 2        -0.2,  0.5
        #       0, 3        -0.5,  0.2
        #       2, 0         0.5, -0.5
        #       3, 1         0.2, -0.2
        #
        # According to tournament_indices, there are 8 tournaments.
        #   In tournament 0 (topmost row), parent0 and parent1 compete.
        #   In tournament 1 (next row), parent2 and parent3 compete; and so on.
        # tournament_ranks tells us:
        #   In tournament 0, left-candidate has rank -0.5, and right-candidate has -0.2.
        #   In tournament 1, left-candidate has rank 0.5, and right-candidate has 0.2; and so on.

        tournament_rows = torch.arange(0, num_tournaments, device=indata.device)
        parents = tournament_indices[tournament_rows, torch.argmax(tournament_ranks, dim=-1)]

        # Continuing from the [ worst, bad, best, good ] example, we end up with:
        #
        #            T                                    T
        #  tournament   tournament     tournament   argmax     parents
        #    rows        indices         ranks      dim=-1
        #
        #      0          0, 1        -0.5, -0.2      1           1
        #      1          2, 3         0.5,  0.2      0           2
        #      2          1, 0        -0.2, -0.5      0           1
        #      3          3, 2         0.2,  0.5      1           2
        #      4          1, 2        -0.2,  0.5      1           2
        #      5          0, 3        -0.5,  0.2      1           3
        #      6          2, 0         0.5, -0.5      0           2
        #      7          3, 1         0.2, -0.2      0           3
        #
        # where tournament_rows represents row indices in tournament_indices tensor (from 0 to 7).
        # argmax() tells us who won the competition (0: left-candidate won, 1: right-candidate won).
        #
        # tournament_rows and argmax() together give us the row and column of the winner in tensor
        # tournament_indices, which in turn gives us the index of the winner solution in the batch.

        # We split the parents array from the middle
        split_point = int(len(parents) / 2)
        parents1 = indata[parents][:split_point]
        parents2 = indata[parents][split_point:]
        # We now have:
        #
        #   parents1            parents2
        #   ===============     ===============
        #   values of sln 1     values of sln 2   (solution1 is to generate a child with solution2)
        #   values of sln 2     values of sln 3   (solution2 is to generate a child with solution3)
        #   values of sln 1     values of sln 2   (solution1 is to generate another child with solution2)
        #   values of sln 2     values of sln 3   (solution2 is to generate another child with solution3)
        #
        # With this, the tournament selection phase is over.
        return parents1, parents2

    def _do_cross_over(
        self,
        parents1: Union[torch.Tensor, ObjectArray],
        parents2: Union[torch.Tensor, ObjectArray],
    ) -> SolutionBatch:
        """
        The actual definition of the cross-over operation.

        This is a protected method, meant to be overriden by the inheriting
        subclass.

        The arguments passed to this function are the decision values of the
        first and the second half of the selected parents, both as PyTorch
        tensors or as `ObjectArray`s.

        In the overriding function, for each integer i, one is expected to
        recombine the values of the i-th row of `parents1` with the values of
        the i-th row of `parents2` twice (twice because each pairing is
        expected to generate two children).
        After that, one is expected to generate a SolutionBatch and place
        all the recombination results into the values of that new batch.

        Args:
            parents1: The decision values of the first half of the
                selected parents.
            parents2: The decision values of the second half of the
                selected parents.
        Returns:
            A new SolutionBatch which contains the recombination
            of the parents.
        """
        raise NotImplementedError

    def _make_children_batch(self, child_values: Union[torch.Tensor, ObjectArray]) -> SolutionBatch:
        result = SolutionBatch(self.problem, device=child_values.device, empty=True, popsize=child_values.shape[0])
        result._data = child_values
        return result

    def _do(self, batch: SolutionBatch) -> SolutionBatch:
        parents1, parents2 = self._do_tournament(batch)
        if len(parents1) != len(parents2):
            raise ValueError(
                f"_do_tournament() returned parents1 and parents2 with incompatible sizes. "
                f"len(parents1): {len(parents1)}; len(parents2): {len(parents2)}."
            )
        return self._do_cross_over(parents1, parents2)

obj_index: Optional[int] property readonly

The objective index according to which the selection will be done

__init__(self, problem, *, tournament_size, obj_index=None, num_children=None, cross_over_rate=None) special

__init__(...): Initialize the CrossOver.

Parameters:

Name Type Description Default
problem Problem

The problem object which is being worked on.

required
tournament_size int

Size of the tournament which will be used for doing selection.

required
obj_index Optional[int]

Index of the objective according to which the selection will be done. If obj_index is None and the problem is single-objective, then the selection will be done according to that single objective. If obj_index is None and the problem is multi-objective, then the selection will be done according to pareto-dominance and crowding criteria, as done in NSGA-II. If obj_index is an integer i, then the selection will be done according to the i-th objective only, even when the problem is multi-objective.

None
num_children Optional[int]

How many children to generate. Expected as an even number. Cannot be used together with cross_over_rate.

None
cross_over_rate Optional[float]

Rate of the cross-over operations in comparison with the population size. 1.0 means that the number of generated children will be equal to the original population size. Cannot be used together with num_children.

None
Source code in evotorch/operators/base.py
def __init__(
    self,
    problem: Problem,
    *,
    tournament_size: int,
    obj_index: Optional[int] = None,
    num_children: Optional[int] = None,
    cross_over_rate: Optional[float] = None,
):
    """
    `__init__(...)`: Initialize the CrossOver.

    Args:
        problem: The problem object which is being worked on.
        tournament_size: Size of the tournament which will be used for
            doing selection.
        obj_index: Index of the objective according to which the selection
            will be done.
            If `obj_index` is None and the problem is single-objective,
            then the selection will be done according to that single
            objective.
            If `obj_index` is None and the problem is multi-objective,
            then the selection will be done according to pareto-dominance
            and crowding criteria, as done in NSGA-II.
            If `obj_index` is an integer `i`, then the selection will be
            done according to the i-th objective only, even when the
            problem is multi-objective.
        num_children: How many children to generate.
            Expected as an even number.
            Cannot be used together with `cross_over_rate`.
        cross_over_rate: Rate of the cross-over operations in comparison
            with the population size.
            1.0 means that the number of generated children will be equal
            to the original population size.
            Cannot be used together with `num_children`.
    """
    super().__init__(problem)

    self._obj_index = None if obj_index is None else problem.normalize_obj_index(obj_index)
    self._tournament_size = int(tournament_size)

    if num_children is not None and cross_over_rate is not None:
        raise ValueError(
            "Received both `num_children` and `cross_over_rate` as values other than None."
            " It was expected to receive both of them as None, or one of them as None,"
            " but not both of them as values other than None."
        )

    self._num_children = None if num_children is None else int(num_children)
    self._cross_over_rate = None if cross_over_rate is None else float(cross_over_rate)

Operator

Base class for various operations on SolutionBatch objects.

Some subclasses of Operator may be operating on the batches in-place, while some others may generate new batches, leaving the original batches untouched.

One is expected to override the definition of the method _do(...) in an inheriting subclass to define a custom Operator.

From outside, a subclass of Operator is meant to be called like a function. In more details, operators which apply in-place modifications are meant to be called like this:

my_operator_instance(my_batch)

Operators which return a new batch are meant to be called like this:

my_new_batch = my_operator_instance(my_batch)
Source code in evotorch/operators/base.py
class Operator:
    """Base class for various operations on SolutionBatch objects.

    Some subclasses of Operator may be operating on the batches in-place,
    while some others may generate new batches, leaving the original batches
    untouched.

    One is expected to override the definition of the method `_do(...)`
    in an inheriting subclass to define a custom `Operator`.

    From outside, a subclass of Operator is meant to be called like
    a function. In more details, operators which apply in-place modifications
    are meant to be called like this:

        my_operator_instance(my_batch)

    Operators which return a new batch are meant to be called like this:

        my_new_batch = my_operator_instance(my_batch)
    """

    def __init__(self, problem: Problem):
        """
        `__init__(...)`: Initialize the Operator.

        Args:
            problem: The problem object which is being worked on.
        """
        if not isinstance(problem, Problem):
            raise TypeError(f"Expected a Problem object, but received {repr(problem)}")
        self._problem = problem
        self._lb = clone(self._problem.lower_bounds)
        self._ub = clone(self._problem.upper_bounds)

    @property
    def problem(self) -> Problem:
        """Get the problem to which this cross-over operator is bound"""
        return self._problem

    @property
    def dtype(self) -> DType:
        """Get the dtype of the bound problem.
        If the problem does not work with SolutionVectors and
        therefore it does not have a dtype, None is returned.
        """
        return self.problem.dtype

    @torch.no_grad()
    def _respect_bounds(self, x: torch.Tensor) -> torch.Tensor:
        """
        Make sure that a given PyTorch tensor respects the problem's bounds.

        This is a protected method which might be used by the
        inheriting subclasses to ensure that the result of their
        various operations are clipped properly to respect the
        boundaries set by the problem object.

        Note that this function might return the tensor itself
        is the problem is not bounded.

        Args:
            x: The PyTorch tensor to be clipped.
        Returns:
            The clipped tensor.
        """
        if self._lb is not None:
            self._lb = torch.as_tensor(self._lb, dtype=x.dtype, device=x.device)
            x = torch.max(self._lb, x)
        if self._ub is not None:
            self._ub = torch.as_tensor(self._ub, dtype=x.dtype, device=x.device)
            x = torch.min(self._ub, x)
        return x

    def __call__(self, batch: SolutionBatch):
        """
        Apply the operator on the given batch.
        """
        if not isinstance(batch, SolutionBatch):
            raise TypeError(
                f"The operation {self.__class__.__name__} can only work on"
                f" SolutionBatch objects, but it received an object of type"
                f" {repr(type(batch))}."
            )
        self._do(batch)

    def _do(self, batch: SolutionBatch):
        """
        The actual definition of the operation on the batch.
        Expected to be overriden by a subclass.
        """
        raise NotImplementedError

dtype: Union[str, torch.dtype, numpy.dtype, Type] property readonly

Get the dtype of the bound problem. If the problem does not work with SolutionVectors and therefore it does not have a dtype, None is returned.

problem: Problem property readonly

Get the problem to which this cross-over operator is bound

__call__(self, batch) special

Apply the operator on the given batch.

Source code in evotorch/operators/base.py
def __call__(self, batch: SolutionBatch):
    """
    Apply the operator on the given batch.
    """
    if not isinstance(batch, SolutionBatch):
        raise TypeError(
            f"The operation {self.__class__.__name__} can only work on"
            f" SolutionBatch objects, but it received an object of type"
            f" {repr(type(batch))}."
        )
    self._do(batch)

__init__(self, problem) special

__init__(...): Initialize the Operator.

Parameters:

Name Type Description Default
problem Problem

The problem object which is being worked on.

required
Source code in evotorch/operators/base.py
def __init__(self, problem: Problem):
    """
    `__init__(...)`: Initialize the Operator.

    Args:
        problem: The problem object which is being worked on.
    """
    if not isinstance(problem, Problem):
        raise TypeError(f"Expected a Problem object, but received {repr(problem)}")
    self._problem = problem
    self._lb = clone(self._problem.lower_bounds)
    self._ub = clone(self._problem.upper_bounds)

SingleObjOperator (Operator)

Base class for all the operators which focus on only one objective.

One is expected to override the definition of the method _do(...) in an inheriting subclass to define a custom SingleObjOperator.

Source code in evotorch/operators/base.py
class SingleObjOperator(Operator):
    """
    Base class for all the operators which focus on only one objective.

    One is expected to override the definition of the method `_do(...)`
    in an inheriting subclass to define a custom `SingleObjOperator`.
    """

    def __init__(self, problem: Problem, obj_index: Optional[int] = None):
        """
        Initialize the SingleObjOperator.

        Args:
            problem: The problem object which is being worked on.
            obj_index: Index of the objective to focus on.
                Can be given as None if the problem is single-objective.
        """
        super().__init__(problem)
        self._obj_index: int = problem.normalize_obj_index(obj_index)

    @property
    def obj_index(self) -> int:
        """Index of the objective on which this operator is to be applied"""
        return self._obj_index

obj_index: int property readonly

Index of the objective on which this operator is to be applied

__init__(self, problem, obj_index=None) special

Initialize the SingleObjOperator.

Parameters:

Name Type Description Default
problem Problem

The problem object which is being worked on.

required
obj_index Optional[int]

Index of the objective to focus on. Can be given as None if the problem is single-objective.

None
Source code in evotorch/operators/base.py
def __init__(self, problem: Problem, obj_index: Optional[int] = None):
    """
    Initialize the SingleObjOperator.

    Args:
        problem: The problem object which is being worked on.
        obj_index: Index of the objective to focus on.
            Can be given as None if the problem is single-objective.
    """
    super().__init__(problem)
    self._obj_index: int = problem.normalize_obj_index(obj_index)

real

This module contains operators defined to work with problems whose dtypes are real numbers (e.g. torch.float32).

CosynePermutation (CopyingOperator)

Representation of permutation operation on a SolutionBatch.

For each decision variable index, a permutation operation across all or a subset of solutions, is performed. The result is returned on a new SolutionBatch. The original SolutionBatch remains unmodified.

Reference:

F.Gomez, J.Schmidhuber, R.Miikkulainen (2008).
Accelerated Neural Evolution through Cooperatively Coevolved Synapses
Journal of Machine Learning Research 9, 937-965
Source code in evotorch/operators/real.py
class CosynePermutation(CopyingOperator):
    """
    Representation of permutation operation on a SolutionBatch.

    For each decision variable index, a permutation operation across
    all or a subset of solutions, is performed.
    The result is returned on a new SolutionBatch.
    The original SolutionBatch remains unmodified.

    Reference:

        F.Gomez, J.Schmidhuber, R.Miikkulainen (2008).
        Accelerated Neural Evolution through Cooperatively Coevolved Synapses
        Journal of Machine Learning Research 9, 937-965
    """

    def __init__(self, problem: Problem, obj_index: Optional[int] = None, *, permute_all: bool = False):
        """
        `__init__(...)`: Initialize the CosynePermutation.

        Args:
            problem: The problem object to work on.
            obj_index: The index of the objective according to which the
                candidates for permutation will be selected.
                Can be left as None if the problem is single-objective,
                or if `permute_all` is given as True (in which case there
                will be no candidate selection as the entire population will
                be subject to permutation).
            permute_all: Whether or not to apply permutation on the entire
                population, instead of using a selective permutation.
        """

        if permute_all:
            if obj_index is not None:
                raise ValueError(
                    "When `permute_all` is given as True (which seems to be the case)"
                    " `obj_index` is expected as None,"
                    " because the operator is independent of any objective and any fitness in this mode."
                    " However, `permute_all` was found to be something other than None."
                )
            self._obj_index = None
        else:
            self._obj_index = problem.normalize_obj_index(obj_index)

        super().__init__(problem)

        self._permute_all = bool(permute_all)

    @property
    def obj_index(self) -> Optional[int]:
        """Objective index according to which the operator will run.
        If `permute_all` was given as True, objectives are irrelevant, in which case
        `obj_index` is returned as None.
        If `permute_all` was given as False, the relevant `obj_index` is provided
        as an integer.
        """
        return self._obj_index

    @torch.no_grad()
    def _do(self, batch: SolutionBatch) -> SolutionBatch:
        indata = batch._data

        if not self._permute_all:
            n = batch.solution_length
            ranks = batch.utility(self._obj_index, ranking_method="centered")
            # fitnesses = batch.evals[:, self._obj_index].clone().reshape(-1)
            # ranks = rank(
            #    fitnesses, ranking_method="centered", higher_is_better=(self.problem.senses[self.obj_index] == "max")
            # )
            prob_permute = (1 - (ranks + 0.5).pow(1 / float(n))).unsqueeze(1).expand(len(batch), batch.solution_length)
        else:
            prob_permute = torch.ones_like(indata)

        perm_mask = self.problem.make_uniform_shaped_like(prob_permute) <= prob_permute

        perm_mask_sorted = torch.sort(perm_mask.to(torch.long), descending=True, dim=0)[0].to(
            torch.bool
        )  # Sort permutations

        perm_rand = self.problem.make_uniform_shaped_like(prob_permute)
        perm_rand[torch.logical_not(perm_mask)] = 1.0
        permutations = torch.argsort(perm_rand, dim=0)  # Generate permutations

        perm_sort = (
            torch.arange(0, perm_mask.shape[0], device=indata.device).unsqueeze(-1).repeat(1, perm_mask.shape[1])
        )
        perm_sort[torch.logical_not(perm_mask)] += perm_mask.shape[0] + 1
        perm_sort = torch.sort(perm_sort, dim=0)[0]  # Generate the origin of permutations

        _, permutation_columns = torch.nonzero(perm_mask_sorted, as_tuple=True)
        permutation_origin_indices = perm_sort[perm_mask_sorted]
        permutation_target_indices = permutations[perm_mask_sorted]

        newbatch = SolutionBatch(like=batch, empty=True)
        newdata = newbatch._data
        newdata[:] = indata[:]
        newdata[permutation_origin_indices, permutation_columns] = newdata[
            permutation_target_indices, permutation_columns
        ]

        return newbatch

obj_index: Optional[int] property readonly

Objective index according to which the operator will run. If permute_all was given as True, objectives are irrelevant, in which case obj_index is returned as None. If permute_all was given as False, the relevant obj_index is provided as an integer.

__init__(self, problem, obj_index=None, *, permute_all=False) special

__init__(...): Initialize the CosynePermutation.

Parameters:

Name Type Description Default
problem Problem

The problem object to work on.

required
obj_index Optional[int]

The index of the objective according to which the candidates for permutation will be selected. Can be left as None if the problem is single-objective, or if permute_all is given as True (in which case there will be no candidate selection as the entire population will be subject to permutation).

None
permute_all bool

Whether or not to apply permutation on the entire population, instead of using a selective permutation.

False
Source code in evotorch/operators/real.py
def __init__(self, problem: Problem, obj_index: Optional[int] = None, *, permute_all: bool = False):
    """
    `__init__(...)`: Initialize the CosynePermutation.

    Args:
        problem: The problem object to work on.
        obj_index: The index of the objective according to which the
            candidates for permutation will be selected.
            Can be left as None if the problem is single-objective,
            or if `permute_all` is given as True (in which case there
            will be no candidate selection as the entire population will
            be subject to permutation).
        permute_all: Whether or not to apply permutation on the entire
            population, instead of using a selective permutation.
    """

    if permute_all:
        if obj_index is not None:
            raise ValueError(
                "When `permute_all` is given as True (which seems to be the case)"
                " `obj_index` is expected as None,"
                " because the operator is independent of any objective and any fitness in this mode."
                " However, `permute_all` was found to be something other than None."
            )
        self._obj_index = None
    else:
        self._obj_index = problem.normalize_obj_index(obj_index)

    super().__init__(problem)

    self._permute_all = bool(permute_all)

GaussianMutation (CopyingOperator)

Gaussian mutation operator.

Follows the algorithm description in:

Sean Luke, 2013, Essentials of Metaheuristics, Lulu, second edition
available for free at http://cs.gmu.edu/~sean/book/metaheuristics/
Source code in evotorch/operators/real.py
class GaussianMutation(CopyingOperator):
    """
    Gaussian mutation operator.

    Follows the algorithm description in:

        Sean Luke, 2013, Essentials of Metaheuristics, Lulu, second edition
        available for free at http://cs.gmu.edu/~sean/book/metaheuristics/
    """

    def __init__(self, problem: Problem, *, stdev: float, mutation_probability: Optional[float] = 1.0):
        """
        `__init__(...)`: Initialize the GaussianMutation.

        Args:
            problem: The problem object to work with.
            stdev: The standard deviation of the Gaussian noise to apply on
                each decision variable.
            mutation_probability: The probability of mutation, for each
                decision variable.
                By default, the value of this argument is 1.0, which means
                that all of the decision variables will be affected by the
                mutation.
        """

        super().__init__(problem)
        self._mutation_probability = float(mutation_probability)
        self._stdev = float(stdev)

    @torch.no_grad()
    def _do(self, batch: SolutionBatch) -> SolutionBatch:
        result = deepcopy(batch)
        data = result.access_values()
        mutation_matrix = self.problem.make_uniform_shaped_like(data) <= self._mutation_probability
        data[mutation_matrix] += self._stdev * self.problem.make_gaussian_shaped_like(data[mutation_matrix])
        data[:] = self._respect_bounds(data)
        return result

__init__(self, problem, *, stdev, mutation_probability=1.0) special

__init__(...): Initialize the GaussianMutation.

Parameters:

Name Type Description Default
problem Problem

The problem object to work with.

required
stdev float

The standard deviation of the Gaussian noise to apply on each decision variable.

required
mutation_probability Optional[float]

The probability of mutation, for each decision variable. By default, the value of this argument is 1.0, which means that all of the decision variables will be affected by the mutation.

1.0
Source code in evotorch/operators/real.py
def __init__(self, problem: Problem, *, stdev: float, mutation_probability: Optional[float] = 1.0):
    """
    `__init__(...)`: Initialize the GaussianMutation.

    Args:
        problem: The problem object to work with.
        stdev: The standard deviation of the Gaussian noise to apply on
            each decision variable.
        mutation_probability: The probability of mutation, for each
            decision variable.
            By default, the value of this argument is 1.0, which means
            that all of the decision variables will be affected by the
            mutation.
    """

    super().__init__(problem)
    self._mutation_probability = float(mutation_probability)
    self._stdev = float(stdev)

OnePointCrossOver (CrossOver)

Representation of a one-point cross-over operator.

When this operator is applied on a SolutionBatch, a tournament selection technique is used for selecting parent solutions from the batch, and then those parent solutions are mated via cutting from a random position and recombining. The result of these recombination operations is a new SolutionBatch, containing the children solutions. The original SolutionBatch stays unmodified.

Source code in evotorch/operators/real.py
class OnePointCrossOver(CrossOver):
    """
    Representation of a one-point cross-over operator.

    When this operator is applied on a SolutionBatch,
    a tournament selection technique is used for selecting
    parent solutions from the batch, and then those parent
    solutions are mated via cutting from a random position
    and recombining. The result of these recombination
    operations is a new SolutionBatch, containing the children
    solutions. The original SolutionBatch stays unmodified.
    """

    def __init__(
        self,
        problem: Problem,
        *,
        tournament_size: int,
        obj_index: Optional[int] = None,
        num_children: Optional[int] = None,
        cross_over_rate: Optional[float] = None,
    ):
        """
        `__init__(...)`: Initialize the OnePointCrossOver.

        Args:
            problem: The problem object to work on.
            tournament_size: What is the size (or length) of a tournament
                when selecting a parent candidate from a population
            obj_index: Objective index according to which the selection
                will be done.
            num_children: Optionally a number of children to produce by the
                cross-over operation.
                Not to be used together with `cross_over_rate`.
                If `num_children` and `cross_over_rate` are both None,
                then the number of children is equal to the number
                of solutions received.
            cross_over_rate: Optionally expected as a real number between
                0.0 and 1.0. Specifies the number of cross-over operations
                to perform. 1.0 means `1.0 * len(solution_batch)` amount of
                cross overs will be performed, resulting in
                `2.0 * len(solution_batch)` amount of children.
                Not to be used together with `num_children`.
                If `num_children` and `cross_over_rate` are both None,
                then the number of children is equal to the number
                of solutions received.
        """

        super().__init__(
            problem,
            tournament_size=tournament_size,
            obj_index=obj_index,
            num_children=num_children,
            cross_over_rate=cross_over_rate,
        )

    @torch.no_grad()
    def _do_cross_over(self, parents1: torch.Tensor, parents2: torch.Tensor) -> SolutionBatch:
        # What we expect here is this:
        #
        #    parents1      parents2
        #    ==========    ==========
        #    parents1[0]   parents2[0]
        #    parents1[1]   parents2[1]
        #    ...           ...
        #    parents1[N]   parents2[N]
        #
        # where parents1 and parents2 are 2D tensors, each containing values of N solutions.
        # For each row i, we will apply cross-over on parents1[i] and parents2[i].
        # From each cross-over, we will obtain 2 children.
        # This means, there are N pairings, and 2N children.

        num_pairings = parents1.shape[0]
        # num_children = num_pairings * 2

        device = parents1[0].device
        dtype = parents1[0].dtype
        solution_length = len(parents1[0])

        # For each pairing, generate a gene index at which the parent solutions will be cut and recombined
        crossover_point = self.problem.make_randint((num_pairings, 1), n=(solution_length - 1), device=device) + 1

        # For each pairing, generate all gene indices (i.e. [0, 1, 2, ...] for each pairing)
        gene_indices = (
            torch.arange(0, solution_length, device=device).unsqueeze(0).expand(num_pairings, solution_length)
        )

        # Make a mask for crossing over. (0: take the value from one parent, 1: take the value from the other parent)
        # For gene indices less than crossover_point of that pairing, the mask takes the value 0.
        # Otherwise, the mask takes the value 1.
        crossover_mask = (gene_indices >= crossover_point).to(dtype)

        # Using the mask, generate two children.
        children1 = crossover_mask * parents1 + (1 - crossover_mask) * parents2
        children2 = crossover_mask * parents2 + (1 - crossover_mask) * parents1

        # Combine the children tensors in one big tensor
        children = torch.cat([children1, children2], dim=0)

        # Write the children solutions into a new SolutionBatch, and return the new batch
        result = self._make_children_batch(children)
        return result

__init__(self, problem, *, tournament_size, obj_index=None, num_children=None, cross_over_rate=None) special

__init__(...): Initialize the OnePointCrossOver.

Parameters:

Name Type Description Default
problem Problem

The problem object to work on.

required
tournament_size int

What is the size (or length) of a tournament when selecting a parent candidate from a population

required
obj_index Optional[int]

Objective index according to which the selection will be done.

None
num_children Optional[int]

Optionally a number of children to produce by the cross-over operation. Not to be used together with cross_over_rate. If num_children and cross_over_rate are both None, then the number of children is equal to the number of solutions received.

None
cross_over_rate Optional[float]

Optionally expected as a real number between 0.0 and 1.0. Specifies the number of cross-over operations to perform. 1.0 means 1.0 * len(solution_batch) amount of cross overs will be performed, resulting in 2.0 * len(solution_batch) amount of children. Not to be used together with num_children. If num_children and cross_over_rate are both None, then the number of children is equal to the number of solutions received.

None
Source code in evotorch/operators/real.py
def __init__(
    self,
    problem: Problem,
    *,
    tournament_size: int,
    obj_index: Optional[int] = None,
    num_children: Optional[int] = None,
    cross_over_rate: Optional[float] = None,
):
    """
    `__init__(...)`: Initialize the OnePointCrossOver.

    Args:
        problem: The problem object to work on.
        tournament_size: What is the size (or length) of a tournament
            when selecting a parent candidate from a population
        obj_index: Objective index according to which the selection
            will be done.
        num_children: Optionally a number of children to produce by the
            cross-over operation.
            Not to be used together with `cross_over_rate`.
            If `num_children` and `cross_over_rate` are both None,
            then the number of children is equal to the number
            of solutions received.
        cross_over_rate: Optionally expected as a real number between
            0.0 and 1.0. Specifies the number of cross-over operations
            to perform. 1.0 means `1.0 * len(solution_batch)` amount of
            cross overs will be performed, resulting in
            `2.0 * len(solution_batch)` amount of children.
            Not to be used together with `num_children`.
            If `num_children` and `cross_over_rate` are both None,
            then the number of children is equal to the number
            of solutions received.
    """

    super().__init__(
        problem,
        tournament_size=tournament_size,
        obj_index=obj_index,
        num_children=num_children,
        cross_over_rate=cross_over_rate,
    )

SimulatedBinaryCrossOver (CrossOver)

Representation of a simulated binary cross-over (SBX).

When this operator is applied on a SolutionBatch, a tournament selection technique is used for selecting parent solutions from the batch, and then those parent solutions are mated via SBX. The generated children solutions are given in a new SolutionBatch. The original SolutionBatch stays unmodified.

Reference:

Kalyanmoy Deb, Hans-Georg Beyer (2001).
Self-Adaptive Genetic Algorithms with Simulated Binary Crossover.
Source code in evotorch/operators/real.py
class SimulatedBinaryCrossOver(CrossOver):
    """
    Representation of a simulated binary cross-over (SBX).

    When this operator is applied on a SolutionBatch,
    a tournament selection technique is used for selecting
    parent solutions from the batch, and then those parent
    solutions are mated via SBX. The generated children
    solutions are given in a new SolutionBatch.
    The original SolutionBatch stays unmodified.

    Reference:

        Kalyanmoy Deb, Hans-Georg Beyer (2001).
        Self-Adaptive Genetic Algorithms with Simulated Binary Crossover.
    """

    def __init__(
        self,
        problem: Problem,
        *,
        tournament_size: int,
        eta: float,
        obj_index: Optional[int] = None,
        num_children: Optional[int] = None,
        cross_over_rate: Optional[float] = None,
    ):
        """
        `__init__(...)`: Initialize the SimulatedBinaryCrossOver.

        Args:
            problem: Problem object to work with.
            tournament_size: What is the size (or length) of a tournament
                when selecting a parent candidate from a population.
            eta: The crowding index, expected as a float.
                Bigger eta values result in children closer
                to their parents.
            obj_index: Objective index according to which the selection
                will be done.
            num_children: Optionally a number of children to produce by the
                cross-over operation.
                Not to be used together with `cross_over_rate`.
                If `num_children` and `cross_over_rate` are both None,
                then the number of children is equal to the number
                of solutions received.
            cross_over_rate: Optionally expected as a real number between
                0.0 and 1.0. Specifies the number of cross-over operations
                to perform. 1.0 means `1.0 * len(solution_batch)` amount of
                cross overs will be performed, resulting in
                `2.0 * len(solution_batch)` amount of children.
                Not to be used together with `num_children`.
                If `num_children` and `cross_over_rate` are both None,
                then the number of children is equal to the number
                of solutions received.
        """

        super().__init__(
            problem,
            tournament_size=int(tournament_size),
            obj_index=obj_index,
            num_children=num_children,
            cross_over_rate=cross_over_rate,
        )
        self._eta = float(eta)

    def _do_cross_over(self, parents1: torch.Tensor, parents2: torch.Tensor) -> SolutionBatch:
        # Generate u_i values which determine the spread
        u = self.problem.make_uniform_shaped_like(parents1)

        # Compute beta_i values from u_i values as the actual spread per dimension
        betas = (2 * u).pow(1.0 / (self._eta + 1.0))  # Compute all values for u_i < 0.5 first
        betas[u > 0.5] = (1.0 / (2 * (1.0 - u[u > 0.5]))).pow(
            1.0 / (self._eta + 1.0)
        )  # Replace the values for u_i >= 0.5
        children1 = 0.5 * (
            (1 + betas) * parents1 + (1 - betas) * parents2
        )  # Create the first set of children from the beta values
        children2 = 0.5 * (
            (1 + betas) * parents2 + (1 - betas) * parents1
        )  # Create the second set of children as a mirror of the first set of children

        # Combine the children tensors in one big tensor
        children = torch.cat([children1, children2], dim=0)

        # Respect the lower and upper bounds defined by the problem object
        children = self._respect_bounds(children)

        # Write the children solutions into a new SolutionBatch, and return the new batch
        result = self._make_children_batch(children)

        return result

__init__(self, problem, *, tournament_size, eta, obj_index=None, num_children=None, cross_over_rate=None) special

__init__(...): Initialize the SimulatedBinaryCrossOver.

Parameters:

Name Type Description Default
problem Problem

Problem object to work with.

required
tournament_size int

What is the size (or length) of a tournament when selecting a parent candidate from a population.

required
eta float

The crowding index, expected as a float. Bigger eta values result in children closer to their parents.

required
obj_index Optional[int]

Objective index according to which the selection will be done.

None
num_children Optional[int]

Optionally a number of children to produce by the cross-over operation. Not to be used together with cross_over_rate. If num_children and cross_over_rate are both None, then the number of children is equal to the number of solutions received.

None
cross_over_rate Optional[float]

Optionally expected as a real number between 0.0 and 1.0. Specifies the number of cross-over operations to perform. 1.0 means 1.0 * len(solution_batch) amount of cross overs will be performed, resulting in 2.0 * len(solution_batch) amount of children. Not to be used together with num_children. If num_children and cross_over_rate are both None, then the number of children is equal to the number of solutions received.

None
Source code in evotorch/operators/real.py
def __init__(
    self,
    problem: Problem,
    *,
    tournament_size: int,
    eta: float,
    obj_index: Optional[int] = None,
    num_children: Optional[int] = None,
    cross_over_rate: Optional[float] = None,
):
    """
    `__init__(...)`: Initialize the SimulatedBinaryCrossOver.

    Args:
        problem: Problem object to work with.
        tournament_size: What is the size (or length) of a tournament
            when selecting a parent candidate from a population.
        eta: The crowding index, expected as a float.
            Bigger eta values result in children closer
            to their parents.
        obj_index: Objective index according to which the selection
            will be done.
        num_children: Optionally a number of children to produce by the
            cross-over operation.
            Not to be used together with `cross_over_rate`.
            If `num_children` and `cross_over_rate` are both None,
            then the number of children is equal to the number
            of solutions received.
        cross_over_rate: Optionally expected as a real number between
            0.0 and 1.0. Specifies the number of cross-over operations
            to perform. 1.0 means `1.0 * len(solution_batch)` amount of
            cross overs will be performed, resulting in
            `2.0 * len(solution_batch)` amount of children.
            Not to be used together with `num_children`.
            If `num_children` and `cross_over_rate` are both None,
            then the number of children is equal to the number
            of solutions received.
    """

    super().__init__(
        problem,
        tournament_size=int(tournament_size),
        obj_index=obj_index,
        num_children=num_children,
        cross_over_rate=cross_over_rate,
    )
    self._eta = float(eta)

sequence

This module contains operators for problems whose solutions contain variable-length sequences (list-like objects).

CutAndSplice (CrossOver)

Cut & Splice operator for variable-length solutions.

This class serves as a cross-over operator to be used on problems with their dtypes set as object, and with their solutions initialized to contain variable-length sequences (list-like objects).

Reference:

David E. Goldberg, Bradley Korb, Kalyanmoy Deb (1989).
Messy Genetic Algorithms: Motivation, Analysis, and First Results.
Complex Systems 3, 493-530.
Source code in evotorch/operators/sequence.py
class CutAndSplice(CrossOver):
    """Cut & Splice operator for variable-length solutions.

    This class serves as a cross-over operator to be used on problems
    with their `dtype`s set as `object`, and with their solutions
    initialized to contain variable-length sequences (list-like objects).

    Reference:

        David E. Goldberg, Bradley Korb, Kalyanmoy Deb (1989).
        Messy Genetic Algorithms: Motivation, Analysis, and First Results.
        Complex Systems 3, 493-530.
    """

    def _cut_and_splice(
        self,
        parents1: ObjectArray,
        parents2: ObjectArray,
        children1: SolutionBatch,
        children2: SolutionBatch,
        row_index: int,
    ):
        parvals1 = parents1[row_index]
        parvals2 = parents2[row_index]

        length1 = len(parvals1)
        length2 = len(parvals2)

        cutpoint1 = int(self.problem.make_randint(tuple(), n=length1))
        cutpoint2 = int(self.problem.make_randint(tuple(), n=length2))

        childvals1 = parvals1[:cutpoint1]
        childvals1.extend(parvals2[cutpoint2:])

        childvals2 = parvals2[:cutpoint2]
        childvals2.extend(parvals1[cutpoint1:])

        children1.access_values(keep_evals=True)[row_index] = childvals1
        children2.access_values(keep_evals=True)[row_index] = childvals2

    def _do_cross_over(self, parents1: ObjectArray, parents2: ObjectArray) -> SolutionBatch:
        n = len(parents1)

        children1 = SolutionBatch(self.problem, popsize=n, empty=True)
        children2 = SolutionBatch(self.problem, popsize=n, empty=True)

        for i in range(n):
            self._cut_and_splice(parents1, parents2, children1, children2, i)

        return children1.concat(children2)