Coverage for moptipy/api/operators.py: 83%
36 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-07 22:33 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-07 22:33 +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
36from typing import Generator as ItGen
38from numpy.random import Generator
39from pycommons.types import type_error
41from moptipy.api.component import Component
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!")
62def check_op0(op0: Any) -> Op0:
63 """
64 Check whether an object is a valid instance of :class:`Op0`.
66 :param op0: the (supposed) instance of :class:`Op0`
67 :return: the object `op0`
68 :raises TypeError: if `op0` is not an instance of :class:`Op0`
70 >>> check_op0(Op0())
71 Op0
72 >>> try:
73 ... check_op0('A')
74 ... except TypeError as te:
75 ... print(te)
76 op0 should be an instance of moptipy.api.operators.Op0 but is \
77str, namely 'A'.
79 >>> try:
80 ... check_op0(None)
81 ... except TypeError as te:
82 ... print(te)
83 op0 should be an instance of moptipy.api.operators.Op0 but is None.
84 """
85 if isinstance(op0, Op0):
86 return op0
87 raise type_error(op0, "op0", Op0)
90class Op1(Component):
91 """A base class to implement a unary search operator."""
93 def op1(self, random: Generator, dest, x) -> None:
94 """
95 Fill `dest` with a modified copy of `x`.
97 :param random: the random number generator
98 :param dest: the destination data structure
99 :param x: the source point in the search space
100 """
101 raise ValueError("Method not implemented!")
103 def iterate(self, random: Generator, dest, x) -> ItGen[Any, None, None]:
104 """
105 Iterate possible moves.
107 This function returns a generator which iterates over a subset or the
108 whole of the neighborhood spanned by :meth:`~op1`.
109 In each step the parameter `dest` is modified.
111 The value yielded is implementation-specific.
112 For some operator implementations, it may be the index of a decision
113 variable that was changed.
114 For other implementations, it may be `dest` itself.
115 Don't rely on it unless you know what you are doing.
117 In ideal implementations, the order in which neighborhood is sampled
118 should be randomized.
119 This would allow you to draw a random sample of at most a pre-defined
120 size from the neighborhood.
122 The default implementation here just applies :meth:`~op1` once and
123 yields `dest`.
125 :param random: the random number generator
126 :param dest: the destination data structure
127 :param x: the source point in the search space
128 """
129 self.op1(random, dest, x)
130 yield dest
133def check_op1(op1: Any) -> Op1:
134 """
135 Check whether an object is a valid instance of :class:`Op1`.
137 :param op1: the (supposed) instance of :class:`Op1`
138 :return: the object
139 :raises TypeError: if `op1` is not an instance of :class:`Op1`
141 >>> check_op1(Op1())
142 Op1
143 >>> try:
144 ... check_op1('A')
145 ... except TypeError as te:
146 ... print(te)
147 op1 should be an instance of moptipy.api.operators.Op1 but is str, \
148namely 'A'.
150 >>> try:
151 ... check_op1(None)
152 ... except TypeError as te:
153 ... print(te)
154 op1 should be an instance of moptipy.api.operators.Op1 but is None.
155 """
156 if isinstance(op1, Op1):
157 return op1
158 raise type_error(op1, "op1", Op1)
161class Op2(Component):
162 """A base class to implement a binary search operator."""
164 def op2(self, random: Generator, dest, x0, x1) -> None:
165 """
166 Fill `dest` with a combination of `x0` and `x1`.
168 :param random: the random number generator
169 :param dest: the destination data structure
170 :param x0: the first source point in the search space
171 :param x1: the second source point in the search space
172 """
173 raise ValueError("Method not implemented!")
176def check_op2(op2: Any) -> Op2:
177 """
178 Check whether an object is a valid instance of :class:`Op2`.
180 :param op2: the (supposed) instance of :class:`Op2`
181 :return: the object `op2`
182 :raises TypeError: if `op2` is not an instance of :class:`Op2`
184 >>> check_op2(Op2())
185 Op2
186 >>> try:
187 ... check_op2('A')
188 ... except TypeError as te:
189 ... print(te)
190 op2 should be an instance of moptipy.api.operators.Op2 but is str, \
191namely 'A'.
193 >>> try:
194 ... check_op2(None)
195 ... except TypeError as te:
196 ... print(te)
197 op2 should be an instance of moptipy.api.operators.Op2 but is None.
198 """
199 if isinstance(op2, Op2):
200 return op2
201 raise type_error(op2, "op2", Op2)
204class Op1WithStepSize(Op1):
205 """A unary search operator with a step size."""
207 def op1(self, random: Generator, dest, x, step_size: float = 0.0) -> None:
208 """
209 Copy `x` to `dest` but apply a modification with a given `step_size`.
211 This operator is similar to :meth:`Op1.op1` in that it stores a
212 modified copy of `x` into `dest`. The difference is that you can also
213 specify how much that copy should be different: The parameter
214 `step_size` can take on any value in the interval `[0.0, 1.0]`,
215 including the two boundary values. A `step_size` of `0.0` indicates
216 the smallest possible move (for which `dest` will still be different
217 from `x`) and `step_size=1.0` will lead to the largest possible move.
219 The `step_size` may be interpreted differently by different operators:
220 Some may interpret it as an exact requirement and enforce steps of the
221 exact specified size, see, for example module
222 :mod:`~moptipy.operators.bitstrings.op1_flip_m`. Others might
223 interpret it stochastically as an expectation. Yet others may
224 interpret it as a goal step width and try to realize it in a best
225 effort kind of way, but may also do smaller or larger steps if the
226 best effort fails, see for example module
227 :mod:`~moptipy.operators.permutations.op1_swap_exactly_n`.
228 What all operators should, however, have in common is that at
229 `step_size=0.0`, they should try to perform a smallest possible change
230 and at `step_size=1.0`, they should try to perform a largest possible
231 change. For all values in between, step sizes should grow with rising
232 `step_size`. This should allow algorithms that know nothing about the
233 nature of the search space or the operator's moves to still tune
234 between small and large moves based on a policy which makes sense in a
235 black-box setting.
237 Every implementation of :class:`Op1WithStepSize` must specify a
238 reasonable default value for this parameter ensure compatibility with
239 :meth:`Op1.op1`. In this base class, we set the default to `0.0`.
241 Finally, if a `step_size` value is passed in which is outside the
242 interval `[0, 1]`, the behavior of this method is undefined. It may
243 throw an exception or not. It may also enter an infinite loop.
245 :param random: the random number generator
246 :param dest: the destination data structure
247 :param x: the source point in the search space
248 :param step_size: the step size parameter for the unary operator
249 """
250 raise ValueError("Method not implemented!")
253def check_op1_with_step_size(op1: Any) -> Op1WithStepSize:
254 """
255 Check whether an object is a valid instance of :class:`Op1WithStepSize`.
257 :param op1: the (supposed) instance of :class:`Op1WithStepSize`
258 :return: the object `op1`
259 :raises TypeError: if `op1` is not an instance of :class:`Op1WithStepSize`
261 >>> check_op1_with_step_size(Op1WithStepSize())
262 Op1WithStepSize
263 >>> try:
264 ... check_op1_with_step_size('A')
265 ... except TypeError as te:
266 ... print(te)
267 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
268but is str, namely 'A'.
270 >>> try:
271 ... check_op1_with_step_size(Op1())
272 ... except TypeError as te:
273 ... print(te)
274 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
275but is moptipy.api.operators.Op1.
277 >>> try:
278 ... check_op1_with_step_size(None)
279 ... except TypeError as te:
280 ... print(te)
281 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
282but is None.
283 """
284 if isinstance(op1, Op1WithStepSize):
285 return op1
286 raise type_error(op1, "op1", Op1WithStepSize)