Coverage for moptipy / api / operators.py: 88%
32 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +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'.
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)
90# start op1
91class Op1(Component):
92 """A base class to implement a unary search operator."""
94 def op1(self, random: Generator, dest, x) -> None:
95 """
96 Fill `dest` with a modified copy of `x`.
98 :param random: the random number generator
99 :param dest: the destination data structure
100 :param x: the source point in the search space
101 """
102 raise ValueError("Method not implemented!")
103# end op1
106def check_op1(op1: Any) -> Op1:
107 """
108 Check whether an object is a valid instance of :class:`Op1`.
110 :param op1: the (supposed) instance of :class:`Op1`
111 :return: the object
112 :raises TypeError: if `op1` is not an instance of :class:`Op1`
114 >>> check_op1(Op1())
115 Op1
116 >>> try:
117 ... check_op1('A')
118 ... except TypeError as te:
119 ... print(te)
120 op1 should be an instance of moptipy.api.operators.Op1 but is str, \
121namely 'A'.
122 >>> try:
123 ... check_op1(None)
124 ... except TypeError as te:
125 ... print(te)
126 op1 should be an instance of moptipy.api.operators.Op1 but is None.
127 """
128 if isinstance(op1, Op1):
129 return op1
130 raise type_error(op1, "op1", Op1)
133# start op2
134class Op2(Component):
135 """A base class to implement a binary search operator."""
137 def op2(self, random: Generator, dest, x0, x1) -> None:
138 """
139 Fill `dest` with a combination of `x0` and `x1`.
141 :param random: the random number generator
142 :param dest: the destination data structure
143 :param x0: the first source point in the search space
144 :param x1: the second source point in the search space
145 """
146 raise ValueError("Method not implemented!")
147# end op2
150def check_op2(op2: Any) -> Op2:
151 """
152 Check whether an object is a valid instance of :class:`Op2`.
154 :param op2: the (supposed) instance of :class:`Op2`
155 :return: the object `op2`
156 :raises TypeError: if `op2` is not an instance of :class:`Op2`
158 >>> check_op2(Op2())
159 Op2
160 >>> try:
161 ... check_op2('A')
162 ... except TypeError as te:
163 ... print(te)
164 op2 should be an instance of moptipy.api.operators.Op2 but is str, \
165namely 'A'.
166 >>> try:
167 ... check_op2(None)
168 ... except TypeError as te:
169 ... print(te)
170 op2 should be an instance of moptipy.api.operators.Op2 but is None.
171 """
172 if isinstance(op2, Op2):
173 return op2
174 raise type_error(op2, "op2", Op2)
177# start op1WithStepSize
178class Op1WithStepSize(Op1):
179 """A unary search operator with a step size."""
181 def op1(self, random: Generator, dest, x, step_size: float = 0.0) -> None:
182 """
183 Copy `x` to `dest` but apply a modification with a given `step_size`.
185 This operator is similar to :meth:`Op1.op1` in that it stores a
186 modified copy of `x` into `dest`. The difference is that you can also
187 specify how much that copy should be different: The parameter
188 `step_size` can take on any value in the interval `[0.0, 1.0]`,
189 including the two boundary values. A `step_size` of `0.0` indicates
190 the smallest possible move (for which `dest` will still be different
191 from `x`) and `step_size=1.0` will lead to the largest possible move.
193 The `step_size` may be interpreted differently by different operators:
194 Some may interpret it as an exact requirement and enforce steps of the
195 exact specified size, see, for example module
196 :mod:`~moptipy.operators.bitstrings.op1_flip_m`. Others might
197 interpret it stochastically as an expectation. Yet others may
198 interpret it as a goal step width and try to realize it in a best
199 effort kind of way, but may also do smaller or larger steps if the
200 best effort fails, see for example module
201 :mod:`~moptipy.operators.permutations.op1_swap_exactly_n`.
202 What all operators should, however, have in common is that at
203 `step_size=0.0`, they should try to perform a smallest possible change
204 and at `step_size=1.0`, they should try to perform a largest possible
205 change. For all values in between, step sizes should grow with rising
206 `step_size`. This should allow algorithms that know nothing about the
207 nature of the search space or the operator's moves to still tune
208 between small and large moves based on a policy which makes sense in a
209 black-box setting.
211 Every implementation of :class:`Op1WithStepSize` must specify a
212 reasonable default value for this parameter ensure compatibility with
213 :meth:`Op1.op1`. In this base class, we set the default to `0.0`.
215 Finally, if a `step_size` value is passed in which is outside the
216 interval `[0, 1]`, the behavior of this method is undefined. It may
217 throw an exception or not. It may also enter an infinite loop.
219 :param random: the random number generator
220 :param dest: the destination data structure
221 :param x: the source point in the search space
222 :param step_size: the step size parameter for the unary operator
223 """
224 raise ValueError("Method not implemented!")
225# end op1WithStepSize
228def check_op1_with_step_size(op1: Any) -> Op1WithStepSize:
229 """
230 Check whether an object is a valid instance of :class:`Op1WithStepSize`.
232 :param op1: the (supposed) instance of :class:`Op1WithStepSize`
233 :return: the object `op1`
234 :raises TypeError: if `op1` is not an instance of :class:`Op1WithStepSize`
236 >>> check_op1_with_step_size(Op1WithStepSize())
237 Op1WithStepSize
238 >>> try:
239 ... check_op1_with_step_size('A')
240 ... except TypeError as te:
241 ... print(te)
242 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
243but is str, namely 'A'.
244 >>> try:
245 ... check_op1_with_step_size(Op1())
246 ... except TypeError as te:
247 ... print(te)
248 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
249but is moptipy.api.operators.Op1.
250 >>> try:
251 ... check_op1_with_step_size(None)
252 ... except TypeError as te:
253 ... print(te)
254 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \
255but is None.
256 """
257 if isinstance(op1, Op1WithStepSize):
258 return op1
259 raise type_error(op1, "op1", Op1WithStepSize)