desdeo 1.2__py3-none-any.whl → 2.0.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 (122) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/api/README.md +73 -0
  3. desdeo/api/__init__.py +15 -0
  4. desdeo/api/app.py +40 -0
  5. desdeo/api/config.py +69 -0
  6. desdeo/api/config.toml +53 -0
  7. desdeo/api/db.py +25 -0
  8. desdeo/api/db_init.py +79 -0
  9. desdeo/api/db_models.py +164 -0
  10. desdeo/api/malaga_db_init.py +27 -0
  11. desdeo/api/models/__init__.py +66 -0
  12. desdeo/api/models/archive.py +34 -0
  13. desdeo/api/models/preference.py +90 -0
  14. desdeo/api/models/problem.py +507 -0
  15. desdeo/api/models/reference_point_method.py +18 -0
  16. desdeo/api/models/session.py +46 -0
  17. desdeo/api/models/state.py +96 -0
  18. desdeo/api/models/user.py +51 -0
  19. desdeo/api/routers/_NAUTILUS.py +245 -0
  20. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  21. desdeo/api/routers/_NIMBUS.py +762 -0
  22. desdeo/api/routers/__init__.py +5 -0
  23. desdeo/api/routers/problem.py +110 -0
  24. desdeo/api/routers/reference_point_method.py +117 -0
  25. desdeo/api/routers/session.py +76 -0
  26. desdeo/api/routers/test.py +16 -0
  27. desdeo/api/routers/user_authentication.py +366 -0
  28. desdeo/api/schema.py +94 -0
  29. desdeo/api/tests/__init__.py +0 -0
  30. desdeo/api/tests/conftest.py +59 -0
  31. desdeo/api/tests/test_models.py +701 -0
  32. desdeo/api/tests/test_routes.py +216 -0
  33. desdeo/api/utils/database.py +274 -0
  34. desdeo/api/utils/logger.py +29 -0
  35. desdeo/core.py +27 -0
  36. desdeo/emo/__init__.py +29 -0
  37. desdeo/emo/hooks/archivers.py +172 -0
  38. desdeo/emo/methods/EAs.py +418 -0
  39. desdeo/emo/methods/__init__.py +0 -0
  40. desdeo/emo/methods/bases.py +59 -0
  41. desdeo/emo/operators/__init__.py +1 -0
  42. desdeo/emo/operators/crossover.py +780 -0
  43. desdeo/emo/operators/evaluator.py +118 -0
  44. desdeo/emo/operators/generator.py +356 -0
  45. desdeo/emo/operators/mutation.py +1053 -0
  46. desdeo/emo/operators/selection.py +1036 -0
  47. desdeo/emo/operators/termination.py +178 -0
  48. desdeo/explanations/__init__.py +6 -0
  49. desdeo/explanations/explainer.py +100 -0
  50. desdeo/explanations/utils.py +90 -0
  51. desdeo/mcdm/__init__.py +19 -0
  52. desdeo/mcdm/nautili.py +345 -0
  53. desdeo/mcdm/nautilus.py +477 -0
  54. desdeo/mcdm/nautilus_navigator.py +655 -0
  55. desdeo/mcdm/nimbus.py +417 -0
  56. desdeo/mcdm/pareto_navigator.py +269 -0
  57. desdeo/mcdm/reference_point_method.py +116 -0
  58. desdeo/problem/__init__.py +79 -0
  59. desdeo/problem/evaluator.py +561 -0
  60. desdeo/problem/gurobipy_evaluator.py +562 -0
  61. desdeo/problem/infix_parser.py +341 -0
  62. desdeo/problem/json_parser.py +944 -0
  63. desdeo/problem/pyomo_evaluator.py +468 -0
  64. desdeo/problem/schema.py +1808 -0
  65. desdeo/problem/simulator_evaluator.py +298 -0
  66. desdeo/problem/sympy_evaluator.py +244 -0
  67. desdeo/problem/testproblems/__init__.py +73 -0
  68. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  69. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  70. desdeo/problem/testproblems/forest_problem.py +275 -0
  71. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  72. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  73. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  74. desdeo/problem/testproblems/momip_problem.py +172 -0
  75. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  76. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  77. desdeo/problem/testproblems/re_problem.py +492 -0
  78. desdeo/problem/testproblems/river_pollution_problem.py +434 -0
  79. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  80. desdeo/problem/testproblems/simple_problem.py +351 -0
  81. desdeo/problem/testproblems/simulator_problem.py +92 -0
  82. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  83. desdeo/problem/testproblems/zdt_problem.py +271 -0
  84. desdeo/problem/utils.py +245 -0
  85. desdeo/tools/GenerateReferencePoints.py +181 -0
  86. desdeo/tools/__init__.py +102 -0
  87. desdeo/tools/generics.py +145 -0
  88. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  89. desdeo/tools/indicators_binary.py +11 -0
  90. desdeo/tools/indicators_unary.py +375 -0
  91. desdeo/tools/interaction_schema.py +38 -0
  92. desdeo/tools/intersection.py +54 -0
  93. desdeo/tools/iterative_pareto_representer.py +99 -0
  94. desdeo/tools/message.py +234 -0
  95. desdeo/tools/ng_solver_interfaces.py +199 -0
  96. desdeo/tools/non_dominated_sorting.py +133 -0
  97. desdeo/tools/patterns.py +281 -0
  98. desdeo/tools/proximal_solver.py +99 -0
  99. desdeo/tools/pyomo_solver_interfaces.py +464 -0
  100. desdeo/tools/reference_vectors.py +462 -0
  101. desdeo/tools/scalarization.py +3138 -0
  102. desdeo/tools/scipy_solver_interfaces.py +454 -0
  103. desdeo/tools/score_bands.py +464 -0
  104. desdeo/tools/utils.py +320 -0
  105. desdeo/utopia_stuff/__init__.py +0 -0
  106. desdeo/utopia_stuff/data/1.json +15 -0
  107. desdeo/utopia_stuff/data/2.json +13 -0
  108. desdeo/utopia_stuff/data/3.json +15 -0
  109. desdeo/utopia_stuff/data/4.json +17 -0
  110. desdeo/utopia_stuff/data/5.json +15 -0
  111. desdeo/utopia_stuff/from_json.py +40 -0
  112. desdeo/utopia_stuff/reinit_user.py +38 -0
  113. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  114. desdeo/utopia_stuff/utopia_problem.py +403 -0
  115. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  116. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  117. desdeo-2.0.0.dist-info/LICENSE +21 -0
  118. desdeo-2.0.0.dist-info/METADATA +168 -0
  119. desdeo-2.0.0.dist-info/RECORD +120 -0
  120. {desdeo-1.2.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
  121. desdeo-1.2.dist-info/METADATA +0 -16
  122. desdeo-1.2.dist-info/RECORD +0 -4
@@ -0,0 +1,271 @@
1
+ from desdeo.problem.schema import (
2
+ ExtraFunction,
3
+ Objective,
4
+ Problem,
5
+ Variable,
6
+ )
7
+
8
+ def zdt1(number_of_variables: int) -> Problem:
9
+ r"""Defines the ZDT1 test problem.
10
+
11
+ The problem has a variable number of decision variables and two objective functions to be minimized as
12
+ follows:
13
+
14
+ \begin{align*}
15
+ \min\quad f_1(\textbf{x}) &= x_1 \\
16
+ \min\quad f_2(\textbf{x}) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\
17
+ g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\
18
+ h(f_1, g) &= 1 - \sqrt{\frac{f_1}{g}}, \\
19
+ \end{align*}
20
+
21
+ where $f_1$ and $f_2$ are objective functions, $x_1,\dots,x_n$ are decision variable, $n$
22
+ is the number of decision variables,
23
+ and $g$ and $h$ are auxiliary functions.
24
+ """
25
+ n = number_of_variables
26
+
27
+ # function f_1
28
+ f1_symbol = "f_1"
29
+ f1_expr = "x_1"
30
+
31
+ # function g
32
+ g_symbol = "g"
33
+ g_expr_1 = f"1 + (9 / ({n} - 1))"
34
+ g_expr_2 = "(" + " + ".join([f"x_{i}" for i in range(2, n + 1)]) + ")"
35
+ g_expr = g_expr_1 + " * " + g_expr_2
36
+
37
+ # function h(f, g)
38
+ h_symbol = "h"
39
+ h_expr = f"1 - Sqrt(({f1_expr}) / ({g_expr}))"
40
+
41
+ # function f_2
42
+ f2_symbol = "f_2"
43
+ f2_expr = f"{g_symbol} * {h_symbol}"
44
+
45
+ variables = [
46
+ Variable(name=f"x_{i}", symbol=f"x_{i}", variable_type="real", lowerbound=0, upperbound=1, initial_value=0.5)
47
+ for i in range(1, n + 1)
48
+ ]
49
+
50
+ objectives = [
51
+ Objective(
52
+ name="f_1",
53
+ symbol=f1_symbol,
54
+ func=f1_expr,
55
+ maximize=False,
56
+ ideal=0,
57
+ nadir=1,
58
+ is_convex=True,
59
+ is_linear=True,
60
+ is_twice_differentiable=True,
61
+ ),
62
+ Objective(
63
+ name="f_2",
64
+ symbol=f2_symbol,
65
+ func=f2_expr,
66
+ maximize=False,
67
+ ideal=0,
68
+ nadir=1,
69
+ is_convex=True,
70
+ is_linear=False,
71
+ is_twice_differentiable=True,
72
+ ),
73
+ ]
74
+
75
+ extras = [
76
+ ExtraFunction(
77
+ name="g", symbol=g_symbol, func=g_expr, is_convex=True, is_linear=True, is_twice_differentiable=True
78
+ ),
79
+ ExtraFunction(
80
+ name="h", symbol=h_symbol, func=h_expr, is_convex=True, is_linear=False, is_twice_differentiable=True
81
+ ),
82
+ ]
83
+
84
+ return Problem(
85
+ name="zdt1",
86
+ description="The ZDT1 test problem.",
87
+ variables=variables,
88
+ objectives=objectives,
89
+ extra_funcs=extras,
90
+ is_convex=True,
91
+ is_linear=False,
92
+ is_twice_differentiable=True,
93
+ )
94
+
95
+
96
+ def zdt2(n_variables: int) -> Problem:
97
+ r"""Defines the ZDT2 test problem.
98
+
99
+ The problem has a variable number of decision variables and two objective functions to be minimized as
100
+ follows:
101
+
102
+ \begin{align*}
103
+ \min\quad f_1(\textbf{x}) &= x_1 \\
104
+ \min\quad f_2(\textbf{x}) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\
105
+ g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\
106
+ h(f_1, g) &= 1 - \left({\frac{f_1}{g}}\right)^2, \\
107
+ \end{align*}
108
+
109
+ where $f_1$ and $f_2$ are objective functions, $x_1,\dots,x_n$ are decision variable, $n$
110
+ is the number of decision variables,
111
+ and $g$ and $h$ are auxiliary functions.
112
+ """
113
+ n = n_variables
114
+
115
+ # function f_1
116
+ f1_symbol = "f_1"
117
+ f1_expr = "x_1"
118
+
119
+ # function g
120
+ g_symbol = "g"
121
+ g_expr_1 = f"1 + (9 / ({n} - 1))"
122
+ g_expr_2 = "(" + " + ".join([f"x_{i}" for i in range(2, n + 1)]) + ")"
123
+ g_expr = g_expr_1 + " * " + g_expr_2
124
+
125
+ # function h(f, g)
126
+ h_symbol = "h"
127
+ h_expr = f"1 - (({f1_expr}) / ({g_expr})) ** 2"
128
+
129
+ # function f_2
130
+ f2_symbol = "f_2"
131
+ f2_expr = f"{g_symbol} * {h_symbol}"
132
+
133
+ variables = [
134
+ Variable(name=f"x_{i}", symbol=f"x_{i}", variable_type="real", lowerbound=0, upperbound=1, initial_value=0.5)
135
+ for i in range(1, n + 1)
136
+ ]
137
+
138
+ objectives = [
139
+ Objective(
140
+ name="f_1",
141
+ symbol=f1_symbol,
142
+ func=f1_expr,
143
+ maximize=False,
144
+ ideal=0,
145
+ nadir=1,
146
+ is_convex=True,
147
+ is_linear=True,
148
+ is_twice_differentiable=True,
149
+ ),
150
+ Objective(
151
+ name="f_2",
152
+ symbol=f2_symbol,
153
+ func=f2_expr,
154
+ maximize=False,
155
+ ideal=0,
156
+ nadir=1,
157
+ is_convex=False,
158
+ is_linear=False,
159
+ is_twice_differentiable=True,
160
+ ),
161
+ ]
162
+
163
+ extras = [
164
+ ExtraFunction(
165
+ name="g", symbol=g_symbol, func=g_expr, is_convex=True, is_linear=True, is_twice_differentiable=True
166
+ ),
167
+ ExtraFunction(
168
+ name="h", symbol=h_symbol, func=h_expr, is_convex=False, is_linear=False, is_twice_differentiable=True
169
+ ),
170
+ ]
171
+
172
+ return Problem(
173
+ name="zdt2",
174
+ description="The ZDT2 test problem.",
175
+ variables=variables,
176
+ objectives=objectives,
177
+ extra_funcs=extras,
178
+ is_convex=False,
179
+ is_linear=False,
180
+ is_twice_differentiable=True,
181
+ )
182
+
183
+
184
+ def zdt3(
185
+ n_variables: int,
186
+ ) -> Problem:
187
+ r"""Defines the ZDT3 test problem.
188
+
189
+ The problem has a variable number of decision variables and two objective functions to be minimized as
190
+ follows:
191
+
192
+ \begin{align*}
193
+ \min\quad f_1(x) &= x_1 \\
194
+ \min\quad f_2(x) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\
195
+ g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\
196
+ h(f_1, g) &= 1 - \sqrt{\frac{f_1}{g}} - \frac{f_1}{g} \sin(10\pi f_1)), \\
197
+ \end{align*}
198
+
199
+ where $f_2$ and $f_2$ are objective functions, $x_1,\dots,x_n$ are decision variable, $n$
200
+ is the number of decision variables,
201
+ and $g$ and $h$ are auxiliary functions.
202
+ """
203
+ n = n_variables
204
+
205
+ # function f_1
206
+ f1_symbol = "f_1"
207
+ f1_expr = "x_1"
208
+
209
+ # function g
210
+ g_symbol = "g"
211
+ g_expr_1 = f"1 + (9 / ({n} - 1))"
212
+ g_expr_2 = "(" + " + ".join([f"x_{i}" for i in range(2, n + 1)]) + ")"
213
+ g_expr = g_expr_1 + " * " + g_expr_2
214
+
215
+ # function h(f, g)
216
+ h_symbol = "h"
217
+ h_expr = f"1 - Sqrt(({f1_expr}) / ({g_expr})) - (({f1_expr}) / ({g_expr})) * Sin (10 * {np.pi} * {f1_expr}) "
218
+
219
+ # function f_2
220
+ f2_symbol = "f_2"
221
+ f2_expr = f"{g_symbol} * {h_symbol}"
222
+
223
+ variables = [
224
+ Variable(name=f"x_{i}", symbol=f"x_{i}", variable_type="real", lowerbound=0, upperbound=1, initial_value=0.5)
225
+ for i in range(1, n + 1)
226
+ ]
227
+
228
+ objectives = [
229
+ Objective(
230
+ name="f_1",
231
+ symbol=f1_symbol,
232
+ func=f1_expr,
233
+ maximize=False,
234
+ ideal=0,
235
+ nadir=1,
236
+ is_convex=True,
237
+ is_linear=True,
238
+ is_twice_differentiable=True,
239
+ ),
240
+ Objective(
241
+ name="f_2",
242
+ symbol=f2_symbol,
243
+ func=f2_expr,
244
+ maximize=False,
245
+ ideal=-1,
246
+ nadir=1,
247
+ is_convex=False,
248
+ is_linear=False,
249
+ is_twice_differentiable=True,
250
+ ),
251
+ ]
252
+
253
+ extras = [
254
+ ExtraFunction(
255
+ name="g", symbol=g_symbol, func=g_expr, is_convex=True, is_linear=True, is_twice_differentiable=True
256
+ ),
257
+ ExtraFunction(
258
+ name="h", symbol=h_symbol, func=h_expr, is_convex=False, is_linear=False, is_twice_differentiable=True
259
+ ),
260
+ ]
261
+
262
+ return Problem(
263
+ name="zdt3",
264
+ description="The ZDT3 test problem.",
265
+ variables=variables,
266
+ objectives=objectives,
267
+ extra_funcs=extras,
268
+ is_convex=False,
269
+ is_linear=False,
270
+ is_twice_differentiable=True,
271
+ )
@@ -0,0 +1,245 @@
1
+ """Various utilities used across the framework related to the Problem formulation."""
2
+
3
+ import itertools
4
+ import warnings
5
+ from functools import reduce
6
+
7
+ import numpy as np
8
+ import polars as pl
9
+
10
+ from desdeo.problem import Problem, TensorConstant, TensorVariable, Variable
11
+
12
+
13
+ class ProblemUtilsError(Exception):
14
+ """Raised when an exception occurs in one of the utils function.
15
+
16
+ Raised when an exception occurs in one of the utils functions defined in the Problem module.
17
+ """
18
+
19
+
20
+ def objective_dict_to_numpy_array(problem: Problem, objective_dict: dict[str, float]) -> np.ndarray:
21
+ """Takes a dict with an objective vector and returns a numpy array.
22
+
23
+ Takes a dict with the keys being objective function symbols and the values
24
+ being the corresponding objective function values. Returns a numpy array
25
+ with the objective function values in the same order they have been defined
26
+ in the original problem.
27
+
28
+ Args:
29
+ problem (Problem): the problem the objective dict belongs to.
30
+ objective_dict (dict[str, float]): the dict with the objective function values.
31
+
32
+ Returns:
33
+ np.ndarray: a numpy array with the objective function values in the order they are
34
+ present in problem.
35
+ """
36
+ if isinstance(objective_dict[problem.objectives[0].symbol], list):
37
+ if len(objective_dict[problem.objectives[0].symbol]) != 1:
38
+ raise ValueError("The objective_dict has multiple values for an objective function")
39
+ return np.array([objective_dict[objective.symbol][0] for objective in problem.objectives])
40
+ return np.array([objective_dict[objective.symbol] for objective in problem.objectives])
41
+
42
+
43
+ def numpy_array_to_objective_dict(problem: Problem, numpy_array: np.ndarray) -> dict[str, float]:
44
+ """Takes a numpy array with objective function values and return a dict.
45
+
46
+ The reverse of objective_dict_to_numpy_array.
47
+
48
+ Args:
49
+ problem (Problem): the problem the numpy array represents an objective vector of.
50
+ numpy_array (np.ndarray): the objective vector as a numpy array. The
51
+ array is squeezed, i.e., axes or length one are removed: [[42]] -> [42].
52
+
53
+ Returns:
54
+ dict[str, float]: a dict with keys being objective function symbols and value being
55
+ objective function values.
56
+ """
57
+ return {objective.symbol: np.squeeze(numpy_array).tolist()[i] for i, objective in enumerate(problem.objectives)}
58
+
59
+
60
+ def unflatten_variable_array(problem: Problem, var_array: np.ndarray) -> dict[str, float | list]:
61
+ """Unflatten a numpy array representing decision variable values.
62
+
63
+ Unflatten a numpy array that represent decision variable values. It is assumed
64
+ that the unflattened values follow a C-like order when it comes to unflattening
65
+ values for `TensorVariable`s. Note that `var_array` must be of dimension 1.
66
+
67
+ Args:
68
+ problem (Problem): the problem instance the decision variables are associated with.
69
+ var_array (np.ndarray): a flat 1D array of numerical values representing
70
+ decision variable values.
71
+
72
+ Raises:
73
+ ValueError: `var_array` is of some other dimension that 1.
74
+ IndexError: `var_array` has too few elements given the variables defined in
75
+ the instance of `Problem`.
76
+ TypeError: unsupported variable type encountered in the variables defined in the
77
+ instance of `Problem`.
78
+
79
+ Returns:
80
+ dict[str, float | list]: a dict with keys equal to the symbols of the variables
81
+ defined in the `Problem` instance, and values equal to the decision variable
82
+ values as they were defined in `var_array`.
83
+ """
84
+ if (dimension := var_array.ndim) != 1:
85
+ msg = f"The given variable array must have a dimension of 1. Current {dimension=}"
86
+ raise ValueError(msg)
87
+
88
+ var_dict = {}
89
+ array_i = 0
90
+ for var in problem.variables:
91
+ if array_i >= len(var_array):
92
+ msg = (
93
+ "End of variable array reached before all variables in the problem were iterated over. "
94
+ f"The variable array is too short with length={len(var_array)}."
95
+ )
96
+ raise IndexError(msg)
97
+
98
+ if isinstance(var, Variable):
99
+ # regular variable, just pick it
100
+ var_dict[var.symbol] = var_array[array_i].item()
101
+ array_i += 1
102
+ continue
103
+
104
+ if isinstance(var, TensorVariable):
105
+ # tensor variable, pick row-wise from var_array
106
+ slice_length = reduce(lambda x1, x2: x1 * x2, var.shape) # product of dimensions
107
+ flat_values = var_array[array_i : array_i + slice_length]
108
+ var_dict[var.symbol] = np.reshape(flat_values, var.shape, order="C").tolist()
109
+ array_i += slice_length
110
+ continue
111
+
112
+ msg = f"Unsupported variable type {type(var)} encountered."
113
+ raise TypeError(msg)
114
+
115
+ # check if values remain in var_array
116
+ if array_i < len(var_array):
117
+ # some values remain, warn user, but do not raise an error
118
+ msg = f"Warning, the variable array had some values that were not unflattened: f{["...", *var_array[array_i:]]}"
119
+ warnings.warn(msg, stacklevel=2)
120
+
121
+ # return the variable dict
122
+ return var_dict
123
+
124
+
125
+ def flatten_variable_dict(problem: Problem, variable_dict: dict[str, float | list]) -> np.ndarray:
126
+ """Flatten a dictionary representing variable values of an instance of `Problem` into a numpy array.
127
+
128
+ Flattens a dictionary representing variable values of an instance of `Problem` into a numpy array.
129
+ The flattening follows a C-like order. Support the flattening of both `Variable` and `TensorVariable`
130
+ types. Note that it is assumed that no more than one value is defined for each symbol in `variable_dict`
131
+ that correspond to the required shape of the underlying variable type.
132
+
133
+ Args:
134
+ problem (Problem): the problem instance the decision variables are associated with.
135
+ variable_dict (dict[str, float | list]): a dictionary with its keys being the symbols
136
+ of the variables defined in the instance of `Problem`, and the values corresponding
137
+ to the variables' values.
138
+
139
+ Raises:
140
+ ValueError: the `variable_dict` does not contain as its keys one or more of the symbols
141
+ defined for the variables in the instance of `Problem`.
142
+ TypeError: unsupported variable type encountered in the variables defined in the
143
+ instance of `Problem`.
144
+
145
+ Returns:
146
+ np.ndarray: a 1D numpy array with the variable values unflattened in C-like order.
147
+ """
148
+ tmp = []
149
+ for var in problem.variables:
150
+ if isinstance(var, Variable):
151
+ # just a regular variable
152
+ if var.symbol not in variable_dict:
153
+ msg = f"The variable_dict is missing values for the variable {var.symbol}."
154
+ raise ValueError(msg)
155
+ tmp.append([variable_dict[var.symbol]])
156
+ continue
157
+
158
+ if isinstance(var, TensorVariable):
159
+ # tensor variable
160
+ if var.symbol in variable_dict:
161
+ # tensor variable is defined in the dict as a tensor
162
+ tmp = [*tmp, np.array(variable_dict[var.symbol]).flatten(order="C")]
163
+ continue
164
+ if any(key.startswith(f"{var.symbol}_") for key in variable_dict):
165
+ # tensor variable flattened in the dict
166
+ indices = itertools.product(*[range(1, s + 1) for s in var.shape])
167
+ flat_symbols = [f"{var.symbol}_{'_'.join(map(str, index))}" for index in indices]
168
+ tmp = [*tmp, np.array([variable_dict[s] for s in flat_symbols])]
169
+ continue
170
+
171
+ msg = f"The variable dict is missing values for the variable {var.symbol}."
172
+ raise ValueError(msg)
173
+
174
+ msg = f"Unsupported variable type {type(var)} encountered."
175
+ raise TypeError(msg)
176
+
177
+ return np.concatenate(tmp)
178
+
179
+
180
+ def get_nadir_dict(problem: Problem) -> dict[str, float]:
181
+ """Return a dict representing a problem's nadir point.
182
+
183
+ Args:
184
+ problem (Problem): the problem with the nadir point.
185
+
186
+ Returns:
187
+ dict[str, float]: key are objective funciton symbols, values are nadir values.
188
+ """
189
+ return {objective.symbol: objective.nadir for objective in problem.objectives}
190
+
191
+
192
+ def get_ideal_dict(problem: Problem) -> dict[str, float]:
193
+ """Return a dict representing a problem's ideal point.
194
+
195
+ Args:
196
+ problem (Problem): the problem with the ideal point.
197
+
198
+ Returns:
199
+ dict[str, float]: key are objective funciton symbols, values are ideal values.
200
+ """
201
+ return {objective.symbol: objective.ideal for objective in problem.objectives}
202
+
203
+
204
+ def tensor_constant_from_dataframe(
205
+ df: pl.DataFrame, name: str, symbol: str, n_rows: int, column_names: list[str]
206
+ ) -> TensorConstant:
207
+ """Create a TensorConstant from a Polars dataframe.
208
+
209
+ Args:
210
+ df (pl.DataFrame): a Polars dataframe with at least the columns in `column_names`
211
+ name (str): name attribute of the created TensorConstant.
212
+ symbol (str): symbol attribute of the created TensorConstant.
213
+ n_rows (int): the number of rows to read from the dataframe.
214
+ column_names (list[str]): the column names in the dataframe from which
215
+ the constant values will be picked.
216
+
217
+ Returns:
218
+ TensorConstant: A TensorConstant instance with values taken from the a given
219
+ Polars dataframe. The shape of the TensorConstant will be
220
+ (`n_rows`, len(`column_names`)).
221
+
222
+ Note:
223
+ In the argument `shape` the first element must be either less or equal to the
224
+ number of rows in `df`. The second element in `shape` must be equal
225
+ to the number of element in `column_names`.
226
+ """
227
+ if n_rows > df.shape[0]:
228
+ # not enough rows in df
229
+ msg = f"Requested {n_rows} rows, but the dataframe has only {df.shape[0]} rows."
230
+ raise ProblemUtilsError(msg)
231
+
232
+ if len(column_names) > df.shape[1]:
233
+ # not enough cols in df
234
+ msg = f"Requested {len(column_names)} columns, but the dataframe has only {df.shape[1]} columns."
235
+ raise ProblemUtilsError(msg)
236
+
237
+ for col in column_names:
238
+ if col not in df.columns:
239
+ msg = f"The requested column '{col}' is not found in the given dataframe with columns {df.columns}."
240
+ raise ProblemUtilsError(msg)
241
+
242
+ selected_df = df.select(column_names).head(n_rows)
243
+ selected_values = [selected_df.to_dict()[col_name].to_list() for col_name in column_names]
244
+
245
+ return TensorConstant(name=name, symbol=symbol, shape=[n_rows, len(column_names)], values=selected_values)
@@ -0,0 +1,181 @@
1
+ """Generate reference points for the IPA algorithm."""
2
+
3
+ from itertools import product
4
+
5
+ import numpy as np
6
+ from numba import njit
7
+ from scipy.spatial import ConvexHull
8
+
9
+
10
+ def normalize(vectors):
11
+ """Normalize a set of vectors.
12
+
13
+ The length of the returned vectors will be 1.
14
+
15
+ Parameters
16
+ ----------
17
+ vectors : np.ndarray
18
+ Set of vectors of any length, except zero.
19
+
20
+ """
21
+ if len(np.asarray(vectors).shape) == 1:
22
+ return vectors / np.linalg.norm(vectors)
23
+ norm = np.linalg.norm(vectors, axis=1)
24
+ return vectors / norm[:, np.newaxis]
25
+
26
+
27
+ def householder(vector):
28
+ """Return reflection matrix via householder transformation."""
29
+ identity_mat = np.eye(len(vector))
30
+ v = vector[np.newaxis]
31
+ denominator = np.matmul(v, v.T)
32
+ numerator = np.matmul(v.T, v)
33
+ rot_mat = identity_mat - (2 * numerator / denominator)
34
+ return rot_mat
35
+
36
+
37
+ def rotate(initial_vector, rotated_vector, other_vectors):
38
+ """Calculate the rotation matrix that rotates the initial_vector to the
39
+ rotated_vector. Apply that rotation on other_vectors and return.
40
+ Uses Householder reflections twice to achieve this."""
41
+
42
+ init_vec_norm = normalize(initial_vector)
43
+ rot_vec_norm = normalize(np.asarray(rotated_vector))
44
+ middle_vec_norm = normalize(init_vec_norm + rot_vec_norm)
45
+ first_reflector = init_vec_norm - middle_vec_norm
46
+ second_reflector = middle_vec_norm - rot_vec_norm
47
+ Q1 = householder(first_reflector)
48
+ Q2 = householder(second_reflector)
49
+ reflection_matrix = np.matmul(Q2, Q1)
50
+ rotated_vectors = np.matmul(other_vectors, np.transpose(reflection_matrix))
51
+ return rotated_vectors
52
+
53
+
54
+ def get_reference_hull(num_dims):
55
+ """Get the convex hull of the valid reference points for IPA.
56
+
57
+ This algorithm generates the vertices of the unit hypercube in the (num_dims)-dimensional space.
58
+ Then, the vertices are projected onto the plane perpendicular to the largest space diagonal (vertex first parallel
59
+ projection) and rotated such that the plane is perpendular to one of the axes. Then, the points are are flattened
60
+ to (num_dims-1)-dimensional space. A convex hull is then constructed from the projected vertices, and a bounding box
61
+ is constructed around the convex hull.
62
+
63
+ Args:
64
+ num_dims (int): The number of dimensions of the space in which the reference points are generated.
65
+
66
+ Returns:
67
+ np.ndarray: A (2) x (num_dims-1) array of the bounding box. Reference points are guaranteed to be within
68
+ this box. However, not all points within this box are valid reference points.
69
+ np.ndarray: A (num_dims-1) x (num_dims-1) array of the coefficients of the hyperplanes defining the convex hull
70
+ of the bounds of the reference points. A point is a valid reference point if it lies within the convex hull.
71
+ np.ndarray: A (num_dims-1) array of the constants of the hyperplanes defining the convex hull. See above.
72
+ scipy.spatial.ConvexHull: The convex hull of the projected vertices/valid reference points.
73
+ """
74
+ vertices = np.array(list(product([0, 1], repeat=num_dims)))
75
+
76
+ # Project vertices onto plane perpendicular to largest space diagonal, rotate to make one of the objectives zero.
77
+ # Then flatten to (num_dims-1) dimensions.
78
+ rotated_vertices = rotate_in(vertices)
79
+
80
+ bounding_box = np.array([np.min(rotated_vertices, axis=0), np.max(rotated_vertices, axis=0)])
81
+
82
+ hull = ConvexHull(rotated_vertices)
83
+
84
+ A, b = get_hull_equations(hull)
85
+
86
+ return bounding_box, A, b, hull
87
+
88
+
89
+ def rotate_in(vertices):
90
+ """
91
+
92
+ Args:
93
+ vertices (_type_): _description_
94
+
95
+ Returns:
96
+ _type_: _description_
97
+ """
98
+ num_dims = len(vertices[0])
99
+ rotated_vertices = rotate([1] * num_dims, ([0] * (num_dims - 1) + [1]), vertices)
100
+ rotated_vertices = rotated_vertices[:, :-1]
101
+ return rotated_vertices
102
+
103
+
104
+ def rotate_out(points):
105
+ points = np.atleast_2d(points)
106
+ num_points, num_dims = points.shape
107
+ points_rotated = np.hstack((points, np.ones((len(points), 1))))
108
+ points_rotated = rotate(([0] * (num_dims) + [1]), [1] * (num_dims + 1), points_rotated)
109
+ # Move (along nadir-ideal direction) the plane of these points such that it passes through nadir
110
+ points_rotated = points_rotated + 1 - 1 / np.sqrt(num_dims + 1)
111
+ return points_rotated
112
+
113
+
114
+ def get_hull_equations(hull):
115
+ """Get the equations of the hyperplanes defining the convex hull.
116
+
117
+ Args:
118
+ hull (scipy.spatial.ConvexHull): A convex hull.
119
+
120
+ Returns:
121
+ np.ndarray: A (num_dims-1) x num_hyperplanes array of the coefficients of the hyperplanes defining the convex
122
+ hull.
123
+ np.ndarray: A (num_hyperplanes) array of the constants of the hyperplanes defining the convex hull.
124
+ """
125
+
126
+ return np.ascontiguousarray(hull.equations[:, :-1].T), np.ascontiguousarray(hull.equations[:, -1].T)
127
+
128
+
129
+ def generate_points(
130
+ num_points: int, num_dims: int
131
+ ) -> tuple[
132
+ np.ndarray,
133
+ np.ndarray,
134
+ ]:
135
+ """Generate reference points for the IPA algorithm.
136
+
137
+ Creates a (large) number of reference points on a plane perpendicular to the largest space diagonal of the unit
138
+ hypercube in the num_dims-dimensional space. First, the vertices of the unit hypercube are generated. Then, the
139
+ vertices are projected onto the plane perpendicular to the largest space diagonal (vertex first parallel
140
+ projection) and rotated such that the plane is perpendular to one of the axes, making all objective values zero.
141
+ A convex hull is then constructed from the projected vertices, and a bounding box is constructed
142
+ around the convex hull. Finally, points are generated uniformly within the bounding box until num_points points
143
+ are generated _inside_ the convex hull. Note that the number of dimensions must be at least 2. Also, the number of
144
+ dimensions of the reference points is one less than the number of dimensions of the objective space. This is because
145
+ the reference points are generated on the projected plane.
146
+
147
+ Args:
148
+ num_points (int): The number of reference points to generate.
149
+ num_dims (int): The number of dimensions of the space in which the reference points are generated.
150
+
151
+ Returns:
152
+ np.ndarray: A (num_points) x (num_dims-1) array of reference points.
153
+ """
154
+
155
+ bounding_box, A, b, _ = get_reference_hull(num_dims)
156
+ points = numba_random_gen(num_points, bounding_box, A, b)
157
+ # Project vertices onto plane perpendicular to largest space diagonal
158
+ points_rotated = rotate_out(points)
159
+ return points, points_rotated
160
+
161
+
162
+ @njit()
163
+ def numba_random_gen(num_points: int, bounding_box: np.ndarray, A: np.ndarray, b: np.ndarray) -> np.ndarray:
164
+ """Generates num_points random points within the convex hull defined by A and b."""
165
+ num_dims_ = bounding_box.shape[1]
166
+ points = np.zeros((num_points, num_dims_))
167
+
168
+ eps = np.finfo(np.float32).eps
169
+
170
+ counter = 0
171
+
172
+ while counter < num_points:
173
+ point = np.zeros(num_dims_)
174
+ for i in range(num_dims_):
175
+ # Generate a random point within the bounding box
176
+ point[i] = np.random.uniform(bounding_box[0, i], bounding_box[1, i])
177
+ if np.all(point @ A + b < eps):
178
+ # If the point is inside the convex hull, add it to the list of points
179
+ points[counter] = point
180
+ counter += 1
181
+ return points