desdeo 1.2__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/adm/ADMAfsar.py +551 -0
  3. desdeo/adm/ADMChen.py +414 -0
  4. desdeo/adm/BaseADM.py +119 -0
  5. desdeo/adm/__init__.py +11 -0
  6. desdeo/api/README.md +73 -0
  7. desdeo/api/__init__.py +15 -0
  8. desdeo/api/app.py +50 -0
  9. desdeo/api/config.py +90 -0
  10. desdeo/api/config.toml +64 -0
  11. desdeo/api/db.py +27 -0
  12. desdeo/api/db_init.py +85 -0
  13. desdeo/api/db_models.py +164 -0
  14. desdeo/api/malaga_db_init.py +27 -0
  15. desdeo/api/models/__init__.py +266 -0
  16. desdeo/api/models/archive.py +23 -0
  17. desdeo/api/models/emo.py +128 -0
  18. desdeo/api/models/enautilus.py +69 -0
  19. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  20. desdeo/api/models/gdm/gdm_base.py +69 -0
  21. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  22. desdeo/api/models/gdm/gnimbus.py +138 -0
  23. desdeo/api/models/generic.py +104 -0
  24. desdeo/api/models/generic_states.py +401 -0
  25. desdeo/api/models/nimbus.py +158 -0
  26. desdeo/api/models/preference.py +128 -0
  27. desdeo/api/models/problem.py +717 -0
  28. desdeo/api/models/reference_point_method.py +18 -0
  29. desdeo/api/models/session.py +49 -0
  30. desdeo/api/models/state.py +463 -0
  31. desdeo/api/models/user.py +52 -0
  32. desdeo/api/models/utopia.py +25 -0
  33. desdeo/api/routers/_EMO.backup +309 -0
  34. desdeo/api/routers/_NAUTILUS.py +245 -0
  35. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  36. desdeo/api/routers/_NIMBUS.py +765 -0
  37. desdeo/api/routers/__init__.py +5 -0
  38. desdeo/api/routers/emo.py +497 -0
  39. desdeo/api/routers/enautilus.py +237 -0
  40. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  41. desdeo/api/routers/gdm/gdm_base.py +420 -0
  42. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  43. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  44. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  45. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  46. desdeo/api/routers/generic.py +233 -0
  47. desdeo/api/routers/nimbus.py +705 -0
  48. desdeo/api/routers/problem.py +307 -0
  49. desdeo/api/routers/reference_point_method.py +93 -0
  50. desdeo/api/routers/session.py +100 -0
  51. desdeo/api/routers/test.py +16 -0
  52. desdeo/api/routers/user_authentication.py +520 -0
  53. desdeo/api/routers/utils.py +187 -0
  54. desdeo/api/routers/utopia.py +230 -0
  55. desdeo/api/schema.py +100 -0
  56. desdeo/api/tests/__init__.py +0 -0
  57. desdeo/api/tests/conftest.py +151 -0
  58. desdeo/api/tests/test_enautilus.py +330 -0
  59. desdeo/api/tests/test_models.py +1179 -0
  60. desdeo/api/tests/test_routes.py +1075 -0
  61. desdeo/api/utils/_database.py +263 -0
  62. desdeo/api/utils/_logger.py +29 -0
  63. desdeo/api/utils/database.py +36 -0
  64. desdeo/api/utils/emo_database.py +40 -0
  65. desdeo/core.py +34 -0
  66. desdeo/emo/__init__.py +159 -0
  67. desdeo/emo/hooks/archivers.py +188 -0
  68. desdeo/emo/methods/EAs.py +541 -0
  69. desdeo/emo/methods/__init__.py +0 -0
  70. desdeo/emo/methods/bases.py +12 -0
  71. desdeo/emo/methods/templates.py +111 -0
  72. desdeo/emo/operators/__init__.py +1 -0
  73. desdeo/emo/operators/crossover.py +1282 -0
  74. desdeo/emo/operators/evaluator.py +114 -0
  75. desdeo/emo/operators/generator.py +459 -0
  76. desdeo/emo/operators/mutation.py +1224 -0
  77. desdeo/emo/operators/scalar_selection.py +202 -0
  78. desdeo/emo/operators/selection.py +1778 -0
  79. desdeo/emo/operators/termination.py +286 -0
  80. desdeo/emo/options/__init__.py +108 -0
  81. desdeo/emo/options/algorithms.py +435 -0
  82. desdeo/emo/options/crossover.py +164 -0
  83. desdeo/emo/options/generator.py +131 -0
  84. desdeo/emo/options/mutation.py +260 -0
  85. desdeo/emo/options/repair.py +61 -0
  86. desdeo/emo/options/scalar_selection.py +66 -0
  87. desdeo/emo/options/selection.py +127 -0
  88. desdeo/emo/options/templates.py +383 -0
  89. desdeo/emo/options/termination.py +143 -0
  90. desdeo/explanations/__init__.py +6 -0
  91. desdeo/explanations/explainer.py +100 -0
  92. desdeo/explanations/utils.py +90 -0
  93. desdeo/gdm/__init__.py +22 -0
  94. desdeo/gdm/gdmtools.py +45 -0
  95. desdeo/gdm/score_bands.py +114 -0
  96. desdeo/gdm/voting_rules.py +50 -0
  97. desdeo/mcdm/__init__.py +41 -0
  98. desdeo/mcdm/enautilus.py +338 -0
  99. desdeo/mcdm/gnimbus.py +484 -0
  100. desdeo/mcdm/nautili.py +345 -0
  101. desdeo/mcdm/nautilus.py +477 -0
  102. desdeo/mcdm/nautilus_navigator.py +656 -0
  103. desdeo/mcdm/nimbus.py +417 -0
  104. desdeo/mcdm/pareto_navigator.py +269 -0
  105. desdeo/mcdm/reference_point_method.py +186 -0
  106. desdeo/problem/__init__.py +83 -0
  107. desdeo/problem/evaluator.py +561 -0
  108. desdeo/problem/external/__init__.py +18 -0
  109. desdeo/problem/external/core.py +356 -0
  110. desdeo/problem/external/pymoo_provider.py +266 -0
  111. desdeo/problem/external/runtime.py +44 -0
  112. desdeo/problem/gurobipy_evaluator.py +562 -0
  113. desdeo/problem/infix_parser.py +341 -0
  114. desdeo/problem/json_parser.py +944 -0
  115. desdeo/problem/pyomo_evaluator.py +487 -0
  116. desdeo/problem/schema.py +1829 -0
  117. desdeo/problem/simulator_evaluator.py +348 -0
  118. desdeo/problem/sympy_evaluator.py +244 -0
  119. desdeo/problem/testproblems/__init__.py +88 -0
  120. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  121. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  122. desdeo/problem/testproblems/cake_problem.py +185 -0
  123. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  124. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  125. desdeo/problem/testproblems/forest_problem.py +283 -0
  126. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  127. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  128. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  129. desdeo/problem/testproblems/momip_problem.py +172 -0
  130. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  131. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  132. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  133. desdeo/problem/testproblems/re_problem.py +492 -0
  134. desdeo/problem/testproblems/river_pollution_problems.py +440 -0
  135. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  136. desdeo/problem/testproblems/simple_problem.py +351 -0
  137. desdeo/problem/testproblems/simulator_problem.py +92 -0
  138. desdeo/problem/testproblems/single_objective.py +289 -0
  139. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  140. desdeo/problem/testproblems/zdt_problem.py +274 -0
  141. desdeo/problem/utils.py +245 -0
  142. desdeo/tools/GenerateReferencePoints.py +181 -0
  143. desdeo/tools/__init__.py +120 -0
  144. desdeo/tools/desc_gen.py +22 -0
  145. desdeo/tools/generics.py +165 -0
  146. desdeo/tools/group_scalarization.py +3090 -0
  147. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  148. desdeo/tools/indicators_binary.py +117 -0
  149. desdeo/tools/indicators_unary.py +362 -0
  150. desdeo/tools/interaction_schema.py +38 -0
  151. desdeo/tools/intersection.py +54 -0
  152. desdeo/tools/iterative_pareto_representer.py +99 -0
  153. desdeo/tools/message.py +265 -0
  154. desdeo/tools/ng_solver_interfaces.py +199 -0
  155. desdeo/tools/non_dominated_sorting.py +134 -0
  156. desdeo/tools/patterns.py +283 -0
  157. desdeo/tools/proximal_solver.py +99 -0
  158. desdeo/tools/pyomo_solver_interfaces.py +477 -0
  159. desdeo/tools/reference_vectors.py +229 -0
  160. desdeo/tools/scalarization.py +2065 -0
  161. desdeo/tools/scipy_solver_interfaces.py +454 -0
  162. desdeo/tools/score_bands.py +627 -0
  163. desdeo/tools/utils.py +388 -0
  164. desdeo/tools/visualizations.py +67 -0
  165. desdeo/utopia_stuff/__init__.py +0 -0
  166. desdeo/utopia_stuff/data/1.json +15 -0
  167. desdeo/utopia_stuff/data/2.json +13 -0
  168. desdeo/utopia_stuff/data/3.json +15 -0
  169. desdeo/utopia_stuff/data/4.json +17 -0
  170. desdeo/utopia_stuff/data/5.json +15 -0
  171. desdeo/utopia_stuff/from_json.py +40 -0
  172. desdeo/utopia_stuff/reinit_user.py +38 -0
  173. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  174. desdeo/utopia_stuff/utopia_problem.py +403 -0
  175. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  176. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  177. desdeo-2.1.0.dist-info/METADATA +186 -0
  178. desdeo-2.1.0.dist-info/RECORD +180 -0
  179. {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  180. desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
  181. desdeo-1.2.dist-info/METADATA +0 -16
  182. desdeo-1.2.dist-info/RECORD +0 -4
@@ -0,0 +1,338 @@
1
+ """Functions related to the E-NAUTILUS method are defined here.
2
+
3
+ Reference of the method:
4
+
5
+ Ruiz, A. B., Sindhya, K., Miettinen, K., Ruiz, F., & Luque, M. (2015).
6
+ E-NAUTILUS: A decision support system for complex multiobjective optimization
7
+ problems based on the NAUTILUS method. European Journal of Operational Research,
8
+ 246(1), 218-231.
9
+ """
10
+
11
+ import numpy as np
12
+ import polars as pl
13
+ from pydantic import BaseModel, Field
14
+ from scipy.cluster.hierarchy import fcluster, linkage
15
+ from scipy.spatial.distance import pdist
16
+
17
+ from desdeo.problem import (
18
+ Problem,
19
+ numpy_array_to_objective_dict,
20
+ objective_dict_to_numpy_array,
21
+ )
22
+ from desdeo.tools import SolverResults, flip_maximized_objective_values
23
+
24
+
25
+ class ENautilusResult(BaseModel):
26
+ """The result of an iteration of the E-NAUTILUS method."""
27
+
28
+ current_iteration: int = Field(description="Number of the current iteration.")
29
+ iterations_left: int = Field(description="Number of iterations left.")
30
+ intermediate_points: list[dict[str, float]] = Field(description="New intermediate points")
31
+ reachable_best_bounds: list[dict[str, float]] = Field(
32
+ description="Best bounds of the objective function values reachable from each intermediate point."
33
+ )
34
+ reachable_worst_bounds: list[dict[str, float]] = Field(
35
+ description="Worst bounds of the objective function values reachable from each intermediate point."
36
+ )
37
+ closeness_measures: list[float] = Field(description="Closeness measures of each intermediate point.")
38
+ reachable_point_indices: list[list[int]] = Field(
39
+ description="Indices of the reachable points from each intermediate point."
40
+ )
41
+
42
+
43
+ def enautilus_get_representative_solutions(
44
+ problem: Problem, result: ENautilusResult, non_dominated_points: pl.DataFrame
45
+ ) -> list[SolverResults]:
46
+ """Returns the solution corresponding to the intermediate points.
47
+
48
+ The representative points are selected based on the current intermediate points.
49
+ If the number of iterations left is 0, then the intermediate and representative points
50
+ are equal.
51
+
52
+ Args:
53
+ problem (Problem): the problem being solved.
54
+ result (ENautilusResult): an ENautilusResponse returned by `enautilus_step`.
55
+ non_dominated_points (pl.DataFrame): a dataframe from which the
56
+ representative solutions are taken.
57
+
58
+ Returns:
59
+ SolverResults: full information about the solutions. If information
60
+ other than just objective function values are expected, then the
61
+ supplied `non_dominated_points` should contain this information.
62
+ """
63
+ obj_syms = [obj.symbol for obj in problem.objectives]
64
+ var_syms = [var.symbol for var in problem.variables]
65
+ const_syms = [con.symbol for con in problem.constraints] if problem.constraints else None
66
+ extra_syms = [extra.symbol for extra in problem.extra_funcs] if problem.extra_funcs else None
67
+ scal_syms = [scal.symbol for scal in problem.scalarization_funcs] if problem.scalarization_funcs else None
68
+
69
+ # Objective matrix (rows = ND points, cols = objectives, original senses)
70
+ obj_matrix = non_dominated_points.select(obj_syms).to_numpy()
71
+
72
+ solver_results: list[SolverResults] = []
73
+
74
+ for interm in result.intermediate_points:
75
+ interm_vec = np.array([interm[sym] for sym in obj_syms], dtype=float)
76
+
77
+ # Find index of closest ND point (Euclidean distance)
78
+ idx = int(np.argmin(np.linalg.norm(obj_matrix - interm_vec, axis=1)))
79
+
80
+ row = non_dominated_points[idx]
81
+
82
+ var_dict = {sym: row[sym] for sym in var_syms if sym in row}
83
+ obj_dict = {sym: row[sym] for sym in obj_syms}
84
+ const_dict = {sym: row[sym] for sym in const_syms if sym in row} if const_syms is not None else None
85
+ extra_dict = {sym: row[sym] for sym in extra_syms if sym in row} if extra_syms is not None else None
86
+ scal_dict = {sym: row[sym] for sym in scal_syms if sym in row} if scal_syms is not None else None
87
+
88
+ solver_results.append(
89
+ SolverResults(
90
+ optimal_variables=var_dict,
91
+ optimal_objectives=obj_dict,
92
+ constraint_values=const_dict,
93
+ extra_func_values=extra_dict,
94
+ scalarization_values=scal_dict,
95
+ success=True,
96
+ message="E-NAUTILUS: nearest non-dominated point selected for intermediate point.",
97
+ )
98
+ )
99
+
100
+ return solver_results
101
+
102
+
103
+ def enautilus_step( # noqa: PLR0913
104
+ problem: Problem,
105
+ non_dominated_points: pl.DataFrame | dict[str, float],
106
+ current_iteration: int,
107
+ iterations_left: int,
108
+ selected_point: dict[str, float],
109
+ reachable_point_indices: list[int],
110
+ number_of_intermediate_points: int,
111
+ ) -> ENautilusResult:
112
+ """Compute one iteration of the E-NAUTILUS method.
113
+
114
+ It is assumed that information from a previous iteration (selected point,
115
+ etc.) is available either from a previous iteration of E-NAUTILUS, or if
116
+ this is the first iteration, then the selected (intermediate) point
117
+ `selected_point` should be the approximated nadir point from
118
+ `non_dominated_points`. In this case, the `reachable_point_indices` should
119
+ cover the whole of `non_dominated_points`. After the first iteration, all
120
+ the information for computing the next iteration is always available from
121
+ the previous iteration's result of this function (plus the `selected_point`
122
+ provided by e.g., a decision maker).
123
+
124
+ Args:
125
+ problem (Problem): the problem being solved. Used mainly for manipulating the other arguments.
126
+ non_dominated_points (pl.DataFrame): a set of non-dominated points
127
+ approximating the Pareto front of `Problem`. This should be a Polars
128
+ dataframe with at least columns that match the objective function
129
+ symbols in `Problem` and the corresponding minimization value column.
130
+ I.e., for an objective with symbol 'f1' the dataframe should have the
131
+ symbols 'f1' and 'f1_min', where the column 'f1_min has the
132
+ corresponding values of 'f1', but assuming minimization (N.B. if 'f1' is
133
+ minimized, then 'f1_min' would have identical values as 'f1'). If provided
134
+ as a `dict`, this will be converted to a polars dataframe.
135
+ current_iteration (int): the number of the current iteration. For the first iteration, this should be zero.
136
+ iterations_left (int): how many iteration are left (counting the current one).
137
+ selected_point (dict[str, float]): the selected intermediate point in
138
+ the previous iteration. If this is the first iteration, then this should
139
+ be the nadir point approximated from `non_dominated_points`.
140
+ reachable_point_indices (list[int]): the indices of the points in
141
+ `non_dominated_points` that are reachable from
142
+ `current_iteration_point`.
143
+ number_of_intermediate_points (int): how many intermediate points are generated.
144
+
145
+ Returns:
146
+ ENautilusResult: the result of the iteration.
147
+ """
148
+ # treat everything as minimized
149
+ # selected point as numpy array, correct for minimization
150
+ z_h = objective_dict_to_numpy_array(problem, flip_maximized_objective_values(problem, selected_point))
151
+
152
+ # subset of reachable solutions, take _min column
153
+ if isinstance(non_dominated_points, dict):
154
+ # dict converted to Polars dataframe
155
+ _non_dominated_points = pl.DataFrame(non_dominated_points)
156
+ else:
157
+ # already Polars dataframe
158
+ _non_dominated_points = non_dominated_points
159
+
160
+ non_dom_objectives = _non_dominated_points[[f"{obj.symbol}_min" for obj in problem.objectives]].to_numpy()
161
+ p_h = non_dom_objectives[reachable_point_indices]
162
+
163
+ # estimate nadir from non-dominated points, treating as minimized problem
164
+ z_nadir = non_dom_objectives.max(axis=0)
165
+
166
+ # compute representative points
167
+ representative_points = prune_by_average_linkage(p_h, number_of_intermediate_points)
168
+
169
+ # calculate intermediate points
170
+ intermediate_points = calculate_intermediate_points(z_h, representative_points, iterations_left)
171
+
172
+ # calculate lower bounds
173
+ intermediate_lower_bounds = [
174
+ calculate_lower_bounds(p_h, intermediate_point) for intermediate_point in intermediate_points
175
+ ]
176
+
177
+ # calculate closeness measures
178
+ closeness_measures = [
179
+ calculate_closeness(intermediate_point, z_nadir, representative_point)
180
+ for (intermediate_point, representative_point) in zip(intermediate_points, representative_points, strict=True)
181
+ ]
182
+
183
+ # calculate the indices of the reachable points for each intermediate point
184
+ reachable_from_intermediate = [
185
+ calculate_reachable_subset(non_dom_objectives, reachable_point_indices, lower_bounds, interm)
186
+ for lower_bounds, interm in zip(intermediate_lower_bounds, intermediate_points, strict=True)
187
+ ]
188
+
189
+ best_bounds = [
190
+ flip_maximized_objective_values(problem, numpy_array_to_objective_dict(problem, bounds))
191
+ for bounds in intermediate_lower_bounds
192
+ ]
193
+ worst_bounds = [
194
+ flip_maximized_objective_values(problem, numpy_array_to_objective_dict(problem, point))
195
+ for point in intermediate_points
196
+ ]
197
+
198
+ corrected_intermediate_points = [
199
+ flip_maximized_objective_values(problem, numpy_array_to_objective_dict(problem, point))
200
+ for point in intermediate_points
201
+ ]
202
+
203
+ return ENautilusResult(
204
+ current_iteration=current_iteration + 1,
205
+ iterations_left=iterations_left - 1,
206
+ intermediate_points=corrected_intermediate_points,
207
+ reachable_best_bounds=best_bounds,
208
+ reachable_worst_bounds=worst_bounds,
209
+ closeness_measures=closeness_measures,
210
+ reachable_point_indices=reachable_from_intermediate,
211
+ )
212
+
213
+
214
+ def prune_by_average_linkage(non_dominated_points: np.ndarray, k: int) -> np.ndarray:
215
+ """Prune a set of non-dominated points using average linkage clustering (Morse, 1980).
216
+
217
+ This is used to calculate the representative solutions in E-NAUTILUS.
218
+
219
+ Args:
220
+ non_dominated_points (np.ndarray): an array of non-dominated points in objective space.
221
+ k (int): Number of representative points to retain.
222
+
223
+ Returns:
224
+ np.ndarray: an array of representative points.
225
+ """
226
+ if len(non_dominated_points) <= k:
227
+ # no need to prune
228
+ return non_dominated_points
229
+
230
+ # Compute pairwise distances
231
+ distances = pdist(non_dominated_points, metric="euclidean")
232
+
233
+ # Hierarchical clustering using average linkage
234
+ z = linkage(distances, method="average")
235
+
236
+ # Cut tree to form k clusters
237
+ cluster_labels = fcluster(z, k, criterion="maxclust")
238
+
239
+ # For each cluster, choose the point closest to the centroid
240
+ representatives = []
241
+ for cluster_id in range(1, k + 1):
242
+ cluster_points = non_dominated_points[cluster_labels == cluster_id]
243
+ centroid = cluster_points.mean(axis=0)
244
+ closest_idx = np.argmin(np.linalg.norm(cluster_points - centroid, axis=1))
245
+ representatives.append(cluster_points[closest_idx])
246
+
247
+ return np.array(representatives)
248
+
249
+
250
+ def calculate_intermediate_points(
251
+ z_previous: np.ndarray, zs_representatives: np.ndarray, iterations_left: int
252
+ ) -> np.ndarray:
253
+ """Calculates the intermediate points to be shown to the decision maker at each iteration.
254
+
255
+ The number of returned points depends on how many `zs_representative points` are supplied.
256
+
257
+ Args:
258
+ z_previous (np.ndarray): the point selected by the decision maker in the previous iteration.
259
+ zs_representatives (np.ndarray): the representative solutions at the current iteration.
260
+ iterations_left (int): the number of iterations left (including the current one).
261
+
262
+ Returns:
263
+ np.ndarray: an array of intermediate points.
264
+ """
265
+ return ((iterations_left - 1) / iterations_left) * z_previous + (1 / iterations_left) * zs_representatives
266
+
267
+
268
+ def calculate_reachable_subset(
269
+ non_dominated_points: np.ndarray, reachable_indices: np.ndarray, lower_bounds: np.ndarray, z_preferred: np.ndarray
270
+ ) -> list[int]:
271
+ """Calculates the reachable subset on a non-dominated set from a selected intermediate point.
272
+
273
+ Args:
274
+ non_dominated_points (np.ndarray): the original set of non-dominated points.
275
+ reachable_indices (np.ndarray): the currently reachable indices, which
276
+ will be used to select the new reachable points.
277
+ lower_bounds (np.ndarray): the lower bounds of the reachable subset of non-dominates points.
278
+ z_preferred (np.ndarray): the selected intermediate point subject to the reachable subset is calculated.
279
+
280
+ Returns:
281
+ list[int]: the indices of the reachable solutions
282
+ """
283
+ return [
284
+ i
285
+ for i, z in enumerate(non_dominated_points)
286
+ if np.all(lower_bounds <= z) and np.all(z <= z_preferred) and i in reachable_indices
287
+ ]
288
+
289
+
290
+ def calculate_lower_bounds(non_dominated_points: np.ndarray, z_intermediate: np.ndarray) -> np.ndarray:
291
+ """Calculates the lower bounds of reachable solutions from an intermediate point.
292
+
293
+ The lower bounds are calculated by solving an epsilon-constraint problem
294
+ with the epsilon values taken from the intermediate point.
295
+
296
+ Args:
297
+ non_dominated_points (np.ndarray): a set of non-dominated points
298
+ according to which the reachable values are computed.
299
+ z_intermediate (np.ndarray): the intermediate point according to which
300
+ the lower bounds are calculated.
301
+
302
+ Returns:
303
+ np.ndarray: the lower bounds of reachable solutions on the non-dominated
304
+ set based from the intermediate point.
305
+ """
306
+ k = non_dominated_points.shape[1]
307
+ bounds = []
308
+
309
+ for r in range(k):
310
+ # Indices of objectives other than r
311
+ other = np.delete(np.arange(k), r)
312
+
313
+ # Find points that are no worse than z_intermediate in all objectives except r
314
+ mask = np.all(non_dominated_points[:, other] <= z_intermediate[other], axis=1)
315
+ feasible = non_dominated_points[mask]
316
+
317
+ if feasible.size > 0:
318
+ bounds.append(np.min(feasible[:, r]))
319
+ else:
320
+ bounds.append(np.inf) # No feasible point in this projection
321
+
322
+ return np.array(bounds)
323
+
324
+
325
+ def calculate_closeness(z_intermediate: np.ndarray, z_nadir: np.ndarray, z_representative: np.ndarray) -> float:
326
+ """Calculate the closeness of an intermediate point to the non-dominated set.
327
+
328
+ The greater the closeness is, the close intermediate point is to the non-dominated set.
329
+
330
+ Args:
331
+ z_intermediate (np.ndarray): the intermediate point.
332
+ z_nadir (np.ndarray): the nadir point of the non-dominated set.
333
+ z_representative (np.ndarray): the representative solution of `z_intermediate`.
334
+
335
+ Returns:
336
+ float: the closeness measure.
337
+ """
338
+ return np.linalg.norm(z_intermediate - z_nadir) / np.linalg.norm(z_representative - z_nadir) * 100