Coverage for moptipy / api / mo_execution.py: 92%

88 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""The multi-objective algorithm execution API.""" 

2 

3from math import isfinite 

4from typing import Final, Self, cast 

5 

6from pycommons.types import check_int_range 

7 

8from moptipy.api._mo_process_no_ss import _MOProcessNoSS 

9from moptipy.api._mo_process_no_ss_log import _MOProcessNoSSLog 

10from moptipy.api._mo_process_ss import _MOProcessSS 

11from moptipy.api._mo_process_ss_log import _MOProcessSSLog 

12from moptipy.api.algorithm import Algorithm, check_algorithm 

13from moptipy.api.encoding import Encoding, check_encoding 

14from moptipy.api.execution import Execution 

15from moptipy.api.mo_archive import MOArchivePruner, check_mo_archive_pruner 

16from moptipy.api.mo_problem import ( 

17 MOProblem, 

18 MOSOProblemBridge, 

19 check_mo_problem, 

20) 

21from moptipy.api.mo_process import MOProcess 

22from moptipy.api.objective import Objective, check_objective 

23from moptipy.api.process import ( 

24 check_goal_f, 

25 check_max_fes, 

26 check_max_time_millis, 

27) 

28from moptipy.api.space import Space, check_space 

29from moptipy.mo.archive.keep_farthest import KeepFarthest 

30from moptipy.utils.nputils import rand_seed_check 

31 

32 

33class MOExecution(Execution): 

34 """ 

35 Define all the components of a multi-objective experiment and execute it. 

36 

37 Different from :class:`~moptipy.api.execution.Execution`, this class here 

38 allows us to construct multi-objective optimization processes, i.e., such 

39 that have more than one optimization goal. 

40 """ 

41 

42 def __init__(self) -> None: 

43 """Create the multi-objective execution.""" 

44 super().__init__() 

45 #: the maximum size of a pruned archive 

46 self._archive_max_size: int | None = None 

47 #: the archive size limit at which pruning should be performed 

48 self._archive_prune_limit: int | None = None 

49 #: the archive pruning strategy 

50 self._archive_pruner: MOArchivePruner | None = None 

51 

52 def set_archive_max_size(self, size: int) -> Self: 

53 """ 

54 Set the upper limit for the archive size (after pruning). 

55 

56 The internal archive of the multi-objective optimization process 

57 retains non-dominated solutions encountered during the search. Since 

58 there can be infinitely many such solutions, the archive could grow 

59 without bound if left untouched. 

60 Therefore, we define two size limits: the maximum archive size 

61 (defined by this method) and the pruning limit. Once the archive grows 

62 beyond the pruning limit, it is cut down to the archive size limit. 

63 

64 :param size: the maximum archive size 

65 :returns: this execution 

66 """ 

67 check_int_range(size, "maximum archive size") 

68 if (self._archive_prune_limit is not None) and \ 

69 (size > self._archive_prune_limit): 

70 raise ValueError( 

71 f"archive max size {size} must be <= than archive " 

72 f"prune limit {self._archive_prune_limit}") 

73 self._archive_max_size = size 

74 return self 

75 

76 def set_archive_pruning_limit(self, 

77 limit: int) -> Self: 

78 """ 

79 Set the size limit of the archive above which pruning is performed. 

80 

81 If the size of the archive grows above this limit, the archive will be 

82 pruned down to the archive size limit. 

83 

84 :param limit: the archive pruning limit 

85 :returns: this execution 

86 """ 

87 check_int_range(limit, "limit", 1) 

88 if (self._archive_max_size is not None) and \ 

89 (limit < self._archive_max_size): 

90 raise ValueError( 

91 f"archive pruning limit {limit} must be >= than archive " 

92 f"maximum size {self._archive_max_size}") 

93 self._archive_prune_limit = limit 

94 return self 

95 

96 def set_archive_pruner(self, 

97 pruner: MOArchivePruner) -> Self: 

98 """ 

99 Set the pruning strategy for downsizing the archive. 

100 

101 :param pruner: the archive pruner 

102 :returns: this execution 

103 """ 

104 self._archive_pruner = check_mo_archive_pruner(pruner) 

105 return self 

106 

107 def set_objective(self, objective: Objective) -> Self: 

108 """ 

109 Set the objective function in form of a multi-objective problem. 

110 

111 :param objective: the objective function 

112 :returns: this execution 

113 """ 

114 check_objective(objective) 

115 if not isinstance(objective, MOProblem): 

116 objective = MOSOProblemBridge(objective) 

117 super().set_objective(check_mo_problem(objective)) 

118 return self 

119 

120 def execute(self) -> MOProcess: 

121 """ 

122 Create a multi-objective process, apply algorithm, and return result. 

123 

124 This method is multi-objective equivalent of the 

125 :meth:`~moptipy.api.execution.Execution.execute` method. It returns a 

126 multi-objective process after applying the multi-objective algorithm. 

127 

128 :returns: the instance of :class:`~moptipy.api.mo_process.MOProcess` 

129 after applying the algorithm. 

130 """ 

131 objective: Final[MOProblem] = cast("MOProblem", self._objective) 

132 solution_space: Final[Space] = check_space(self._solution_space) 

133 search_space: Final[Space | None] = check_space( 

134 self._search_space, self._encoding is None) 

135 encoding: Final[Encoding | None] = check_encoding( 

136 self._encoding, search_space is None) 

137 rand_seed = self._rand_seed 

138 if rand_seed is not None: 

139 rand_seed = rand_seed_check(rand_seed) 

140 max_time_millis = check_max_time_millis(self._max_time_millis, True) 

141 max_fes = check_max_fes(self._max_fes, True) 

142 goal_f = check_goal_f(self._goal_f, True) 

143 f_lb = objective.lower_bound() 

144 if (f_lb is not None) and isfinite(f_lb) and \ 

145 ((goal_f is None) or (f_lb > goal_f)): 

146 goal_f = f_lb 

147 

148 log_all_fes = self._log_all_fes 

149 log_improvements = self._log_improvements or self._log_all_fes 

150 

151 log_file = self._log_file 

152 if log_file is None: 

153 if log_all_fes: 

154 raise ValueError("Log file cannot be None " 

155 "if all FEs should be logged.") 

156 if log_improvements: 

157 raise ValueError("Log file cannot be None " 

158 "if improvements should be logged.") 

159 else: 

160 log_file.create_file_or_truncate() 

161 

162 pruner: Final[MOArchivePruner] = \ 

163 self._archive_pruner if self._archive_pruner is not None \ 

164 else KeepFarthest(objective) 

165 dim: Final[int] = objective.f_dimension() 

166 size: Final[int] = self._archive_max_size if \ 

167 self._archive_max_size is not None else ( 

168 self._archive_prune_limit if 

169 self._archive_prune_limit is not None 

170 else (1 if dim == 1 else 32)) 

171 limit: Final[int] = self._archive_prune_limit if \ 

172 self._archive_prune_limit is not None \ 

173 else (1 if dim == 1 else (size * 4)) 

174 algorithm: Final[Algorithm] = check_algorithm(self._algorithm) 

175 

176 process: Final[_MOProcessNoSS] = (_MOProcessNoSSLog( 

177 solution_space, objective, algorithm, pruner, size, limit, 

178 log_file, rand_seed, max_fes, max_time_millis, goal_f, 

179 log_all_fes) if log_improvements or log_all_fes else 

180 _MOProcessNoSS(solution_space, objective, algorithm, pruner, 

181 size, limit, log_file, rand_seed, max_fes, 

182 max_time_millis, goal_f)) \ 

183 if search_space is None else (_MOProcessSSLog( 

184 solution_space, objective, algorithm, pruner, size, limit, 

185 log_file, search_space, encoding, rand_seed, max_fes, 

186 max_time_millis, goal_f, 

187 log_all_fes) if log_improvements or log_all_fes else 

188 _MOProcessSS(solution_space, objective, algorithm, pruner, size, 

189 limit, log_file, search_space, encoding, rand_seed, 

190 max_fes, max_time_millis, goal_f)) 

191 

192 try: 

193 # noinspection PyProtectedMember 

194 process._after_init() # finalize the created process 

195 pruner.initialize() # initialize the pruner 

196 objective.initialize() # initialize the multi-objective problem 

197 if encoding is not None: 

198 encoding.initialize() # initialize the encoding 

199 solution_space.initialize() # initialize the solution space 

200 if search_space is not None: 

201 search_space.initialize() # initialize the search space 

202 algorithm.initialize() # initialize the algorithm 

203 algorithm.solve(process) # apply the algorithm 

204 except Exception as be: # noqa: BLE001 

205 # noinspection PyProtectedMember 

206 process._caught = be 

207 return process