Coverage for moptipy / api / operators.py: 88%
32 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 11:18 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 11:18 +0000
1"""
2The base classes for implementing search operators.
4Nullary search operators are used to sample the initial starting points of the
5optimization processes. They inherit from class
6:class:`~moptipy.api.operators.Op0`. The pre-defined unit test routine
7:func:`~moptipy.tests.op0.validate_op0` can and should be used to test all the
8nullary operators that are implemented.
10Unary search operators accept one point in the search space as input and
11generate a new, similar point as output. They inherit from class
12:class:`~moptipy.api.operators.Op1`. The pre-defined unit test routine
13:func:`~moptipy.tests.op1.validate_op1` can and should be used to test all the
14unary operators that are implemented.
16The basic unary operators :class:`~moptipy.api.operators.Op1` have no
17parameter telling them how much of the input point to change. They may do a
18hard-coded number of modifications (as, e.g.,
19:class:`~moptipy.operators.permutations.op1_swap2.Op1Swap2` does) or may
20apply a random number of modifications (like
21:class:`~moptipy.operators.permutations.op1_swapn.Op1SwapN`). There is a
22sub-class of unary operators named
23:class:`~moptipy.api.operators.Op1WithStepSize` where a parameter `step_size`
24with a value from the closed interval `[0.0, 1.0]` can be supplied. If
25`step_size=0.0`, such an operator should perform the smallest possible
26modification and for `step_size=1.0`, it should perform the largest possible
27modification.
29Binary search operators accept two points in the search space as input and
30generate a new point that should be similar to both inputs as output. They
31inherit from class :class:`~moptipy.api.operators.Op2`. The pre-defined unit
32test routine :func:`~moptipy.tests.op2.validate_op2` can and should be used to
33test all the binary operators that are implemented.
34"""
35from typing import Any
37from numpy.random import Generator
38from pycommons.types import type_error
40from moptipy.api.component import Component
43# start op0
44class Op0(Component):
45 """A base class to implement a nullary search operator."""
47 def op0(self, random: Generator, dest) -> None:
48 """
49 Apply the nullary search operator to fill object `dest`.
51 Afterwards `dest` will hold a valid point in the search space.
52 Often, this would be a point uniformly randomly sampled from the
53 search space, but it could also be the result of a heuristic or
54 even a specific solution.
56 :param random: the random number generator
57 :param dest: the destination data structure
58 """
59 raise ValueError("Method not implemented!")
60# end op0
63def check_op0(op0: Any) -> Op0:
64 """
65 Check whether an object is a valid instance of :class:`Op0`.
67 :param op0: the (supposed) instance of :class:`Op0`
68 :return: the object `op0`
69 :raises TypeError: if `op0` is not an instance of :class:`Op0`
71 >>> check_op0(Op0())
72 Op0
73 >>> try:
74 ... check_op0('A')
75 ... except TypeError as te:
76 ... print(te)
77 op0 should be an instance of moptipy.api.operators.Op0 but is \
78str, namely 'A'.
80 >>> try:
81 ... check_op0(None)
82 ... except TypeError as te:
83 ... print(te)
84 op0 should be an instance of moptipy.api.operators.Op0 but is None.
85 """
86 if isinstance(op0, Op0):
87 return op0
88 raise type_error(op0, "op0", Op0)
91# start op1
92class Op1(Component):
93 """A base class to implement a unary search operator."""
95 def op1(self, random: Generator, dest, x) -> None:
96 """
97 Fill `dest` with a modified copy of `x`.
99 :param random: the random number generator
100 :param dest: the destination data structure
101 :param x: the source point in the search space
102 """
103 raise ValueError("Method not implemented!")
104# end op1
107def check_op1(op1: Any) -> Op1:
108 """
109 Check whether an object is a valid instance of :class:`Op1`.
111 :param op1: the (supposed) instance of :class:`Op1`
112 :return: the object
113 :raises TypeError: if `op1` is not an instance of :class:`Op1`
115 >>> check_op1(Op1())
116 Op1
117 >>> try:
118 ... check_op1('A')
119 ... except TypeError as te:
120 ... print(te)
121 op1 should be an instance of moptipy.api.operators.Op1 but is str, \
122namely 'A'.
124 >>> try:
125 ... check_op1(None)
126 ... except TypeError as te:
127 ... print(te)
128 op1 should be an instance of moptipy.api.operators.Op1 but is None.
129 """
130 if isinstance(op1, Op1):
131 return op1
132 raise type_error(op1, "op1", Op1)
135# start op2
136class Op2(Component):
137 """A base class to implement a binary search operator."""
139 def op2(self, random: Generator, dest, x0, x1) -> None:
140 """
141 Fill `dest` with a combination of `x0` and `x1`.
143 :param random: the random number generator
144 :param dest: the destination data structure
145 :param x0: the first source point in the search space
146 :param x1: the second source point in the search space
147 """
148 raise ValueError("Method not implemented!")
149# end op2
152def check_op2(op2: Any) -> Op2:
153 """
154 Check whether an object is a valid instance of :class:`Op2`.
156 :param op2: the (supposed) instance of :class:`Op2`
157 :return: the object `op2`
158 :raises TypeError: if `op2` is not an instance of :class:`Op2`
160 >>> check_op2(Op2())
161 Op2
162 >>> try:
163 ... check_op2('A')
164 ... except TypeError as te:
165 ... print(te)
166 op2 should be an instance of moptipy.api.operators.Op2 but is str, \
167namely 'A'.
169 >>> try:
170 ... check_op2(None)
171 ... except TypeError as te:
172 ... print(te)
173 op2 should be an instance of moptipy.api.operators.Op2 but is None.
174 """
175 if isinstance(op2, Op2):
176 return op2
177 raise type_error(op2, "op2", Op2)
180# start op1WithStepSize
181class Op1WithStepSize(Op1):
182 """A unary search operator with a step size."""
184 def op1(self, random: Generator, dest, x, step_size: float = 0.0) -> None:
185 """
186 Copy `x` to `dest` but apply a modification with a given `step_size`.
188 This operator is similar to :meth:`Op1.op1` in that it stores a
189 modified copy of `x` into `dest`. The difference is that you can also
190 specify how much that copy should be different: The parameter
191 `step_size` can take on any value in the interval `[0.0, 1.0]`,
192 including the two boundary values. A `step_size` of `0.0` indicates
193 the smallest possible move (for which `dest` will still be different
194 from `x`) and `step_size=1.0` will lead to the largest possible move.
196 The `step_size` may be interpreted differently by different operators:
197 Some may interpret it as an exact requirement and enforce steps of the
198 exact specified size, see, for example module
199 :mod:`~moptipy.operators.bitstrings.op1_flip_m`. Others might
200 interpret it stochastically as an expectation. Yet others may
201 interpret it as a goal step width and try to realize it in a best
202 effort kind of way, but may also do smaller or larger steps if the
203 best effort fails, see for example module
204 :mod:`~moptipy.operators.permutations.op1_swap_exactly_n`.
205 What all operators should, however, have in common is that at
206 `step_size=0.0`, they should try to perform a smallest possible change
207 and at `step_size=1.0`, they should try to perform a largest possible
208 change. For all values in between, step sizes should grow with rising
209 `step_size`. This should allow algorithms that know nothing about the
210 nature of the search space or the operator's moves to still tune
211 between small and large moves based on a policy which makes sense in a
212 black-box setting.
214 Every implementation of :class:`Op1WithStepSize` must specify a
215 reasonable default value for this parameter ensure compatibility with
216 :meth:`Op1.op1`. In this base class, we set the default to `0.0`.
218 Finally, if a `step_size` value is passed in which is outside the
219 interval `[0, 1]`, the behavior of this method is undefined. It may
220 throw an exception or not. It may also enter an infinite loop.
222 :param random: the random number generator
223 :param dest: the destination data structure
224 :param x: the source point in the search space
225 :param step_size: the step size parameter for the unary operator
226 """
227 raise ValueError("Method not implemented!")
228# end op1WithStepSize
231def check_op1_with_step_size(op1: Any) -> Op1WithStepSize:
232 """
233 Check whether an object is a valid instance of :class:`Op1WithStepSize`.
235 :param op1: the (supposed) instance of :class:`Op1WithStepSize`
236 :return: the object `op1`
237 :raises TypeError: if `op1` is not an instance of :class:`Op1WithStepSize`
239 >>> check_op1_with_step_size(Op1WithStepSize())
240 Op1WithStepSize
241 >>> try:
242 ... check_op1_with_step_size('A')
243 ... except TypeError as te:
244 ... print(te)
245 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
246but is str, namely 'A'.
248 >>> try:
249 ... check_op1_with_step_size(Op1())
250 ... except TypeError as te:
251 ... print(te)
252 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
253but is moptipy.api.operators.Op1.
255 >>> try:
256 ... check_op1_with_step_size(None)
257 ... except TypeError as te:
258 ... print(te)
259 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
260but is None.
261 """
262 if isinstance(op1, Op1WithStepSize):
263 return op1
264 raise type_error(op1, "op1", Op1WithStepSize)