1
- """A base class for bitstring-based problems."""
1
+ """
2
+ A base class for bitstring-based problems.
2
3
4
+ Many benchmark problems from discrete optimization are simple functions
5
+ defined over bit strings. We here offer the class :class:`BitStringProblem`,
6
+ which provides reasonable default behavior and several utilities for
7
+ implementing such problems.
8
+ """
9
+
10
+ from math import isqrt
3
11
from typing import Final
4
12
5
13
from pycommons .types import check_int_range
@@ -16,7 +24,26 @@ class BitStringProblem(Objective):
16
24
This base class has a set of default behaviors. It has an attribute
17
25
:attr:`n` denoting the lengths of the accepted bit strings. Its
18
26
:meth:`lower_bound` returns `0` and its :meth:`upper_bound` returns
19
- :attr:`n`. :meth:`is_always_integer` returns `True`.
27
+ :attr:`n`. :meth:`is_always_integer` returns `True`. If also offers
28
+ the method :meth:`space` which returns an instance of
29
+ :class:`~moptipy.spaces.bitstrings.BitStrings` for bit strings of
30
+ length :attr:`n`.
31
+
32
+ >>> bs = BitStringProblem(1)
33
+ >>> bs.n
34
+ 1
35
+
36
+ >>> try:
37
+ ... bs = BitStringProblem(0)
38
+ ... except ValueError as ve:
39
+ ... print(ve)
40
+ n=0 is invalid, must be in 1..1000000000.
41
+
42
+ >>> try:
43
+ ... bs = BitStringProblem("a")
44
+ ... except TypeError as te:
45
+ ... print(te)
46
+ n should be an instance of int but is str, namely 'a'.
20
47
"""
21
48
22
49
def __init__ (self , n : int ) -> None : # +book
@@ -33,6 +60,9 @@ def lower_bound(self) -> int:
33
60
"""
34
61
Get the lower bound of the bit string based problem.
35
62
63
+ By default, this method returns `0`. Problems where the lower bound
64
+ differs should override this method.
65
+
36
66
:return: 0
37
67
38
68
>>> print(BitStringProblem(10).lower_bound())
@@ -44,7 +74,10 @@ def upper_bound(self) -> int:
44
74
"""
45
75
Get the upper bound of the bit string based problem.
46
76
47
- :return: the length of the bit string
77
+ The default value is the length of the bit string. Problems where the
78
+ upper bound differs should overrrite this method.
79
+
80
+ :return: by default, this is the length of the bit string
48
81
49
82
>>> print(BitStringProblem(7).upper_bound())
50
83
7
@@ -56,7 +89,8 @@ def is_always_integer(self) -> bool:
56
89
Return `True` if the `evaluate` function always returns an `int`.
57
90
58
91
This pre-defined function for bit-string based problems will always
59
- return `True`.
92
+ return `True`. Problems where this is not the case should overwrite
93
+ this method.
60
94
61
95
:retval True: always
62
96
"""
@@ -84,7 +118,7 @@ def log_parameters_to(self, logger: KeyValueLogSection) -> None:
84
118
... BitStringProblem(5).log_parameters_to(kv)
85
119
... text = l.get_log()
86
120
>>> text[1]
87
- 'name: BitStringProblem '
121
+ 'name: bitstringproblem_5 '
88
122
>>> text[3]
89
123
'lowerBound: 0'
90
124
>>> text[4]
@@ -96,3 +130,181 @@ def log_parameters_to(self, logger: KeyValueLogSection) -> None:
96
130
"""
97
131
super ().log_parameters_to (logger )
98
132
logger .key_value ("n" , self .n )
133
+
134
+ def __str__ (self ) -> str :
135
+ """
136
+ Get the name of the problem.
137
+
138
+ :returns: the name of the problem, which by default is the class name
139
+ in lower case, followed by an underscore and the number of bits
140
+ """
141
+ return f"{ super ().__str__ ().lower ()} _{ self .n } "
142
+
143
+
144
+ class SquareBitStringProblem (BitStringProblem ):
145
+ """
146
+ A bitstring problem which requires that the string length is square.
147
+
148
+ >>> sb = SquareBitStringProblem(9)
149
+ >>> sb.n
150
+ 9
151
+ >>> sb.k
152
+ 3
153
+
154
+ >>> try:
155
+ ... bs = SquareBitStringProblem(0)
156
+ ... except ValueError as ve:
157
+ ... print(ve)
158
+ n=0 is invalid, must be in 4..1000000000.
159
+
160
+ >>> try:
161
+ ... bs = SquareBitStringProblem(3)
162
+ ... except ValueError as ve:
163
+ ... print(ve)
164
+ n=3 is invalid, must be in 4..1000000000.
165
+
166
+ >>> try:
167
+ ... bs = SquareBitStringProblem(21)
168
+ ... except ValueError as ve:
169
+ ... print(ve)
170
+ n=21 must be a square number, but isqrt(n)=4 does not satisfy n = k*k.
171
+
172
+ >>> try:
173
+ ... bs = SquareBitStringProblem("a")
174
+ ... except TypeError as te:
175
+ ... print(te)
176
+ n should be an instance of int but is str, namely 'a'.
177
+ """
178
+
179
+ def __init__ (self , n : int ) -> None :
180
+ """
181
+ Initialize the square bitstring problem.
182
+
183
+ :param n: the dimension of the problem (must be a perfect square)
184
+ """
185
+ super ().__init__ (check_int_range (n , "n" , 4 ))
186
+ k : Final [int ] = check_int_range (isqrt (n ), "k" , 2 )
187
+ if (k * k ) != n :
188
+ raise ValueError (f"n={ n } must be a square number, but"
189
+ f" isqrt(n)={ k } does not satisfy n = k*k." )
190
+ #: the k value, i.e., the number of bits per row and column of
191
+ #: the square
192
+ self .k : Final [int ] = k
193
+
194
+ def log_parameters_to (self , logger : KeyValueLogSection ) -> None :
195
+ """
196
+ Log all parameters of this component as key-value pairs.
197
+
198
+ :param logger: the logger for the parameters
199
+
200
+ >>> from moptipy.utils.logger import InMemoryLogger
201
+ >>> with InMemoryLogger() as l:
202
+ ... with l.key_values("C") as kv:
203
+ ... SquareBitStringProblem(49).log_parameters_to(kv)
204
+ ... text = l.get_log()
205
+ >>> text[1]
206
+ 'name: squarebitstringproblem_49'
207
+ >>> text[3]
208
+ 'lowerBound: 0'
209
+ >>> text[4]
210
+ 'upperBound: 49'
211
+ >>> text[5]
212
+ 'n: 49'
213
+ >>> text[6]
214
+ 'k: 7'
215
+ >>> len(text)
216
+ 8
217
+ """
218
+ super ().log_parameters_to (logger )
219
+ logger .key_value ("k" , self .k )
220
+
221
+
222
+ class BitStringNKProblem (BitStringProblem ):
223
+ """
224
+ A bit string problem with a second parameter `k` with `1 < k < n/2`.
225
+
226
+ >>> sb = BitStringNKProblem(9, 3)
227
+ >>> sb.n
228
+ 9
229
+ >>> sb.k
230
+ 3
231
+
232
+ >>> try:
233
+ ... bs = BitStringNKProblem(0, 3)
234
+ ... except ValueError as ve:
235
+ ... print(ve)
236
+ n=0 is invalid, must be in 6..1000000000.
237
+
238
+ >>> try:
239
+ ... bs = BitStringNKProblem(5, 2)
240
+ ... except ValueError as ve:
241
+ ... print(ve)
242
+ n=5 is invalid, must be in 6..1000000000.
243
+
244
+ >>> try:
245
+ ... bs = BitStringNKProblem(21, 20)
246
+ ... except ValueError as ve:
247
+ ... print(ve)
248
+ k=20 is invalid, must be in 2..9.
249
+
250
+ >>> try:
251
+ ... bs = BitStringNKProblem("a", 3)
252
+ ... except TypeError as te:
253
+ ... print(te)
254
+ n should be an instance of int but is str, namely 'a'.
255
+
256
+ >>> try:
257
+ ... bs = BitStringNKProblem(13, "x")
258
+ ... except TypeError as te:
259
+ ... print(te)
260
+ k should be an instance of int but is str, namely 'x'.
261
+ """
262
+
263
+ def __init__ (self , n : int , k : int ) -> None : # +book
264
+ """
265
+ Initialize the n-k objective function.
266
+
267
+ :param n: the dimension of the problem
268
+ :param k: the second parameter
269
+ """
270
+ super ().__init__ (check_int_range (n , "n" , 6 ))
271
+ #: the second parameter, with `1 < k < n/2`
272
+ self .k : Final [int ] = check_int_range (k , "k" , 2 , (n >> 1 ) - 1 )
273
+
274
+ def __str__ (self ) -> str :
275
+ """
276
+ Get the name of the objective function.
277
+
278
+ :return: `class_` + length of string + `_` + k
279
+
280
+ >>> BitStringNKProblem(13, 4)
281
+ bitstringnkproblem_13_4
282
+ """
283
+ return f"{ super ().__str__ ()} _{ self .k } "
284
+
285
+ def log_parameters_to (self , logger : KeyValueLogSection ) -> None :
286
+ """
287
+ Log all parameters of this component as key-value pairs.
288
+
289
+ :param logger: the logger for the parameters
290
+
291
+ >>> from moptipy.utils.logger import InMemoryLogger
292
+ >>> with InMemoryLogger() as l:
293
+ ... with l.key_values("C") as kv:
294
+ ... BitStringNKProblem(23, 7).log_parameters_to(kv)
295
+ ... text = l.get_log()
296
+ >>> text[1]
297
+ 'name: bitstringnkproblem_23_7'
298
+ >>> text[3]
299
+ 'lowerBound: 0'
300
+ >>> text[4]
301
+ 'upperBound: 23'
302
+ >>> text[5]
303
+ 'n: 23'
304
+ >>> text[6]
305
+ 'k: 7'
306
+ >>> len(text)
307
+ 8
308
+ """
309
+ super ().log_parameters_to (logger )
310
+ logger .key_value ("k" , self .k )
0 commit comments