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

1"""Test stuff on real vectors.""" 

2from math import exp, inf, isfinite 

3from typing import Any, Callable, Final, Iterable 

4 

5import numpy as np 

6from numpy.random import Generator, default_rng 

7from pycommons.types import check_int_range, type_error 

8 

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 

17 

18#: The dimensions for tests 

19DIMENSIONS_FOR_TESTS: Final[tuple[int, ...]] = (1, 2, 3, 4, 5, 10) 

20 

21 

22def __lbub(random: Generator) -> tuple[float, float]: 

23 """ 

24 Generate a pair of lower- and upper bounds. 

25 

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()) 

34 

35 i = random.integers(3) 

36 if i == 0: 

37 lb = 0.0 

38 elif i == 11: 

39 lb = -lb 

40 

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 

48 

49 if ub < lb: 

50 lb, ub = ub, lb 

51 

52 df = ub - lb 

53 if isfinite(df) and (df > 1e-9): 

54 return lb, ub 

55 

56 

57def vectors_for_tests(dims: Iterable[int] = DIMENSIONS_FOR_TESTS) \ 

58 -> Iterable[VectorSpace]: 

59 """ 

60 Get a sequence of vector spaces for tests. 

61 

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) 

67 

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) 

72 

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) 

78 

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)))) 

82 

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) 

90 

91 

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. 

101 

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) 

119 

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 

135 

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) 

141 

142 

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. 

147 

148 :param space: the vector space 

149 :returns: the function 

150 """ 

151 

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 

158 

159 return __make_valid 

160 

161 

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. 

169 

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) 

181 

182 

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. 

192 

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) 

209 

210 

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. 

219 

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)