Source code for moptipy.api.operators

"""
The base classes for implementing search operators.

Nullary search operators are used to sample the initial starting points of the
optimization processes. They inherit from class
:class:`~moptipy.api.operators.Op0`. The pre-defined unit test routine
:func:`~moptipy.tests.op0.validate_op0` can and should be used to test all the
nullary operators that are implemented.

Unary search operators accept one point in the search space as input and
generate a new, similar point as output. They inherit from class
:class:`~moptipy.api.operators.Op1`. The pre-defined unit test routine
:func:`~moptipy.tests.op1.validate_op1` can and should be used to test all the
unary operators that are implemented.

The basic unary operators :class:`~moptipy.api.operators.Op1` have no
parameter telling them how much of the input point to change. They may do a
hard-coded number of modifications (as, e.g.,
:class:`~moptipy.operators.permutations.op1_swap2.Op1Swap2` does) or may
apply a random number of modifications (like
:class:`~moptipy.operators.permutations.op1_swapn.Op1SwapN`). There is a
sub-class of unary operators named
:class:`~moptipy.api.operators.Op1WithStepSize` where a parameter `step_size`
with a value from the closed interval `[0.0, 1.0]` can be supplied. If
`step_size=0.0`, such an operator should perform the smallest possible
modification and for `step_size=1.0`, it should perform the largest possible
modification.

Binary search operators accept two points in the search space as input and
generate a new point that should be similar to both inputs as output. They
inherit from class :class:`~moptipy.api.operators.Op2`. The pre-defined unit
test routine :func:`~moptipy.tests.op2.validate_op2` can and should be used to
test all the binary operators that are implemented.
"""
from typing import Any

from numpy.random import Generator
from pycommons.types import type_error

from moptipy.api.component import Component


# start op0
[docs] class Op0(Component): """A base class to implement a nullary search operator."""
[docs] def op0(self, random: Generator, dest) -> None: """ Apply the nullary search operator to fill object `dest`. Afterwards `dest` will hold a valid point in the search space. Often, this would be a point uniformly randomly sampled from the search space, but it could also be the result of a heuristic or even a specific solution. :param random: the random number generator :param dest: the destination data structure """ raise ValueError("Method not implemented!")
# end op0
[docs] def check_op0(op0: Any) -> Op0: """ Check whether an object is a valid instance of :class:`Op0`. :param op0: the (supposed) instance of :class:`Op0` :return: the object `op0` :raises TypeError: if `op0` is not an instance of :class:`Op0` >>> check_op0(Op0()) Op0 >>> try: ... check_op0('A') ... except TypeError as te: ... print(te) op0 should be an instance of moptipy.api.operators.Op0 but is \ str, namely 'A'. >>> try: ... check_op0(None) ... except TypeError as te: ... print(te) op0 should be an instance of moptipy.api.operators.Op0 but is None. """ if isinstance(op0, Op0): return op0 raise type_error(op0, "op0", Op0)
# start op1
[docs] class Op1(Component): """A base class to implement a unary search operator."""
[docs] def op1(self, random: Generator, dest, x) -> None: """ Fill `dest` with a modified copy of `x`. :param random: the random number generator :param dest: the destination data structure :param x: the source point in the search space """ raise ValueError("Method not implemented!")
# end op1
[docs] def check_op1(op1: Any) -> Op1: """ Check whether an object is a valid instance of :class:`Op1`. :param op1: the (supposed) instance of :class:`Op1` :return: the object :raises TypeError: if `op1` is not an instance of :class:`Op1` >>> check_op1(Op1()) Op1 >>> try: ... check_op1('A') ... except TypeError as te: ... print(te) op1 should be an instance of moptipy.api.operators.Op1 but is str, \ namely 'A'. >>> try: ... check_op1(None) ... except TypeError as te: ... print(te) op1 should be an instance of moptipy.api.operators.Op1 but is None. """ if isinstance(op1, Op1): return op1 raise type_error(op1, "op1", Op1)
# start op2
[docs] class Op2(Component): """A base class to implement a binary search operator."""
[docs] def op2(self, random: Generator, dest, x0, x1) -> None: """ Fill `dest` with a combination of `x0` and `x1`. :param random: the random number generator :param dest: the destination data structure :param x0: the first source point in the search space :param x1: the second source point in the search space """ raise ValueError("Method not implemented!")
# end op2
[docs] def check_op2(op2: Any) -> Op2: """ Check whether an object is a valid instance of :class:`Op2`. :param op2: the (supposed) instance of :class:`Op2` :return: the object `op2` :raises TypeError: if `op2` is not an instance of :class:`Op2` >>> check_op2(Op2()) Op2 >>> try: ... check_op2('A') ... except TypeError as te: ... print(te) op2 should be an instance of moptipy.api.operators.Op2 but is str, \ namely 'A'. >>> try: ... check_op2(None) ... except TypeError as te: ... print(te) op2 should be an instance of moptipy.api.operators.Op2 but is None. """ if isinstance(op2, Op2): return op2 raise type_error(op2, "op2", Op2)
# start op1WithStepSize
[docs] class Op1WithStepSize(Op1): """A unary search operator with a step size."""
[docs] def op1(self, random: Generator, dest, x, step_size: float = 0.0) -> None: """ Copy `x` to `dest` but apply a modification with a given `step_size`. This operator is similar to :meth:`Op1.op1` in that it stores a modified copy of `x` into `dest`. The difference is that you can also specify how much that copy should be different: The parameter `step_size` can take on any value in the interval `[0.0, 1.0]`, including the two boundary values. A `step_size` of `0.0` indicates the smallest possible move (for which `dest` will still be different from `x`) and `step_size=1.0` will lead to the largest possible move. The `step_size` may be interpreted differently by different operators: Some may interpret it as an exact requirement and enforce steps of the exact specified size, see, for example module :mod:`~moptipy.operators.bitstrings.op1_flip_m`. Others might interpret it stochastically as an expectation. Yet others may interpret it as a goal step width and try to realize it in a best effort kind of way, but may also do smaller or larger steps if the best effort fails, see for example module :mod:`~moptipy.operators.permutations.op1_swap_exactly_n`. What all operators should, however, have in common is that at `step_size=0.0`, they should try to perform a smallest possible change and at `step_size=1.0`, they should try to perform a largest possible change. For all values in between, step sizes should grow with rising `step_size`. This should allow algorithms that know nothing about the nature of the search space or the operator's moves to still tune between small and large moves based on a policy which makes sense in a black-box setting. Every implementation of :class:`Op1WithStepSize` must specify a reasonable default value for this parameter ensure compatibility with :meth:`Op1.op1`. In this base class, we set the default to `0.0`. Finally, if a `step_size` value is passed in which is outside the interval `[0, 1]`, the behavior of this method is undefined. It may throw an exception or not. It may also enter an infinite loop. :param random: the random number generator :param dest: the destination data structure :param x: the source point in the search space :param step_size: the step size parameter for the unary operator """ raise ValueError("Method not implemented!")
# end op1WithStepSize
[docs] def check_op1_with_step_size(op1: Any) -> Op1WithStepSize: """ Check whether an object is a valid instance of :class:`Op1WithStepSize`. :param op1: the (supposed) instance of :class:`Op1WithStepSize` :return: the object `op1` :raises TypeError: if `op1` is not an instance of :class:`Op1WithStepSize` >>> check_op1_with_step_size(Op1WithStepSize()) Op1WithStepSize >>> try: ... check_op1_with_step_size('A') ... except TypeError as te: ... print(te) op1 should be an instance of moptipy.api.operators.Op1WithStepSize \ but is str, namely 'A'. >>> try: ... check_op1_with_step_size(Op1()) ... except TypeError as te: ... print(te) op1 should be an instance of moptipy.api.operators.Op1WithStepSize \ but is moptipy.api.operators.Op1. >>> try: ... check_op1_with_step_size(None) ... except TypeError as te: ... print(te) op1 should be an instance of moptipy.api.operators.Op1WithStepSize \ but is None. """ if isinstance(op1, Op1WithStepSize): return op1 raise type_error(op1, "op1", Op1WithStepSize)