Skip to content

Commit 715a921

Browse files
committed
better tests and minor bug fixes
1 parent d4d82b6 commit 715a921

23 files changed

+714
-62
lines changed

moptipy/algorithms/mo/nsga2.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from moptipy.api.mo_process import MOProcess
4242
from moptipy.api.operators import Op0, Op1, Op2
4343
from moptipy.utils.logger import KeyValueLogSection
44-
from moptipy.utils.strings import num_to_str
44+
from moptipy.utils.strings import num_to_str_for_name
4545
from moptipy.utils.types import type_error
4646

4747

@@ -201,7 +201,8 @@ def __init__(self, op0: Op0, op1: Op1, op2: Op2,
201201
:param pop_size: the population size
202202
:param cr: the crossover rate
203203
"""
204-
super().__init__(f"nsga2_{pop_size}_{num_to_str(cr)}", op0, op1, op2)
204+
super().__init__(
205+
f"nsga2_{pop_size}_{num_to_str_for_name(cr)}", op0, op1, op2)
205206
if not isinstance(pop_size, int):
206207
raise type_error(pop_size, "pop_size", int)
207208
if pop_size < 3:

moptipy/algorithms/so/ea.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from moptipy.api.operators import Op0, Op1, Op2
3535
from moptipy.api.process import Process
3636
from moptipy.utils.logger import KeyValueLogSection
37-
from moptipy.utils.strings import float_to_str
37+
from moptipy.utils.strings import num_to_str_for_name
3838
from moptipy.utils.types import type_error
3939

4040

@@ -265,7 +265,7 @@ def __init__(self, op0: Op0,
265265
br = 0.2
266266

267267
super().__init__(
268-
f"ea_{mu}_{lambda_}_{float_to_str(br).replace('.', 'd')}"
268+
f"ea_{mu}_{lambda_}_{num_to_str_for_name(br)}"
269269
if 0 < br < 1 else f"ea_{mu}_{lambda_}", op0, op1, op2)
270270

271271
if not isinstance(mu, int):

moptipy/api/_mo_process_no_ss.py

+5
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ def register(self, x, f: Union[int, float]) -> None:
197197
def get_archive(self) -> List[MORecord]:
198198
return self._archive[0:self._archive_size]
199199

200+
def get_copy_of_best_fs(self, fs: np.ndarray) -> None:
201+
if self._current_fes > 0:
202+
return copyto(fs, self._current_best_fs)
203+
raise ValueError('No current best available.')
204+
200205
def _log_own_parameters(self, logger: KeyValueLogSection) -> None:
201206
super()._log_own_parameters(logger)
202207
logger.key_value(KEY_ARCHIVE_MAX_SIZE, self._archive_max_size)

moptipy/api/mo_process.py

+24
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,30 @@ def check_in(self, x: Any, fs: np.ndarray,
6464
entered the archive, `False` if it has not entered the archive
6565
"""
6666

67+
def get_copy_of_best_fs(self, fs: np.ndarray) -> None:
68+
"""
69+
Get a copy of the objective vector of the current best solution.
70+
71+
This always corresponds to the best-so-far solution based on the
72+
scalarization of the objective vector. It is the best solution that
73+
the process has seen *so far*, the current best solution.
74+
75+
You should only call this method if you are either sure that you
76+
have invoked :meth:`~moptipy.api.process.Process.evaluate`, have
77+
invoked :meth:`~moptipy.api.mo_problem.MOProblem.f_evaluate`, or
78+
have called :meth:`~moptipy.api.process.Process.has_best` before
79+
and it returned `True`.
80+
81+
:param fs: the destination vector to be overwritten
82+
83+
See also:
84+
- :meth:`~moptipy.api.process.Process.has_best`
85+
- :meth:`~moptipy.api.process.Process.get_best_f`
86+
- :meth:`~moptipy.api.process.Process.get_copy_of_best_x`
87+
- :meth:`~moptipy.api.process.Process.get_copy_of_best_y`
88+
- :meth:`~moptipy.api.mo_problem.MOProblem.f_evaluate`
89+
"""
90+
6791
def __str__(self) -> str:
6892
"""
6993
Get the name of this process implementation.

moptipy/api/process.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def get_copy_of_best_x(self, x) -> None: # +book
231231
the process has seen *so far*, the current best solution.
232232
233233
You should only call this method if you are either sure that you
234-
have invoked meth:`evaluate` before :meth:`register` of if you called
234+
have invoked :meth:`evaluate` before :meth:`register` of if you called
235235
:meth:`has_best` before and it returned `True`.
236236
237237
:param x: the destination data structure to be overwritten

moptipy/examples/bitstrings/onemax.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ def onemax(x: np.ndarray) -> int:
1414
Get the length of a string minus the number of ones in it.
1515
1616
:param x: the np array
17-
:return: the number of ones
17+
:return: the length of the string minus the number of ones, i.e., the
18+
number of zeros
1819
1920
>>> print(onemax(np.array([True, True, False, False, False])))
2021
3
+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
An objective function counting the number of zeros in a bit string.
3+
4+
This problem exists mainly for testing purposes as counterpart of
5+
:class:`~moptipy.examples.bitstrings.onemax.OneMax`.
6+
"""
7+
from typing import Final
8+
9+
import numba # type: ignore
10+
import numpy as np
11+
12+
from moptipy.api.objective import Objective
13+
from moptipy.utils.types import type_error
14+
15+
16+
@numba.njit(nogil=True, cache=True)
17+
def zeromax(x: np.ndarray) -> int:
18+
"""
19+
Get the length of a string minus the number of zeros in it.
20+
21+
:param x: the np array
22+
:return: the length of the string minus the number of zeros, i.e., the
23+
number of ones
24+
25+
>>> print(zeromax(np.array([True, True, False, False, False])))
26+
2
27+
>>> print(zeromax(np.array([True, False, True, False, False])))
28+
2
29+
>>> print(zeromax(np.array([False, True, True, False, False])))
30+
2
31+
>>> print(zeromax(np.array([True, True, True, True, True])))
32+
5
33+
>>> print(zeromax(np.array([False, True, True, True, True])))
34+
4
35+
>>> print(zeromax(np.array([False, False, False, False, False])))
36+
0
37+
"""
38+
return int(x.sum())
39+
40+
41+
class ZeroMax(Objective):
42+
"""Maximize the number of zeros in a bit string."""
43+
44+
def __init__(self, n: int) -> None:
45+
"""
46+
Initialize the zeromax objective function.
47+
48+
:param n: the dimension of the problem
49+
50+
>>> print(ZeroMax(2).n)
51+
2
52+
>>> print(ZeroMax(4).evaluate(np.array([True, True, False, True])))
53+
3
54+
"""
55+
super().__init__()
56+
if not isinstance(n, int):
57+
raise type_error(n, "n", int)
58+
#: the upper bound = the length of the bit strings
59+
self.n: Final[int] = n
60+
self.evaluate = zeromax # type: ignore
61+
62+
def lower_bound(self) -> int:
63+
"""
64+
Get the lower bound of the zeromax objective function.
65+
66+
:return: 0
67+
68+
>>> print(ZeroMax(10).lower_bound())
69+
0
70+
"""
71+
return 0
72+
73+
def upper_bound(self) -> int:
74+
"""
75+
Get the upper bound of the zeromax objective function.
76+
77+
:return: the length of the bit string
78+
79+
>>> print(ZeroMax(7).upper_bound())
80+
7
81+
"""
82+
return self.n
83+
84+
def is_always_integer(self) -> bool:
85+
"""
86+
Return `True` because :func:`zeromax` always returns `int` values.
87+
88+
:retval True: always
89+
"""
90+
return True
91+
92+
def __str__(self) -> str:
93+
"""
94+
Get the name of the zeromax objective function.
95+
96+
:return: `zeromax_` + length of string
97+
98+
>>> print(ZeroMax(13))
99+
zeromax_13
100+
"""
101+
return f"zeromax_{self.n}"

moptipy/mo/problem/basic_mo_problem.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ def __init__(self, objectives: Iterable[Objective],
5151
get_scalarizer: Callable[
5252
[bool, int, List[Union[int, float]],
5353
List[Union[int, float]]],
54-
Callable[[np.ndarray], Union[int, float]]] = None) \
55-
-> None:
54+
Callable[[np.ndarray], Union[int, float]]] = None,
55+
domination: Optional[Callable[[np.ndarray, np.ndarray], int]]
56+
= dominates) -> None:
5657
"""
5758
Create the basic multi-objective optimization problem.
5859
@@ -65,6 +66,11 @@ def __init__(self, objectives: Iterable[Objective],
6566
with the lower and upper bounds of the objective functions. It can
6667
use this information to dynamically create and return the most
6768
efficient scalarization function.
69+
:param domination: a function reflecting the domination relationship
70+
between two vectors of objective values. It must obey the contract
71+
of :meth:`~moptipy.api.mo_problem.MOProblem.f_dominates`, which is
72+
the same as :func:`moptipy.api.mo_utils.dominates`, to which it
73+
defaults. `None` overrides nothing.
6874
"""
6975
if not isinstance(objectives, Iterable):
7076
raise type_error(objectives, "objectives", Iterable)
@@ -209,7 +215,11 @@ def __init__(self, objectives: Iterable[Objective],
209215
#: the internal temporary array
210216
self._temp: Final[np.ndarray] = self.f_create() \
211217
if temp is None else temp
212-
self.f_dominates = dominates # type: ignore
218+
219+
if domination is not None:
220+
if not callable(domination):
221+
raise type_error(domination, "domination", call=True)
222+
self.f_dominates = domination # type: ignore
213223

214224
def f_dimension(self) -> int:
215225
"""

moptipy/mo/problem/weighted_sum.py

+34-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import numpy as np
1919
from numpy import sum as npsum
20+
from moptipy.api.mo_utils import dominates
2021

2122
from moptipy.api.objective import Objective
2223
from moptipy.mo.problem.basic_mo_problem import BasicMOProblem
@@ -64,11 +65,18 @@ def __init__(self, objectives: Iterable[Objective],
6465
List[Union[int, float]], Callable[
6566
[Union[None, np.dtype, Tuple[
6667
Union[int, float], ...]]], None]],
67-
Callable[[np.ndarray], Union[int, float]]]) -> None:
68+
Callable[[np.ndarray], Union[int, float]]],
69+
domination: Optional[Callable[[np.ndarray, np.ndarray], int]]
70+
= dominates) -> None:
6871
"""
6972
Create the sum-based scalarization.
7073
7174
:param objectives: the objectives
75+
:param domination: a function reflecting the domination relationship
76+
between two vectors of objective values. It must obey the contract
77+
of :meth:`~moptipy.api.mo_problem.MOProblem.f_dominates`, which is
78+
the same as :func:`moptipy.api.mo_utils.dominates`, to which it
79+
defaults. `None` overrides nothing.
7280
"""
7381
holder: List[Any] = []
7482
super().__init__(
@@ -77,7 +85,8 @@ def __init__(self, objectives: Iterable[Objective],
7785
List[Union[int, float]]], Callable[
7886
[np.ndarray], Union[int, float]]],
7987
lambda ai, n, lb, ub, fwd=holder.append:
80-
get_scalarizer(ai, n, lb, ub, fwd)))
88+
get_scalarizer(ai, n, lb, ub, fwd)),
89+
domination)
8190
if len(holder) != 2:
8291
raise ValueError(
8392
f"need weights and weights dtype, but got {holder}.")
@@ -242,14 +251,20 @@ class WeightedSum(BasicWeightedSum):
242251
"""Scalarize objective values by computing their weighted sum."""
243252

244253
def __init__(self, objectives: Iterable[Objective],
245-
weights: Optional[Iterable[Union[int, float]]] = None) \
246-
-> None:
254+
weights: Optional[Iterable[Union[int, float]]] = None,
255+
domination: Optional[Callable[[np.ndarray, np.ndarray], int]]
256+
= dominates) -> None:
247257
"""
248258
Create the sum-based scalarization.
249259
250260
:param objectives: the objectives
251261
:param weights: the weights of the objective values, or `None` if all
252262
weights are `1`.
263+
:param domination: a function reflecting the domination relationship
264+
between two vectors of objective values. It must obey the contract
265+
of :meth:`~moptipy.api.mo_problem.MOProblem.f_dominates`, which is
266+
the same as :func:`moptipy.api.mo_utils.dominates`, to which it
267+
defaults. `None` overrides nothing.
253268
"""
254269
use_weights: Optional[Tuple[Union[int, float], ...]] \
255270
= None if weights is None else tuple(try_int(w) for w in weights)
@@ -263,15 +278,17 @@ def __init__(self, objectives: Iterable[Objective],
263278
Union[int, float], ...]]], None]],
264279
Callable[[np.ndarray], Union[int, float]]],
265280
lambda ai, n, lb, ub, cb, uw=use_weights:
266-
_make_sum_scalarizer(ai, n, lb, ub, uw, cb)))
281+
_make_sum_scalarizer(ai, n, lb, ub, uw, cb)),
282+
domination)
267283

268284
def __str__(self):
269285
"""
270286
Get the string representation of the weighted sum scalarization.
271287
272288
:returns: `"weightedSum"`
273289
"""
274-
return "weightedSum"
290+
return "weightedSum" if self.f_dominates is dominates \
291+
else "weightedSumWithDominationFunc"
275292

276293

277294
def _prioritize(always_int: bool,
@@ -332,18 +349,26 @@ def _prioritize(always_int: bool,
332349
class Prioritize(BasicWeightedSum):
333350
"""Prioritize the first objective over the second and so on."""
334351

335-
def __init__(self, objectives: Iterable[Objective]) -> None:
352+
def __init__(self, objectives: Iterable[Objective],
353+
domination: Optional[Callable[[np.ndarray, np.ndarray], int]]
354+
= dominates) -> None:
336355
"""
337356
Create the sum-based prioritization.
338357
339358
:param objectives: the objectives
359+
:param domination: a function reflecting the domination relationship
360+
between two vectors of objective values. It must obey the contract
361+
of :meth:`~moptipy.api.mo_problem.MOProblem.f_dominates`, which is
362+
the same as :func:`moptipy.api.mo_utils.dominates`, to which it
363+
defaults. `None` overrides nothing.
340364
"""
341-
super().__init__(objectives, _prioritize)
365+
super().__init__(objectives, _prioritize, domination)
342366

343367
def __str__(self):
344368
"""
345369
Get the name of the weighted sum-based prioritization.
346370
347371
:returns: `"weightBasedPrioritization"`
348372
"""
349-
return "weightBasedPrioritization"
373+
return "weightBasedPrioritization" if self.f_dominates is dominates \
374+
else "weightBasedPrioritizationWithDominationFunc"

moptipy/tests/algorithm.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818

1919
def validate_algorithm(algorithm: Algorithm,
20-
solution_space: Optional[Space] = None,
21-
objective: Optional[Objective] = None,
20+
solution_space: Space,
21+
objective: Objective,
2222
search_space: Optional[Space] = None,
2323
encoding: Optional[Encoding] = None,
2424
max_fes: int = 100,

0 commit comments

Comments
 (0)