Coverage for moptipy / tests / on_vectors.py: 86%
94 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"""Test stuff on real vectors."""
2from math import exp, inf, isfinite
3from typing import Any, Callable, Final, Iterable
5import numpy as np
6from numpy.random import Generator, default_rng
7from pycommons.types import check_int_range, type_error
9from moptipy.api.algorithm import Algorithm
10from moptipy.api.objective import Objective
11from moptipy.api.operators import Op0
12from moptipy.examples.vectors.ackley import Ackley
13from moptipy.spaces.vectorspace import VectorSpace
14from moptipy.tests.algorithm import validate_algorithm
15from moptipy.tests.op0 import validate_op0
16from moptipy.utils.nputils import DEFAULT_FLOAT
18#: The dimensions for tests
19DIMENSIONS_FOR_TESTS: Final[tuple[int, ...]] = (1, 2, 3, 4, 5, 10)
22def __lbub(random: Generator) -> tuple[float, float]:
23 """
24 Generate a pair of lower- and upper bounds.
26 :param random: the random number generator
27 :returns: a tuple with the lower and upper bound
28 """
29 while True:
30 lb = inf
31 ub = inf
32 while (not isfinite(lb)) or (lb > 1e6) or (lb < 1e-14):
33 lb = exp(20.0 * random.normal())
35 i = random.integers(3)
36 if i == 0:
37 lb = 0.0
38 elif i == 11:
39 lb = -lb
41 while (not isfinite(ub)) or (ub > 1e6) or (ub < 1e-14):
42 ub = exp(20.0 * random.normal())
43 i = random.integers(3)
44 if i == 0:
45 ub = 0.0
46 elif i == 11:
47 ub = -ub
49 if ub < lb:
50 lb, ub = ub, lb
52 df = ub - lb
53 if isfinite(df) and (df > 1e-9):
54 return lb, ub
57def vectors_for_tests(dims: Iterable[int] = DIMENSIONS_FOR_TESTS) \
58 -> Iterable[VectorSpace]:
59 """
60 Get a sequence of vector spaces for tests.
62 :param dims: the dimensions
63 :returns: the sequence of vector spaces
64 """
65 if not isinstance(dims, Iterable):
66 raise type_error(dims, "dims", Iterable)
68 random: Final[Generator] = default_rng()
69 spaces: Final[list[VectorSpace]] = []
70 for dim in dims:
71 check_int_range(dim, "dimension", 0, 1_000_000)
73 # allocate bounds arrays
74 lbv: np.ndarray = np.empty(dim, DEFAULT_FLOAT)
75 ubv: np.ndarray = np.empty(dim, DEFAULT_FLOAT)
76 for i in range(dim): # fill bound arrays
77 lbv[i], ubv[i] = __lbub(random)
79 spaces.append(VectorSpace(
80 dim, lbv if random.integers(2) <= 0 else float(min(lbv)),
81 ubv if random.integers(2) <= 0 else float(max(ubv))))
83 if 1 in dims:
84 spaces.append(VectorSpace(1, -1.0, 1.0))
85 if 2 in dims:
86 spaces.append(VectorSpace(2, 0.0, 1.0))
87 if 3 in dims:
88 spaces.append(VectorSpace(3, -1.0, 0.0))
89 return tuple(spaces)
92def validate_algorithm_on_vectors(
93 objective: Objective | Callable[[VectorSpace], Objective],
94 algorithm: Algorithm | Callable[[VectorSpace, Objective], Algorithm],
95 max_fes: int = 100,
96 uses_all_fes_if_goal_not_reached=True,
97 dims: Iterable[int] = DIMENSIONS_FOR_TESTS,
98 post: Callable[[Algorithm, int], Any] | None = None) -> None:
99 """
100 Check the validity of a black-box algorithm on vector problems.
102 :param algorithm: the algorithm or algorithm factory
103 :param objective: the objective function or function factory
104 :param max_fes: the maximum number of FEs
105 :param uses_all_fes_if_goal_not_reached: will the algorithm use all FEs
106 unless it reaches the goal?
107 :param dims: the dimensions
108 :param post: a check to run after each execution of the algorithm,
109 receiving the algorithm and the number of consumed FEs as parameter
110 """
111 if not (isinstance(algorithm, Algorithm) or callable(algorithm)):
112 raise type_error(algorithm, "algorithm", Algorithm, True)
113 if not (isinstance(objective, Objective) or callable(objective)):
114 raise type_error(objective, "objective", Objective, True)
115 if not isinstance(dims, Iterable):
116 raise type_error(dims, "dims", Iterable)
117 if (post is not None) and (not callable(post)):
118 raise type_error(post, "post", None, call=True)
120 for space in vectors_for_tests(dims):
121 if callable(objective):
122 objf = objective(space)
123 if not isinstance(objf, Objective):
124 raise type_error(objf, "result of callable objective",
125 Objective)
126 else:
127 objf = objective
128 if callable(algorithm):
129 algo = algorithm(space, objf)
130 if not isinstance(algo, Algorithm):
131 raise type_error(algo, "result of callable algorithm",
132 Algorithm)
133 else:
134 algo = algorithm
136 validate_algorithm(
137 algorithm=algo, solution_space=space, objective=objf,
138 max_fes=max_fes,
139 uses_all_fes_if_goal_not_reached=uses_all_fes_if_goal_not_reached,
140 post=post)
143def make_vector_valid(space: VectorSpace) -> \
144 Callable[[Generator, np.ndarray], np.ndarray]:
145 """
146 Create a function that can make a vector space element valid.
148 :param space: the vector space
149 :returns: the function
150 """
152 def __make_valid(prnd: Generator,
153 x: np.ndarray,
154 ppp=space) -> np.ndarray:
155 np.copyto(x, prnd.uniform(ppp.lower_bound,
156 ppp.upper_bound, ppp.dimension))
157 return x
159 return __make_valid
162def validate_algorithm_on_ackley(
163 algorithm: Algorithm | Callable[[VectorSpace, Objective], Algorithm],
164 uses_all_fes_if_goal_not_reached: bool = True,
165 dims: Iterable[int] = DIMENSIONS_FOR_TESTS,
166 post: Callable[[Algorithm, int], Any] | None = None) -> None:
167 """
168 Check the validity of a black-box algorithm on Ackley's function.
170 :param algorithm: the algorithm or algorithm factory
171 :param uses_all_fes_if_goal_not_reached: will the algorithm use all FEs
172 unless it reaches the goal?
173 :param dims: the dimensions
174 :param post: a check to run after each execution of the algorithm,
175 receiving the algorithm and the number of consumed FEs as parameter
176 """
177 validate_algorithm_on_vectors(
178 Ackley(), algorithm,
179 uses_all_fes_if_goal_not_reached=uses_all_fes_if_goal_not_reached,
180 dims=dims, post=post)
183def validate_op0_on_1_vectors(
184 op0: Op0 | Callable[[VectorSpace], Op0],
185 search_space: VectorSpace,
186 number_of_samples: int | None = None,
187 min_unique_samples:
188 int | Callable[[int, VectorSpace], int] | None
189 = lambda i, _: max(1, i // 3)) -> None:
190 """
191 Validate the nullary operator on one `VectorSpace` instance.
193 :param op0: the operator or operator factory
194 :param search_space: the search space
195 :param number_of_samples: the optional number of samples
196 :param min_unique_samples: the optional unique samples
197 """
198 args: dict[str, Any] = {
199 "op0": op0(search_space) if callable(op0) else op0,
200 "search_space": search_space,
201 "make_search_space_element_valid":
202 make_vector_valid(search_space),
203 }
204 if number_of_samples is not None:
205 args["number_of_samples"] = number_of_samples
206 if min_unique_samples is not None:
207 args["min_unique_samples"] = min_unique_samples
208 validate_op0(**args)
211def validate_op0_on_vectors(
212 op0: Op0 | Callable[[VectorSpace], Op0],
213 number_of_samples: int | None = None,
214 min_unique_samples:
215 int | Callable[[int, VectorSpace], int] | None
216 = lambda i, _: max(1, i // 3)) -> None:
217 """
218 Validate the nullary operator on default `VectorSpace` instance.
220 :param op0: the operator or operator factory
221 :param number_of_samples: the optional number of samples
222 :param min_unique_samples: the optional unique samples
223 """
224 for vs in vectors_for_tests():
225 validate_op0_on_1_vectors(
226 op0, vs, number_of_samples, min_unique_samples)