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,477 @@
1
+ """Defines solver interfaces for pyomo."""
2
+
3
+ import itertools
4
+
5
+ import numpy as np
6
+ import pyomo.environ as pyomo
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+ from pyomo.opt import SolverResults as _pyomo_SolverResults
9
+ from pyomo.opt import SolverStatus as _pyomo_SolverStatus
10
+ from pyomo.opt import TerminationCondition as _pyomo_TerminationCondition
11
+
12
+ from desdeo.problem import Problem, PyomoEvaluator, TensorVariable
13
+ from desdeo.tools.generics import BaseSolver, SolverError, SolverResults
14
+
15
+
16
+ class BonminOptions(BaseModel):
17
+ """Defines a pydantic model to store and pass options to the Bonmin solver.
18
+
19
+ Because Bonmin utilizes many sub-solver, the options specific to Bonmin
20
+ must be prefixed in their name with 'bonmin.{option_name}',
21
+ e.g., `bonmin.integer_tolerance`. For a list of options, see
22
+ https://www.coin-or.org/Bonmin/options_list.html
23
+
24
+ Note:
25
+ Not all options are available through this model.
26
+ Please add options as they are needed and make a pull request.
27
+ """
28
+
29
+ tol: float = Field(description="Sets the convergence tolerance of ipopt. Defaults to 1e-8.", default=1e-8)
30
+ """Sets the convergence tolerance of ipopt. Defaults to 1e-8."""
31
+
32
+ bonmin_integer_tolerance: float = Field(
33
+ description="Numbers within this value of an integer are considered integers. Defaults to 1e-6.", default=1e-6
34
+ )
35
+ """Numbers within this value of an integer are considered integers. Defaults to 1e-6."""
36
+
37
+ bonmin_algorithm: str = Field(
38
+ description=(
39
+ "Presets some of the options in Bonmin based on the algorithm choice. Defaults to 'B-BB'. "
40
+ "A good first option to try is 'B-Hyb'."
41
+ ),
42
+ default="B-BB",
43
+ )
44
+ """Presets some of the options in Bonmin based on the algorithm choice. Defaults to 'B-BB'.
45
+ A good first option to try is 'B-Hyb'.
46
+ """
47
+
48
+ def asdict(self) -> dict[str, float]:
49
+ """Converts the Pydantic model into a dict so that Bonmin specific options are in the correct format.
50
+
51
+ This means that the attributes starting with `bonmin_optionname` will be
52
+ converted to keys in the format `bonmin.optionname` in the returned dict.
53
+ """
54
+ output = {}
55
+ for field_name, _ in BonminOptions.model_fields.items():
56
+ if (rest := field_name.split(sep="_"))[0] == "bonmin":
57
+ # Convert to Bonmin specific format
58
+ output[f"bonmin.{'_'.join(rest[1:])}"] = getattr(self, field_name)
59
+ else:
60
+ # Keep the field as is
61
+ output[field_name] = getattr(self, field_name)
62
+
63
+ return output
64
+
65
+
66
+ class IpoptOptions(BaseModel):
67
+ """Defines a pydantic dataclass to pass options to the Ipopt solver.
68
+
69
+ For more information and documentation on the options,
70
+ see https://coin-or.github.io/Ipopt/
71
+
72
+ Note:
73
+ Not all options are available through this model.
74
+ Please add options as they are needed and make a pull request.
75
+ """
76
+
77
+ tol: float = Field(description="The desired relative convergence tolerance. Defaults to 1e-8.", default=1e-8)
78
+ """The desired relative convergence tolerance. Defaults to 1e-8."""
79
+
80
+ max_iter: int = Field(description="Maximum number of iterations. Must be >1. Defaults to 3000.", default=3000)
81
+ """Maximum number of iterations. Must be >1. Defaults to 3000."""
82
+
83
+ print_level: int = Field(
84
+ description="The verbosity level of the solver's output. Ranges between 0 and 12. Defaults to 5.", default=5
85
+ )
86
+ """The verbosity level of the solver's output. Ranges between 0 and 12."""
87
+
88
+
89
+ class CbcOptions(BaseModel):
90
+ """Defines a pydantic dataclass to pass options to the CBC solver.
91
+
92
+ For more information and documentation on the options,
93
+ see https://github.com/coin-or/Cbc
94
+
95
+ Note:
96
+ Not all options are available through this model.
97
+ Please add options as they are needed and make a pull request.
98
+ """
99
+
100
+ model_config = ConfigDict(frozen=True, populate_by_name=True)
101
+
102
+ sec: int = Field(
103
+ description="The maximum amount of time (in seconds) the solver should run. Defaults to 600.", default=600
104
+ )
105
+ """The maximum amount of time (in seconds) the solver should run. Defaults to 600."""
106
+
107
+ threads: int = Field(
108
+ description="Number of threads (cores) to use for solving the problem. Defaults to 4.", default=4
109
+ )
110
+ """Number of threads (cores) to use for solving the problem. Defaults to 4."""
111
+
112
+ log_level: int = Field(
113
+ alias="logLevel",
114
+ description=(
115
+ "Controls the level of logging output. Values range from 0 (no output) to 5 (very detailed output)."
116
+ " Defaults to 2."
117
+ ),
118
+ default=2,
119
+ )
120
+ """Controls the level of logging output. Values range from 0 (no output) to 5 (very detailed output).
121
+ Defaults to 2.
122
+ """
123
+
124
+ max_solutions: int = Field(
125
+ alias="maxSolutions",
126
+ description="Limits the number of feasible solutions found by the solver. Defaults to 10.",
127
+ default=10,
128
+ )
129
+ """Limits the number of feasible solutions found by the solver. Defaults to 10."""
130
+
131
+ max_nodes: int = Field(
132
+ alias="maxNodes",
133
+ description="Sets the maximum number of branch-and-bound nodes to explore. Defaults to 1000.",
134
+ default=1000,
135
+ )
136
+ """Sets the maximum number of branch-and-bound nodes to explore. Defaults to 1000."""
137
+
138
+ ratio_gap: float = Field(
139
+ alias="ratioGap",
140
+ description=(
141
+ "Sets the relative MIP gap (as a fraction of the optimal solution value) at which the solver will"
142
+ " terminate. Defaults to 0.01."
143
+ ),
144
+ default=0.01,
145
+ )
146
+ """Sets the relative MIP gap (as a fraction of the optimal solution value) at which the solver will terminate.
147
+ Defaults to 0.01.
148
+ """
149
+
150
+ absolute_gap: float = Field(
151
+ alias="absoluteGap",
152
+ description=(
153
+ "Sets the absolute MIP gap (an absolute value) at which the solver will terminate. Defaults to 1.0."
154
+ ),
155
+ default=1.0,
156
+ )
157
+ """Sets the absolute MIP gap (an absolute value) at which the solver will terminate. Defaults to 1.0."""
158
+
159
+ solve: str = Field(
160
+ description=(
161
+ "Determines the strategy to use for solving the problem (e.g., 'branchAndCut', 'tree', 'trunk')."
162
+ " Defaults to 'branchAndCut'."
163
+ ),
164
+ default="branchAndCut",
165
+ )
166
+ """Determines the strategy to use for solving the problem (e.g., 'branchAndCut', 'tree', 'trunk').
167
+ Defaults to 'branchAndCut'.
168
+ """
169
+
170
+ presolve: int = Field(
171
+ description="Controls the presolve level (0: no presolve, 1: default, 2: aggressive). Defaults to 2.", default=2
172
+ )
173
+ """Controls the presolve level (0: no presolve, 1: default, 2: aggressive). Defaults to 2."""
174
+
175
+ feasibility_tolerance: float = Field(
176
+ alias="feasibilityTolerance",
177
+ description="Sets the feasibility tolerance for constraints. Defaults to 1e-6.",
178
+ default=1e-6,
179
+ )
180
+ """Sets the feasibility tolerance for constraints. Defaults to 1e-6."""
181
+
182
+ integer_tolerance: float = Field(
183
+ alias="integerTolerance",
184
+ description="Sets the tolerance for integrality of integer variables. Defaults to 1e-5.",
185
+ default=1e-5,
186
+ )
187
+ """Sets the tolerance for integrality of integer variables. Defaults to 1e-5."""
188
+
189
+
190
+ _default_cbc_options = CbcOptions()
191
+ """Defines CBC options with default values."""
192
+
193
+ _default_bonmin_options = BonminOptions()
194
+ """Defines Bonmin options with default values."""
195
+
196
+ _default_ipopt_options = IpoptOptions()
197
+ """Defines Ipopt optins with default values."""
198
+
199
+
200
+ def parse_pyomo_optimizer_results(
201
+ opt_res: _pyomo_SolverResults, problem: Problem, evaluator: PyomoEvaluator
202
+ ) -> SolverResults:
203
+ """Parses pyomo SolverResults into DESDEO SolverResults.
204
+
205
+ Args:
206
+ opt_res (SolverResults): the pyomo solver results.
207
+ problem (Problem): the problem being solved.
208
+ evaluator (PyomoEvaluator): the evaluator utilized to get the pyomo solver results.
209
+
210
+ Returns:
211
+ SolverResults: DESDEO solver results.
212
+ """
213
+ results = evaluator.get_values()
214
+
215
+ variable_values = {}
216
+ for var in problem.variables:
217
+ if isinstance(var, TensorVariable):
218
+ # handle tensor variables
219
+ # 1-indexing in Pyomo...
220
+ values_list = np.zeros(var.shape)
221
+ for indices in itertools.product(*(range(1, dim + 1) for dim in var.shape)):
222
+ values_list[*[idx - 1 for idx in indices]] = results[var.symbol][
223
+ indices if len(indices) > 1 else indices[0]
224
+ ]
225
+ variable_values[var.symbol] = values_list.tolist()
226
+ else:
227
+ # variable_values = {var.symbol: results[var.symbol] for var in problem.variables}
228
+ variable_values[var.symbol] = results[var.symbol]
229
+
230
+ objective_values = {obj.symbol: results[obj.symbol] for obj in problem.objectives}
231
+ constraint_values = (
232
+ {con.symbol: results[con.symbol] for con in problem.constraints} if problem.constraints else None
233
+ )
234
+
235
+ # handle constraint, which might be multi-valued
236
+ if problem.constraints is not None:
237
+ constraint_values = {}
238
+
239
+ for con in problem.constraints:
240
+ result = results[con.symbol]
241
+
242
+ if isinstance(result, dict):
243
+ # multi-valued
244
+ indices = list(getattr(evaluator.model, con.symbol).keys())
245
+ shape = tuple(len({idx[k] for idx in indices}) for k in range(len(indices[0])))
246
+ values_list = np.zeros(shape)
247
+
248
+ for idx in indices:
249
+ values_list[*[i - 1 for i in idx]] = result[idx]
250
+
251
+ constraint_values[con.symbol] = values_list.tolist()
252
+
253
+ else:
254
+ # scalar-valued
255
+ constraint_values[con.symbol] = result
256
+ else:
257
+ constraint_values = None
258
+
259
+ extra_func_values = (
260
+ {extra.symbol: results[extra.symbol] for extra in problem.extra_funcs}
261
+ if problem.extra_funcs is not None
262
+ else None
263
+ )
264
+ scalarization_values = (
265
+ {scal.symbol: results[scal.symbol] for scal in problem.scalarization_funcs}
266
+ if problem.scalarization_funcs is not None
267
+ else None
268
+ )
269
+ success = (
270
+ opt_res.solver.status == _pyomo_SolverStatus.ok
271
+ and opt_res.solver.termination_condition == _pyomo_TerminationCondition.optimal
272
+ )
273
+ msg = (
274
+ f"Pyomo solver status is: '{opt_res.solver.status}', with termination condition: "
275
+ f"'{opt_res.solver.termination_condition}'."
276
+ )
277
+
278
+ return SolverResults(
279
+ optimal_variables=variable_values,
280
+ optimal_objectives=objective_values,
281
+ constraint_values=constraint_values,
282
+ extra_func_values=extra_func_values,
283
+ scalarization_values=scalarization_values,
284
+ success=success,
285
+ message=msg,
286
+ )
287
+
288
+
289
+ class PyomoBonminSolver(BaseSolver):
290
+ """Creates pyomo solvers that utilize bonmin."""
291
+
292
+ def __init__(self, problem: Problem, options: BonminOptions | None = _default_bonmin_options):
293
+ """The solver is initialized with a problem and solver options.
294
+
295
+ Suitable for mixed-integer problems. The objective function being minimized
296
+ (target) and the constraint functions must be twice continuously
297
+ differentiable. When the objective functions and constraints are convex, the
298
+ solution is exact. When the objective or any of the constraints, or both,
299
+ are non-convex, then the solution is based on heuristics.
300
+
301
+ For more info about bonmin, see: https://www.coin-or.org/Bonmin/
302
+
303
+ Note:
304
+ Bonmin must be installed on the system running DESDEO, and its executable
305
+ must be defined in the PATH.
306
+
307
+ Args:
308
+ problem (Problem): the problem to be solved.
309
+ options (BonminOptions, optional): options to be passed to the Bonmin solver.
310
+ If `None` is passed, defaults to `_default_bonmin_options` defined in
311
+ this source file. Defaults to `None`.
312
+ """
313
+ if not problem.is_twice_differentiable:
314
+ raise SolverError("Problem must be twice differentiable.")
315
+ self.problem = problem
316
+ self.evaluator = PyomoEvaluator(problem)
317
+
318
+ if options is None:
319
+ self.options = _default_bonmin_options
320
+ else:
321
+ self.options = options
322
+
323
+ def solve(self, target: str) -> SolverResults:
324
+ """Solve the problem for a given target.
325
+
326
+ Args:
327
+ target (str): the symbol of the objective function to be optimized.
328
+
329
+ Returns:
330
+ SolverResults: the results of the optimization.
331
+ """
332
+ self.evaluator.set_optimization_target(target)
333
+
334
+ opt = pyomo.SolverFactory("bonmin", tee=True)
335
+
336
+ # set solver options
337
+ for key, value in self.options.asdict().items():
338
+ opt.options[key] = value
339
+ opt_res = opt.solve(self.evaluator.model)
340
+
341
+ return parse_pyomo_optimizer_results(opt_res, self.problem, self.evaluator)
342
+
343
+
344
+ class PyomoIpoptSolver(BaseSolver):
345
+ """Create a pyomo solver that utilizes Ipopt."""
346
+
347
+ def __init__(self, problem: Problem, options: IpoptOptions | None = _default_ipopt_options):
348
+ """The solver is initialized with a problem and solver options.
349
+
350
+ Suitable for non-linear, twice differentiable constrained problems.
351
+ The problem may be convex or non-convex.
352
+
353
+ For more information, see https://coin-or.github.io/Ipopt/
354
+
355
+ Note:
356
+ Ipopt must be installed on the system running DESDEO, and its executable
357
+ must be defined in the PATH.
358
+
359
+ Args:
360
+ problem (Problem): the problem being solved.
361
+ options (IpoptOptions, optional): options to be passed to the Ipopt solver.
362
+ If `None` is passed, defaults to `_default_ipopt_options` defined in
363
+ this source file. Defaults to `None`.
364
+ """
365
+ if not problem.is_twice_differentiable:
366
+ raise SolverError("Problem must be twice differentiable.")
367
+ self.problem = problem
368
+ self.evaluator = PyomoEvaluator(problem)
369
+
370
+ if options is None:
371
+ self.options = _default_ipopt_options
372
+ else:
373
+ self.options = options
374
+
375
+ def solve(self, target: str) -> SolverResults:
376
+ """Solve the problem for a given target.
377
+
378
+ Args:
379
+ target (str): the symbol of the objective function to be optimized.
380
+
381
+ Returns:
382
+ SolverResults: results of the Optimization.
383
+ """
384
+ self.evaluator.set_optimization_target(target)
385
+
386
+ opt = pyomo.SolverFactory("ipopt", tee=True, options=self.options.model_dump())
387
+ opt_res = opt.solve(self.evaluator.model)
388
+ return parse_pyomo_optimizer_results(opt_res, self.problem, self.evaluator)
389
+
390
+
391
+ class PyomoGurobiSolver(BaseSolver):
392
+ """Creates a pyomo solver that utilized Gurobi."""
393
+
394
+ def __init__(self, problem: Problem, options: dict[str, any] | None = None):
395
+ """Creates a pyomo solver that utilizes gurobi.
396
+
397
+ You need to have gurobi installed on your system for this to work.
398
+
399
+ Suitable for solving mixed-integer linear and quadratic optimization
400
+ problems.
401
+
402
+ Args:
403
+ problem (Problem): the problem to be solved.
404
+ options (GurobiOptions): Dictionary of Gurobi parameters to set.
405
+ This is passed to pyomo as is, so it works the same as options
406
+ would for calling pyomo SolverFactory directly.
407
+ See https://www.gurobi.com/documentation/current/refman/parameters.html
408
+ for information on the available options
409
+ """
410
+ self.problem = problem
411
+ self.evaluator = PyomoEvaluator(problem)
412
+
413
+ if options is None:
414
+ self.options = {}
415
+ else:
416
+ self.options = options
417
+
418
+ def solve(self, target: str) -> SolverResults:
419
+ """Solve the problem for a given target.
420
+
421
+ Args:
422
+ target (str): the symbol of the objective function to be optimized.
423
+
424
+ Returns:
425
+ SolverResults: the results of the optimization.
426
+ """
427
+ self.evaluator.set_optimization_target(target)
428
+
429
+ with pyomo.SolverFactory("gurobi", solver_io="python") as opt:
430
+ opt_res = opt.solve(self.evaluator.model)
431
+ return parse_pyomo_optimizer_results(opt_res, self.problem, self.evaluator)
432
+
433
+
434
+ class PyomoCBCSolver(BaseSolver):
435
+ """Create a pyomo solver that utilizes CBC."""
436
+
437
+ def __init__(self, problem: Problem, options: CbcOptions | None = _default_cbc_options):
438
+ """The solver is initialized with a problem and solver options.
439
+
440
+ Suitable for combinatorial and large-scale mixed-integer linear problems.
441
+
442
+ For more information, see https://coin-or.github.io/Ipopt/
443
+
444
+ Note:
445
+ CBC must be installed on the system running DESDEO, and its executable
446
+ must be defined in the PATH.
447
+
448
+ Args:
449
+ problem (Problem): the problem being solved.
450
+ options (CbcOptions, optional): options to be passed to the CBC solver.
451
+ If `None` is passed, defaults to `_default_cbc_options` defined in
452
+ this source file. Defaults to `None`.
453
+ """
454
+ if not problem.is_linear:
455
+ raise SolverError("Nonlinear problems not supported.")
456
+ self.problem = problem
457
+ self.evaluator = PyomoEvaluator(problem)
458
+
459
+ if options is None:
460
+ self.options = _default_cbc_options
461
+ else:
462
+ self.options = options
463
+
464
+ def solve(self, target: str) -> SolverResults:
465
+ """Solve the problem for a given target.
466
+
467
+ Args:
468
+ target (str): the symbol of the objective function to be optimized.
469
+
470
+ Returns:
471
+ SolverResults: results of the Optimization.
472
+ """
473
+ self.evaluator.set_optimization_target(target)
474
+
475
+ opt = pyomo.SolverFactory("cbc", tee=True, options=self.options.model_dump())
476
+ opt_res = opt.solve(self.evaluator.model)
477
+ return parse_pyomo_optimizer_results(opt_res, self.problem, self.evaluator)
@@ -0,0 +1,229 @@
1
+ from itertools import combinations
2
+
3
+ import numpy as np
4
+ from scipy.special import comb
5
+
6
+
7
+ def normalize(vectors):
8
+ """Normalize a set of vectors.
9
+
10
+ The length of the returned vectors will be unity.
11
+
12
+ Parameters
13
+ ----------
14
+ vectors : np.ndarray
15
+ Set of vectors of any length, except zero.
16
+
17
+ """
18
+ if len(np.asarray(vectors).shape) == 1:
19
+ return vectors / np.linalg.norm(vectors)
20
+ norm = np.linalg.norm(vectors, axis=1)
21
+ return vectors / norm[:, np.newaxis]
22
+
23
+
24
+ def shear(vectors, degrees: float = 5):
25
+ """Shear a set of vectors lying on the plane z=0 towards the z-axis.
26
+
27
+ The resulting vectors are'degrees' angle away from the z axis.
28
+
29
+ Parameters
30
+ ----------
31
+ vectors : numpy.ndarray
32
+ The final element of each vector should be zero.
33
+ degrees : float, optional
34
+ The angle that the resultant vectors make with the z axis. Unit is radians.
35
+ (the default is 5)
36
+ """
37
+ angle = degrees * np.pi / 180
38
+ m = 1 / np.tan(angle)
39
+ norm = np.linalg.norm(vectors, axis=1)
40
+ vectors[:, -1] += norm * m
41
+ return normalize(vectors)
42
+
43
+
44
+ def rotate(initial_vector, rotated_vector, other_vectors):
45
+ """Calculate the rotation matrix that rotates the initial_vector to the rotated_vector.
46
+
47
+ Apply that rotation on other_vectors and return.
48
+ Uses Householder reflections twice to achieve this.
49
+ """
50
+ init_vec_norm = normalize(initial_vector)
51
+ rot_vec_norm = normalize(np.asarray(rotated_vector))
52
+ middle_vec_norm = normalize(init_vec_norm + rot_vec_norm)
53
+ first_reflector = init_vec_norm - middle_vec_norm
54
+ second_reflector = middle_vec_norm - rot_vec_norm
55
+ Q1 = householder(first_reflector)
56
+ Q2 = householder(second_reflector)
57
+ reflection_matrix = np.matmul(Q2, Q1)
58
+ rotated_vectors = np.matmul(other_vectors, np.transpose(reflection_matrix))
59
+ return rotated_vectors
60
+
61
+
62
+ def householder(vector):
63
+ """Return reflection matrix via householder transformation."""
64
+ identity_mat = np.eye(len(vector))
65
+ v = vector[np.newaxis]
66
+ denominator = np.matmul(v, v.T)
67
+ numerator = np.matmul(v.T, v)
68
+ rot_mat = identity_mat - (2 * numerator / denominator)
69
+ return rot_mat
70
+
71
+
72
+ def rotate_toward(initial_vector, final_vector, other_vectors, degrees: float = 5):
73
+ """Rotate other_vectors (with the centre at initial_vector) towards final_vector by an angle degrees.
74
+
75
+ Parameters
76
+ ----------
77
+ initial_vector : np.ndarray
78
+ Centre of the vectors to be rotated.
79
+ final_vector : np.ndarray
80
+ The final position of the center of other_vectors.
81
+ other_vectors : np.ndarray
82
+ The array of vectors to be rotated
83
+ degrees : float, optional
84
+ The amount of rotation (the default is 5)
85
+
86
+ Returns:
87
+ -------
88
+ rotated_vectors : np.ndarray
89
+ The rotated vectors
90
+ reached: bool
91
+ True if final_vector has been reached
92
+ """
93
+ final_vector = normalize(final_vector)
94
+ initial_vector = normalize(initial_vector)
95
+ cos_phi = np.dot(initial_vector, final_vector)
96
+ theta = degrees * np.pi / 180
97
+ cos_theta = np.cos(theta)
98
+ phi = np.arccos(cos_phi)
99
+ if phi < theta:
100
+ return (rotate(initial_vector, final_vector, other_vectors), True)
101
+ cos_phi_theta = np.cos(phi - theta)
102
+ A = np.asarray([[cos_phi, 1], [1, cos_phi]])
103
+ B = np.asarray([cos_phi_theta, cos_theta])
104
+ x = np.linalg.solve(A, B)
105
+ rotated_vector = x[0] * initial_vector + x[1] * final_vector
106
+ return (rotate(initial_vector, rotated_vector, other_vectors), False)
107
+
108
+
109
+ def approx_lattice_resolution(number_of_vectors: int, num_dims: int) -> int:
110
+ """
111
+ Approximate the lattice resolution based on the number of vectors and dimensions.
112
+
113
+ Args:
114
+ number_of_vectors (int): Desired number of reference vectors.
115
+ num_dims (int): Number of objectives (dimensions).
116
+
117
+ Returns:
118
+ int: The smallest lattice resolution that produces more than the desired number of vectors.
119
+ """
120
+ temp_lattice_resolution = 0
121
+ while True:
122
+ temp_lattice_resolution += 1
123
+ temp_number_of_vectors = comb(
124
+ temp_lattice_resolution + num_dims - 1,
125
+ num_dims - 1,
126
+ exact=True,
127
+ )
128
+ if temp_number_of_vectors > number_of_vectors:
129
+ break
130
+ return temp_lattice_resolution - 1
131
+
132
+
133
+ def create_simplex(
134
+ number_of_objectives: int,
135
+ lattice_resolution: int = None,
136
+ number_of_vectors: int = None,
137
+ ) -> np.ndarray:
138
+ """
139
+ Create reference vectors using the simplex lattice design.
140
+
141
+ Args:
142
+ number_of_objectives (int): Number of objectives (dimensions).
143
+ lattice_resolution (int, optional): Lattice resolution to use. If None, will be determined from number_of_vectors.
144
+ number_of_vectors (int, optional): Desired number of reference vectors. Used if lattice_resolution is None.
145
+
146
+ Returns:
147
+ np.ndarray: Array of normalized reference vectors.
148
+
149
+ Raises:
150
+ ValueError: If both lattice_resolution and number_of_vectors are None.
151
+ """
152
+ if lattice_resolution is None and number_of_vectors is None:
153
+ raise ValueError(
154
+ "Either lattice resolution or number of vectors must be specified."
155
+ )
156
+
157
+ if lattice_resolution is None:
158
+ lattice_resolution = approx_lattice_resolution(
159
+ number_of_vectors, number_of_objectives
160
+ )
161
+
162
+ number_of_vectors = comb(
163
+ lattice_resolution + number_of_objectives - 1,
164
+ number_of_objectives - 1,
165
+ exact=True,
166
+ )
167
+
168
+ temp1 = range(1, number_of_objectives + lattice_resolution)
169
+ temp1 = np.array(list(combinations(temp1, number_of_objectives - 1)))
170
+ temp2 = np.array([range(number_of_objectives - 1)] * number_of_vectors)
171
+ temp = temp1 - temp2 - 1
172
+ weight = np.zeros((number_of_vectors, number_of_objectives), dtype=int)
173
+ weight[:, 0] = temp[:, 0]
174
+ for i in range(1, number_of_objectives - 1):
175
+ weight[:, i] = temp[:, i] - temp[:, i - 1]
176
+ weight[:, -1] = lattice_resolution - temp[:, -1]
177
+ values = weight / lattice_resolution
178
+ return normalize(values)
179
+
180
+
181
+ def normalize(values: np.ndarray) -> np.ndarray:
182
+ """
183
+ Normalize a set of vectors to unit length (project onto the unit hypersphere).
184
+
185
+ Args:
186
+ values (np.ndarray): Array of vectors to normalize.
187
+
188
+ Returns:
189
+ np.ndarray: Normalized vectors.
190
+ """
191
+ norm_2 = np.linalg.norm(values, axis=1).reshape(-1, 1)
192
+ norm_2[norm_2 == 0] = np.finfo(float).eps
193
+ values = np.divide(values, norm_2)
194
+ return values
195
+
196
+
197
+ def neighbouring_angles(values: np.ndarray) -> np.ndarray:
198
+ """
199
+ Calculate the angles to the nearest neighbor for each reference vector.
200
+
201
+ Args:
202
+ values (np.ndarray): Array of normalized reference vectors.
203
+
204
+ Returns:
205
+ np.ndarray: Array of angles (in radians) to the nearest neighbor for each vector.
206
+ """
207
+ cosvv = np.dot(values, values.transpose())
208
+ cosvv.sort(axis=1)
209
+ cosvv = np.flip(cosvv, 1)
210
+ cosvv[cosvv > 1] = 1
211
+ acosvv = np.arccos(cosvv[:, 1])
212
+ return acosvv
213
+
214
+
215
+ def add_edge_vectors(values: np.ndarray) -> np.ndarray:
216
+ """
217
+ Add edge (axis-aligned) vectors to the set of reference vectors.
218
+
219
+ This ensures that each axis direction is represented in the set.
220
+
221
+ Args:
222
+ values (np.ndarray): Array of reference vectors.
223
+
224
+ Returns:
225
+ np.ndarray: Array of reference vectors with edge vectors added and normalized.
226
+ """
227
+ edge_vectors = np.eye(values.shape[1])
228
+ values = np.vstack([values, edge_vectors])
229
+ return normalize(values)