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.
- desdeo/__init__.py +8 -8
- desdeo/adm/ADMAfsar.py +551 -0
- desdeo/adm/ADMChen.py +414 -0
- desdeo/adm/BaseADM.py +119 -0
- desdeo/adm/__init__.py +11 -0
- desdeo/api/README.md +73 -0
- desdeo/api/__init__.py +15 -0
- desdeo/api/app.py +50 -0
- desdeo/api/config.py +90 -0
- desdeo/api/config.toml +64 -0
- desdeo/api/db.py +27 -0
- desdeo/api/db_init.py +85 -0
- desdeo/api/db_models.py +164 -0
- desdeo/api/malaga_db_init.py +27 -0
- desdeo/api/models/__init__.py +266 -0
- desdeo/api/models/archive.py +23 -0
- desdeo/api/models/emo.py +128 -0
- desdeo/api/models/enautilus.py +69 -0
- desdeo/api/models/gdm/gdm_aggregate.py +139 -0
- desdeo/api/models/gdm/gdm_base.py +69 -0
- desdeo/api/models/gdm/gdm_score_bands.py +114 -0
- desdeo/api/models/gdm/gnimbus.py +138 -0
- desdeo/api/models/generic.py +104 -0
- desdeo/api/models/generic_states.py +401 -0
- desdeo/api/models/nimbus.py +158 -0
- desdeo/api/models/preference.py +128 -0
- desdeo/api/models/problem.py +717 -0
- desdeo/api/models/reference_point_method.py +18 -0
- desdeo/api/models/session.py +49 -0
- desdeo/api/models/state.py +463 -0
- desdeo/api/models/user.py +52 -0
- desdeo/api/models/utopia.py +25 -0
- desdeo/api/routers/_EMO.backup +309 -0
- desdeo/api/routers/_NAUTILUS.py +245 -0
- desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
- desdeo/api/routers/_NIMBUS.py +765 -0
- desdeo/api/routers/__init__.py +5 -0
- desdeo/api/routers/emo.py +497 -0
- desdeo/api/routers/enautilus.py +237 -0
- desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
- desdeo/api/routers/gdm/gdm_base.py +420 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
- desdeo/api/routers/generic.py +233 -0
- desdeo/api/routers/nimbus.py +705 -0
- desdeo/api/routers/problem.py +307 -0
- desdeo/api/routers/reference_point_method.py +93 -0
- desdeo/api/routers/session.py +100 -0
- desdeo/api/routers/test.py +16 -0
- desdeo/api/routers/user_authentication.py +520 -0
- desdeo/api/routers/utils.py +187 -0
- desdeo/api/routers/utopia.py +230 -0
- desdeo/api/schema.py +100 -0
- desdeo/api/tests/__init__.py +0 -0
- desdeo/api/tests/conftest.py +151 -0
- desdeo/api/tests/test_enautilus.py +330 -0
- desdeo/api/tests/test_models.py +1179 -0
- desdeo/api/tests/test_routes.py +1075 -0
- desdeo/api/utils/_database.py +263 -0
- desdeo/api/utils/_logger.py +29 -0
- desdeo/api/utils/database.py +36 -0
- desdeo/api/utils/emo_database.py +40 -0
- desdeo/core.py +34 -0
- desdeo/emo/__init__.py +159 -0
- desdeo/emo/hooks/archivers.py +188 -0
- desdeo/emo/methods/EAs.py +541 -0
- desdeo/emo/methods/__init__.py +0 -0
- desdeo/emo/methods/bases.py +12 -0
- desdeo/emo/methods/templates.py +111 -0
- desdeo/emo/operators/__init__.py +1 -0
- desdeo/emo/operators/crossover.py +1282 -0
- desdeo/emo/operators/evaluator.py +114 -0
- desdeo/emo/operators/generator.py +459 -0
- desdeo/emo/operators/mutation.py +1224 -0
- desdeo/emo/operators/scalar_selection.py +202 -0
- desdeo/emo/operators/selection.py +1778 -0
- desdeo/emo/operators/termination.py +286 -0
- desdeo/emo/options/__init__.py +108 -0
- desdeo/emo/options/algorithms.py +435 -0
- desdeo/emo/options/crossover.py +164 -0
- desdeo/emo/options/generator.py +131 -0
- desdeo/emo/options/mutation.py +260 -0
- desdeo/emo/options/repair.py +61 -0
- desdeo/emo/options/scalar_selection.py +66 -0
- desdeo/emo/options/selection.py +127 -0
- desdeo/emo/options/templates.py +383 -0
- desdeo/emo/options/termination.py +143 -0
- desdeo/explanations/__init__.py +6 -0
- desdeo/explanations/explainer.py +100 -0
- desdeo/explanations/utils.py +90 -0
- desdeo/gdm/__init__.py +22 -0
- desdeo/gdm/gdmtools.py +45 -0
- desdeo/gdm/score_bands.py +114 -0
- desdeo/gdm/voting_rules.py +50 -0
- desdeo/mcdm/__init__.py +41 -0
- desdeo/mcdm/enautilus.py +338 -0
- desdeo/mcdm/gnimbus.py +484 -0
- desdeo/mcdm/nautili.py +345 -0
- desdeo/mcdm/nautilus.py +477 -0
- desdeo/mcdm/nautilus_navigator.py +656 -0
- desdeo/mcdm/nimbus.py +417 -0
- desdeo/mcdm/pareto_navigator.py +269 -0
- desdeo/mcdm/reference_point_method.py +186 -0
- desdeo/problem/__init__.py +83 -0
- desdeo/problem/evaluator.py +561 -0
- desdeo/problem/external/__init__.py +18 -0
- desdeo/problem/external/core.py +356 -0
- desdeo/problem/external/pymoo_provider.py +266 -0
- desdeo/problem/external/runtime.py +44 -0
- desdeo/problem/gurobipy_evaluator.py +562 -0
- desdeo/problem/infix_parser.py +341 -0
- desdeo/problem/json_parser.py +944 -0
- desdeo/problem/pyomo_evaluator.py +487 -0
- desdeo/problem/schema.py +1829 -0
- desdeo/problem/simulator_evaluator.py +348 -0
- desdeo/problem/sympy_evaluator.py +244 -0
- desdeo/problem/testproblems/__init__.py +88 -0
- desdeo/problem/testproblems/benchmarks_server.py +120 -0
- desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
- desdeo/problem/testproblems/cake_problem.py +185 -0
- desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
- desdeo/problem/testproblems/dtlz2_problem.py +102 -0
- desdeo/problem/testproblems/forest_problem.py +283 -0
- desdeo/problem/testproblems/knapsack_problem.py +163 -0
- desdeo/problem/testproblems/mcwb_problem.py +831 -0
- desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
- desdeo/problem/testproblems/momip_problem.py +172 -0
- desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
- desdeo/problem/testproblems/nimbus_problem.py +143 -0
- desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
- desdeo/problem/testproblems/re_problem.py +492 -0
- desdeo/problem/testproblems/river_pollution_problems.py +440 -0
- desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
- desdeo/problem/testproblems/simple_problem.py +351 -0
- desdeo/problem/testproblems/simulator_problem.py +92 -0
- desdeo/problem/testproblems/single_objective.py +289 -0
- desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
- desdeo/problem/testproblems/zdt_problem.py +274 -0
- desdeo/problem/utils.py +245 -0
- desdeo/tools/GenerateReferencePoints.py +181 -0
- desdeo/tools/__init__.py +120 -0
- desdeo/tools/desc_gen.py +22 -0
- desdeo/tools/generics.py +165 -0
- desdeo/tools/group_scalarization.py +3090 -0
- desdeo/tools/gurobipy_solver_interfaces.py +258 -0
- desdeo/tools/indicators_binary.py +117 -0
- desdeo/tools/indicators_unary.py +362 -0
- desdeo/tools/interaction_schema.py +38 -0
- desdeo/tools/intersection.py +54 -0
- desdeo/tools/iterative_pareto_representer.py +99 -0
- desdeo/tools/message.py +265 -0
- desdeo/tools/ng_solver_interfaces.py +199 -0
- desdeo/tools/non_dominated_sorting.py +134 -0
- desdeo/tools/patterns.py +283 -0
- desdeo/tools/proximal_solver.py +99 -0
- desdeo/tools/pyomo_solver_interfaces.py +477 -0
- desdeo/tools/reference_vectors.py +229 -0
- desdeo/tools/scalarization.py +2065 -0
- desdeo/tools/scipy_solver_interfaces.py +454 -0
- desdeo/tools/score_bands.py +627 -0
- desdeo/tools/utils.py +388 -0
- desdeo/tools/visualizations.py +67 -0
- desdeo/utopia_stuff/__init__.py +0 -0
- desdeo/utopia_stuff/data/1.json +15 -0
- desdeo/utopia_stuff/data/2.json +13 -0
- desdeo/utopia_stuff/data/3.json +15 -0
- desdeo/utopia_stuff/data/4.json +17 -0
- desdeo/utopia_stuff/data/5.json +15 -0
- desdeo/utopia_stuff/from_json.py +40 -0
- desdeo/utopia_stuff/reinit_user.py +38 -0
- desdeo/utopia_stuff/utopia_db_init.py +212 -0
- desdeo/utopia_stuff/utopia_problem.py +403 -0
- desdeo/utopia_stuff/utopia_problem_old.py +415 -0
- desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
- desdeo-2.1.0.dist-info/METADATA +186 -0
- desdeo-2.1.0.dist-info/RECORD +180 -0
- {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
- desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
- desdeo-1.2.dist-info/METADATA +0 -16
- 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)
|