desdeo 2.0.0__py3-none-any.whl → 2.1.1__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/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/__init__.py +6 -6
- desdeo/api/app.py +38 -28
- desdeo/api/config.py +65 -44
- desdeo/api/config.toml +23 -12
- desdeo/api/db.py +10 -8
- desdeo/api/db_init.py +12 -6
- desdeo/api/models/__init__.py +220 -20
- desdeo/api/models/archive.py +16 -27
- 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 +44 -6
- desdeo/api/models/problem.py +274 -64
- desdeo/api/models/session.py +4 -1
- desdeo/api/models/state.py +419 -52
- desdeo/api/models/user.py +7 -6
- desdeo/api/models/utopia.py +25 -0
- desdeo/api/routers/_EMO.backup +309 -0
- desdeo/api/routers/_NIMBUS.py +6 -3
- 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 +201 -4
- desdeo/api/routers/reference_point_method.py +20 -44
- desdeo/api/routers/session.py +50 -26
- desdeo/api/routers/user_authentication.py +180 -26
- desdeo/api/routers/utils.py +187 -0
- desdeo/api/routers/utopia.py +230 -0
- desdeo/api/schema.py +10 -4
- desdeo/api/tests/conftest.py +94 -2
- desdeo/api/tests/test_enautilus.py +330 -0
- desdeo/api/tests/test_models.py +550 -72
- desdeo/api/tests/test_routes.py +902 -43
- desdeo/api/utils/_database.py +263 -0
- desdeo/api/utils/database.py +28 -266
- desdeo/api/utils/emo_database.py +40 -0
- desdeo/core.py +7 -0
- desdeo/emo/__init__.py +154 -24
- desdeo/emo/hooks/archivers.py +18 -2
- desdeo/emo/methods/EAs.py +128 -5
- desdeo/emo/methods/bases.py +9 -56
- desdeo/emo/methods/templates.py +111 -0
- desdeo/emo/operators/crossover.py +544 -42
- desdeo/emo/operators/evaluator.py +10 -14
- desdeo/emo/operators/generator.py +127 -24
- desdeo/emo/operators/mutation.py +212 -41
- desdeo/emo/operators/scalar_selection.py +202 -0
- desdeo/emo/operators/selection.py +956 -214
- desdeo/emo/operators/termination.py +124 -16
- 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/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 +23 -1
- desdeo/mcdm/enautilus.py +338 -0
- desdeo/mcdm/gnimbus.py +484 -0
- desdeo/mcdm/nautilus_navigator.py +7 -6
- desdeo/mcdm/reference_point_method.py +70 -0
- desdeo/problem/__init__.py +16 -11
- desdeo/problem/evaluator.py +4 -5
- 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 +37 -12
- desdeo/problem/infix_parser.py +1 -16
- desdeo/problem/json_parser.py +7 -11
- desdeo/problem/pyomo_evaluator.py +25 -6
- desdeo/problem/schema.py +73 -55
- desdeo/problem/simulator_evaluator.py +65 -15
- desdeo/problem/testproblems/__init__.py +26 -11
- desdeo/problem/testproblems/benchmarks_server.py +120 -0
- desdeo/problem/testproblems/cake_problem.py +185 -0
- desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
- desdeo/problem/testproblems/forest_problem.py +77 -69
- desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
- desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
- desdeo/problem/testproblems/single_objective.py +289 -0
- desdeo/problem/testproblems/zdt_problem.py +4 -1
- desdeo/problem/utils.py +1 -1
- desdeo/tools/__init__.py +39 -21
- desdeo/tools/desc_gen.py +22 -0
- desdeo/tools/generics.py +22 -2
- desdeo/tools/group_scalarization.py +3090 -0
- desdeo/tools/indicators_binary.py +107 -1
- desdeo/tools/indicators_unary.py +3 -16
- desdeo/tools/message.py +33 -2
- desdeo/tools/non_dominated_sorting.py +4 -3
- desdeo/tools/patterns.py +9 -7
- desdeo/tools/pyomo_solver_interfaces.py +49 -36
- desdeo/tools/reference_vectors.py +118 -351
- desdeo/tools/scalarization.py +340 -1413
- desdeo/tools/score_bands.py +491 -328
- desdeo/tools/utils.py +117 -49
- desdeo/tools/visualizations.py +67 -0
- desdeo/utopia_stuff/utopia_problem.py +1 -1
- desdeo/utopia_stuff/utopia_problem_old.py +1 -1
- {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/METADATA +47 -30
- desdeo-2.1.1.dist-info/RECORD +180 -0
- {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/WHEEL +1 -1
- desdeo-2.0.0.dist-info/RECORD +0 -120
- /desdeo/api/utils/{logger.py → _logger.py} +0 -0
- {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info/licenses}/LICENSE +0 -0
desdeo/mcdm/gnimbus.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""Functions related to the GNIMBUS method.
|
|
2
|
+
|
|
3
|
+
References:
|
|
4
|
+
TBA
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from desdeo.gdm.gdmtools import agg_aspbounds, dict_of_rps_to_list_of_rps, scale_delta
|
|
12
|
+
from desdeo.gdm.voting_rules import plurality_rule
|
|
13
|
+
from desdeo.mcdm.nimbus import infer_classifications, solve_sub_problems
|
|
14
|
+
from desdeo.problem import (
|
|
15
|
+
Problem,
|
|
16
|
+
)
|
|
17
|
+
from desdeo.tools import (
|
|
18
|
+
BaseSolver,
|
|
19
|
+
SolverOptions,
|
|
20
|
+
SolverResults,
|
|
21
|
+
GurobipySolver,
|
|
22
|
+
add_group_asf,
|
|
23
|
+
add_group_asf_agg,
|
|
24
|
+
add_group_asf_agg_diff,
|
|
25
|
+
add_group_asf_diff,
|
|
26
|
+
add_group_guess,
|
|
27
|
+
add_group_guess_agg,
|
|
28
|
+
add_group_guess_agg_diff,
|
|
29
|
+
add_group_guess_diff,
|
|
30
|
+
add_group_nimbus,
|
|
31
|
+
add_group_nimbus_compromise,
|
|
32
|
+
add_group_nimbus_compromise_diff,
|
|
33
|
+
add_group_nimbus_diff,
|
|
34
|
+
add_group_stom,
|
|
35
|
+
add_group_stom_agg,
|
|
36
|
+
add_group_stom_agg_diff,
|
|
37
|
+
add_group_stom_diff,
|
|
38
|
+
guess_best_solver,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GNIMBUSError(Exception):
|
|
43
|
+
"""Raised when an error with a NIMBUS method is encountered."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def voting_procedure(problem: Problem, solutions, votes_idxs: dict[str, int]) -> SolverResults:
|
|
47
|
+
"""More general procedure for GNIMBUS for any number of DMs.
|
|
48
|
+
TODO(@jpajasmaa): docs and cleaning up.
|
|
49
|
+
"""
|
|
50
|
+
# winner_idx = None
|
|
51
|
+
# call majority
|
|
52
|
+
""" general procedure does not apply majority rule
|
|
53
|
+
winner_idx = majority_rule(votes_idxs)
|
|
54
|
+
if winner_idx is not None:
|
|
55
|
+
print("Majority winner", winner_idx)
|
|
56
|
+
return solutions[winner_idx]
|
|
57
|
+
"""
|
|
58
|
+
# call plurality
|
|
59
|
+
winners = plurality_rule(votes_idxs)
|
|
60
|
+
print("winners")
|
|
61
|
+
if len(winners) == 1:
|
|
62
|
+
print("Plurality winner", winners[0])
|
|
63
|
+
return solutions[winners[0]] # need to unlist the winners list
|
|
64
|
+
|
|
65
|
+
print("TIE-breaking, select a solution randomly among top voted ones")
|
|
66
|
+
"""
|
|
67
|
+
# if two same solutions with same number of votes, call intermediate
|
|
68
|
+
# TODO:(@jpajasmaa) not perfect check as it is possible to have a problem that we can calculate more solutions
|
|
69
|
+
# AND discrete representation also.
|
|
70
|
+
if problem.discrete_representation is None:
|
|
71
|
+
wsol1, wsol2 = solutions[winners[0]].optimal_variables, solutions[winners[1]].optimal_variables
|
|
72
|
+
else:
|
|
73
|
+
wsol1, wsol2 = solutions[winners[0]].optimal_objectives, solutions[winners[1]].optimal_objectives
|
|
74
|
+
print("Finding intermediate solution between", wsol1, wsol2)
|
|
75
|
+
# return solve_intermediate_solutions_only_objs(problem, wsol1, wsol2, num_desired=3)
|
|
76
|
+
return solve_intermediate_solutions(problem, wsol1, wsol2, num_desired=1)[0]
|
|
77
|
+
"""
|
|
78
|
+
# n_of_sols = len(solutions)
|
|
79
|
+
rng = np.random.default_rng()
|
|
80
|
+
random_idx = rng.choice(winners)
|
|
81
|
+
return solutions[random_idx]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def infer_group_classifications(
|
|
85
|
+
problem: Problem,
|
|
86
|
+
current_objectives: dict[str, float],
|
|
87
|
+
reference_points: dict[str, dict[str, float]],
|
|
88
|
+
*,
|
|
89
|
+
silent: bool = True,
|
|
90
|
+
) -> dict[str, tuple[Literal["improve", "worsen", "conflict"], list[float]]]:
|
|
91
|
+
"""Infers group classification from the reference points given by the group.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
problem (Problem): the problem being solved
|
|
95
|
+
current_objectives (dict[str, float]): objective values at the current iteration
|
|
96
|
+
reference_points (dict[str, dict[str, float]]): The reference points given by the group.
|
|
97
|
+
The keys of the outer dict are the decision makers and the keys of the inner dict are objective symbols.
|
|
98
|
+
silent (bool): If false, the classifications will be printed.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
GNIMBUSError: _description_
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
dict[str, tuple[str, list[float]]]: _description_
|
|
105
|
+
"""
|
|
106
|
+
for dm, reference_point in reference_points.items():
|
|
107
|
+
# for rp in reference_point:
|
|
108
|
+
if not all(obj.symbol in reference_point for obj in problem.objectives):
|
|
109
|
+
print(reference_point)
|
|
110
|
+
msg = (
|
|
111
|
+
f"The reference point {reference_point} of {dm} is missing entries "
|
|
112
|
+
"for one or more of the objective functions."
|
|
113
|
+
)
|
|
114
|
+
raise GNIMBUSError(msg)
|
|
115
|
+
|
|
116
|
+
group_classifications = {}
|
|
117
|
+
for obj in problem.objectives:
|
|
118
|
+
# maximization
|
|
119
|
+
if obj.maximize and all(
|
|
120
|
+
(
|
|
121
|
+
reference_points[dm][obj.symbol] >= current_objectives[obj.symbol]
|
|
122
|
+
or np.isclose(reference_points[dm][obj.symbol], current_objectives[obj.symbol])
|
|
123
|
+
)
|
|
124
|
+
for dm in reference_points
|
|
125
|
+
):
|
|
126
|
+
classify = "improve"
|
|
127
|
+
elif obj.maximize and all(
|
|
128
|
+
reference_points[dm][obj.symbol] < current_objectives[obj.symbol] for dm in reference_points
|
|
129
|
+
):
|
|
130
|
+
classify = "worsen"
|
|
131
|
+
# minimization
|
|
132
|
+
elif (not obj.maximize) and all(
|
|
133
|
+
(
|
|
134
|
+
reference_points[dm][obj.symbol] <= current_objectives[obj.symbol]
|
|
135
|
+
or np.isclose(reference_points[dm][obj.symbol], current_objectives[obj.symbol])
|
|
136
|
+
)
|
|
137
|
+
for dm in reference_points
|
|
138
|
+
):
|
|
139
|
+
classify = "improve"
|
|
140
|
+
elif (not obj.maximize) and all(
|
|
141
|
+
reference_points[dm][obj.symbol] > current_objectives[obj.symbol] for dm in reference_points
|
|
142
|
+
):
|
|
143
|
+
classify = "worsen"
|
|
144
|
+
else:
|
|
145
|
+
classify = "conflict"
|
|
146
|
+
group_classifications[obj.symbol] = (classify, [reference_points[dm][obj.symbol] for dm in reference_points])
|
|
147
|
+
|
|
148
|
+
if not silent:
|
|
149
|
+
for symbol, value in group_classifications.items():
|
|
150
|
+
if value[0] == "improve":
|
|
151
|
+
print(f"The group wants to improve objective {symbol}")
|
|
152
|
+
print(value[1])
|
|
153
|
+
if value[0] == "worsen":
|
|
154
|
+
print(f"The group wants to worsen objective {symbol}")
|
|
155
|
+
print(value[1])
|
|
156
|
+
if value[0] == "conflict":
|
|
157
|
+
print(f"The group has conflicting views about objective {symbol}")
|
|
158
|
+
print(value[1])
|
|
159
|
+
|
|
160
|
+
return group_classifications
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def solve_group_sub_problems( # noqa: PLR0913, RET503
|
|
164
|
+
problem: Problem,
|
|
165
|
+
current_objectives: dict[str, float],
|
|
166
|
+
reference_points: dict[str, dict[str, float]],
|
|
167
|
+
phase: str,
|
|
168
|
+
scalarization_options: dict | None = None,
|
|
169
|
+
create_solver: BaseSolver | None = None,
|
|
170
|
+
solver_options: SolverOptions | None = None,
|
|
171
|
+
) -> list[SolverResults]:
|
|
172
|
+
r"""Solves a number of sub-problems as defined in the GNIMBUS methods.
|
|
173
|
+
|
|
174
|
+
TODO: update docs
|
|
175
|
+
|
|
176
|
+
Solves 4 scalarized problems utilizing different scalarization
|
|
177
|
+
functions. The scalarizations are based on the classification of a
|
|
178
|
+
solutions provided by a decision maker. The classifications
|
|
179
|
+
are represented by a reference point. Returns a number of new solutions
|
|
180
|
+
corresponding to the number of scalarization functions solved.
|
|
181
|
+
|
|
182
|
+
Solves the following scalarized problems corresponding
|
|
183
|
+
the the following scalarization functions:
|
|
184
|
+
|
|
185
|
+
1. the NIMBUS scalarization function,
|
|
186
|
+
2. the STOM scalarization function,
|
|
187
|
+
3. the achievement scalarizing function, and
|
|
188
|
+
4. the GUESS scalarization function.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
GNIMBUSError: the given problem has an undefined ideal or nadir point, or both.
|
|
192
|
+
GNIMBUSError: either the reference point of current objective functions value are
|
|
193
|
+
missing entries for one or more of the objective functions defined in the problem.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
problem(Problem): the problem being solved.
|
|
197
|
+
current_objectives(dict[str, float]): an objective dictionary with the objective functions values
|
|
198
|
+
the classifications have been given with respect to.
|
|
199
|
+
reference_points(dict[str, dict[str, float]]): A dictionary containing an objective dictionary with a reference point for each DM.
|
|
200
|
+
The classifications utilized in the sub problems are derived from
|
|
201
|
+
the reference points.
|
|
202
|
+
phase(str): The selected phase of the solution process. Must be one of "learning", "crp", "decision" or "compromise".
|
|
203
|
+
scalarization_options(dict | None, optional): optional kwargs passed to the scalarization function.
|
|
204
|
+
Defaults to None.
|
|
205
|
+
create_solver(CreateSolverType | None, optional): a function that given a problem, will return a solver.
|
|
206
|
+
If not given, an appropriate solver will be automatically determined based on the features of `problem`.
|
|
207
|
+
Defaults to None.
|
|
208
|
+
solver_options(SolverOptions | None, optional): optional options passed
|
|
209
|
+
to the `create_solver` routine. Ignored if `create_solver` is `None`.
|
|
210
|
+
Defaults to None.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
list[SolverResults]: a list of `SolverResults` objects. Contains as many elements
|
|
214
|
+
as defined in `num_desired`.
|
|
215
|
+
"""
|
|
216
|
+
if None in problem.get_ideal_point() or None in problem.get_nadir_point():
|
|
217
|
+
msg = "The given problem must have both an ideal and nadir point defined."
|
|
218
|
+
raise GNIMBUSError(msg)
|
|
219
|
+
|
|
220
|
+
DMs = reference_points.keys()
|
|
221
|
+
for dm in DMs:
|
|
222
|
+
reference_point = reference_points[dm]
|
|
223
|
+
# for rp in reference_point:
|
|
224
|
+
if not all(obj.symbol in reference_point for obj in problem.objectives):
|
|
225
|
+
print(reference_point)
|
|
226
|
+
msg = (
|
|
227
|
+
f"The reference point {reference_point} is missing entries for one or more of the objective functions."
|
|
228
|
+
)
|
|
229
|
+
raise GNIMBUSError(msg)
|
|
230
|
+
# check that at least one objective function is allowed to be improved and one is allowed to worsen
|
|
231
|
+
classifications = infer_classifications(problem, current_objectives, reference_point)
|
|
232
|
+
if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
|
|
233
|
+
classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
|
|
234
|
+
):
|
|
235
|
+
msg = (
|
|
236
|
+
f"The given classifications {classifications} should allow at least one objective function value "
|
|
237
|
+
"to improve and one to worsen."
|
|
238
|
+
)
|
|
239
|
+
raise GNIMBUSError(msg)
|
|
240
|
+
|
|
241
|
+
if not all(obj.symbol in current_objectives for obj in problem.objectives):
|
|
242
|
+
msg = f"The current point {current_objectives} is missing entries for one or more of the objective functions."
|
|
243
|
+
raise GNIMBUSError(msg)
|
|
244
|
+
|
|
245
|
+
init_solver = create_solver if create_solver is not None else guess_best_solver(problem)
|
|
246
|
+
if init_solver is GurobipySolver and not solver_options:
|
|
247
|
+
solver_options = {"OutputFlag": 0} #TODO: how does one want this to behave?
|
|
248
|
+
_solver_options = solver_options if solver_options is not None else None
|
|
249
|
+
# print("solver is ", init_solver)
|
|
250
|
+
|
|
251
|
+
solutions = []
|
|
252
|
+
classification_list = []
|
|
253
|
+
achievable_prefs = []
|
|
254
|
+
|
|
255
|
+
ind_sols = []
|
|
256
|
+
reference_points_list = dict_of_rps_to_list_of_rps(reference_points)
|
|
257
|
+
|
|
258
|
+
# Solve for individual solutions using nimbus scalarization.
|
|
259
|
+
for dm_rp in reference_points:
|
|
260
|
+
ind_sols.append(solve_sub_problems(
|
|
261
|
+
problem=problem,
|
|
262
|
+
current_objectives=current_objectives,
|
|
263
|
+
reference_point=reference_points[dm_rp],
|
|
264
|
+
num_desired=1,
|
|
265
|
+
scalarization_options=None,
|
|
266
|
+
solver=init_solver,
|
|
267
|
+
solver_options=_solver_options)[0],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
achievable_prefs = []
|
|
271
|
+
for q in range(len(reference_points)):
|
|
272
|
+
achievable_prefs.append(ind_sols[q].optimal_objectives)
|
|
273
|
+
|
|
274
|
+
agg_aspirations, agg_bounds = agg_aspbounds(achievable_prefs, problem)
|
|
275
|
+
delta = scale_delta(problem, d=1e-6)
|
|
276
|
+
|
|
277
|
+
if phase == "decision":
|
|
278
|
+
for dm_rp in reference_points:
|
|
279
|
+
classification_list.append(infer_classifications(problem, current_objectives, reference_points[dm_rp]))
|
|
280
|
+
gnimbus_scala = add_group_nimbus_diff if problem.is_twice_differentiable else add_group_nimbus
|
|
281
|
+
add_nimbus_sf = gnimbus_scala
|
|
282
|
+
|
|
283
|
+
problem_g_nimbus, gnimbus_target = add_nimbus_sf(
|
|
284
|
+
problem, "nimbus_sf", classification_list, current_objectives, agg_bounds, delta, **(scalarization_options or {})
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if _solver_options:
|
|
288
|
+
gnimbus_solver = init_solver(problem_g_nimbus, _solver_options) # type:ignore
|
|
289
|
+
else:
|
|
290
|
+
gnimbus_solver = init_solver(problem_g_nimbus) # type:ignore
|
|
291
|
+
|
|
292
|
+
solutions.append(gnimbus_solver.solve(gnimbus_target))
|
|
293
|
+
|
|
294
|
+
infer_group_classifications(problem, current_objectives, reference_points, silent=False)
|
|
295
|
+
|
|
296
|
+
return solutions
|
|
297
|
+
|
|
298
|
+
elif phase == "compromise":
|
|
299
|
+
# Run compromise phase with applying group-asf.
|
|
300
|
+
reference_points_list = dict_of_rps_to_list_of_rps(reference_points)
|
|
301
|
+
# solve ASF
|
|
302
|
+
add_asf = add_group_asf_diff if problem.is_twice_differentiable else add_group_asf
|
|
303
|
+
problem_w_asf, asf_target = add_asf(
|
|
304
|
+
problem, "asf", reference_points_list, agg_bounds, delta, **(scalarization_options or {})
|
|
305
|
+
)
|
|
306
|
+
if _solver_options:
|
|
307
|
+
asf_solver = init_solver(problem_w_asf, _solver_options) # type:ignore
|
|
308
|
+
else:
|
|
309
|
+
asf_solver = init_solver(problem_w_asf) # type:ignore
|
|
310
|
+
|
|
311
|
+
solutions.append(asf_solver.solve(asf_target))
|
|
312
|
+
|
|
313
|
+
return solutions
|
|
314
|
+
|
|
315
|
+
"""
|
|
316
|
+
classification_list = infer_group_classifications(problem, current_objectives, reference_points)
|
|
317
|
+
# All cool, the preference's are in a bit of a different format that other branches but works.
|
|
318
|
+
|
|
319
|
+
gnimbus_scala = add_group_nimbus_compromise_diff \
|
|
320
|
+
if problem.is_twice_differentiable else add_group_nimbus_compromise
|
|
321
|
+
add_nimbus_sf = gnimbus_scala
|
|
322
|
+
|
|
323
|
+
problem_g_nimbus, gnimbus_target = add_nimbus_sf(
|
|
324
|
+
problem, "nimbus_sf", classification_list, current_objectives, **(scalarization_options or {})
|
|
325
|
+
)
|
|
326
|
+
# ISSUE: makes the problem not twice differentiable, thus the initial solver doesn't work
|
|
327
|
+
# Also needed a little tweaking inside the scalarization functions.
|
|
328
|
+
|
|
329
|
+
if _solver_options:
|
|
330
|
+
gnimbus_solver = init_solver(problem_g_nimbus, _solver_options) # type:ignore
|
|
331
|
+
else:
|
|
332
|
+
gnimbus_solver = init_solver(problem_g_nimbus) # type:ignore
|
|
333
|
+
|
|
334
|
+
solutions.append(gnimbus_solver.solve(gnimbus_target))
|
|
335
|
+
|
|
336
|
+
infer_group_classifications(problem, current_objectives, reference_points, silent=False)
|
|
337
|
+
|
|
338
|
+
return solutions
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
elif phase == "learning":
|
|
342
|
+
reference_points_list = dict_of_rps_to_list_of_rps(reference_points)
|
|
343
|
+
|
|
344
|
+
# Add individual solutions
|
|
345
|
+
for i in range(len(ind_sols)):
|
|
346
|
+
solutions.append(ind_sols[i])
|
|
347
|
+
""" Group nimbus scalarization with delta and added hard_constraints """
|
|
348
|
+
classification_list = []
|
|
349
|
+
for dm_rp in reference_points:
|
|
350
|
+
classification_list.append(infer_classifications(problem, current_objectives, reference_points[dm_rp]))
|
|
351
|
+
print(classification_list)
|
|
352
|
+
gnimbus_scala = add_group_nimbus_diff if problem.is_twice_differentiable else add_group_nimbus
|
|
353
|
+
add_nimbus_sf = gnimbus_scala
|
|
354
|
+
|
|
355
|
+
problem_w_nimbus, nimbus_target = add_nimbus_sf(
|
|
356
|
+
problem,
|
|
357
|
+
"nimbus_sf",
|
|
358
|
+
classification_list,
|
|
359
|
+
current_objectives,
|
|
360
|
+
agg_bounds,
|
|
361
|
+
delta,
|
|
362
|
+
**(scalarization_options or {}),
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if _solver_options:
|
|
366
|
+
nimbus_solver = init_solver(problem_w_nimbus, _solver_options) # type:ignore
|
|
367
|
+
else:
|
|
368
|
+
nimbus_solver = init_solver(problem_w_nimbus) # type:ignore
|
|
369
|
+
|
|
370
|
+
solutions.append(nimbus_solver.solve(nimbus_target))
|
|
371
|
+
|
|
372
|
+
""" SOLVING Group Scals with scaled delta, original RPs and hard_constraints """
|
|
373
|
+
# solve STOM
|
|
374
|
+
add_stom_sf = add_group_stom_diff if problem.is_twice_differentiable else add_group_stom
|
|
375
|
+
problem_w_stom, stom_target = add_stom_sf(
|
|
376
|
+
problem, "stom_sf", reference_points_list, agg_bounds, delta, **(scalarization_options or {})
|
|
377
|
+
)
|
|
378
|
+
if _solver_options:
|
|
379
|
+
stom_solver = init_solver(problem_w_stom, _solver_options) # type:ignore
|
|
380
|
+
else:
|
|
381
|
+
stom_solver = init_solver(problem_w_stom) # type:ignore
|
|
382
|
+
|
|
383
|
+
solutions.append(stom_solver.solve(stom_target))
|
|
384
|
+
|
|
385
|
+
# solve ASF
|
|
386
|
+
add_asf = add_group_asf_diff if problem.is_twice_differentiable else add_group_asf
|
|
387
|
+
problem_w_asf, asf_target = add_asf(
|
|
388
|
+
problem, "asf", reference_points_list, agg_bounds, delta, **(scalarization_options or {})
|
|
389
|
+
)
|
|
390
|
+
if _solver_options:
|
|
391
|
+
asf_solver = init_solver(problem_w_asf, _solver_options) # type:ignore
|
|
392
|
+
else:
|
|
393
|
+
asf_solver = init_solver(problem_w_asf) # type:ignore
|
|
394
|
+
|
|
395
|
+
solutions.append(asf_solver.solve(asf_target))
|
|
396
|
+
|
|
397
|
+
# Solve GUESS
|
|
398
|
+
add_guess_sf = add_group_guess_diff if problem.is_twice_differentiable else add_group_guess
|
|
399
|
+
problem_w_guess, guess_target = add_guess_sf(
|
|
400
|
+
problem, "guess_sf", reference_points_list, agg_bounds, delta, **(scalarization_options or {})
|
|
401
|
+
)
|
|
402
|
+
if _solver_options:
|
|
403
|
+
guess_solver = init_solver(problem_w_guess, _solver_options) # type:ignore
|
|
404
|
+
else:
|
|
405
|
+
guess_solver = init_solver(problem_w_guess) # type:ignore
|
|
406
|
+
|
|
407
|
+
solutions.append(guess_solver.solve(guess_target))
|
|
408
|
+
|
|
409
|
+
infer_group_classifications(problem, current_objectives, reference_points, silent=False)
|
|
410
|
+
|
|
411
|
+
return solutions
|
|
412
|
+
|
|
413
|
+
else: # phase is concsensus reaching
|
|
414
|
+
# Add individual solutions
|
|
415
|
+
for i in range(len(ind_sols)):
|
|
416
|
+
solutions.append(ind_sols[i])
|
|
417
|
+
|
|
418
|
+
""" Group nimbus scalarization with delta and added hard_constraints """
|
|
419
|
+
classification_list = []
|
|
420
|
+
for dm_rp in reference_points:
|
|
421
|
+
print("RPS", reference_points[dm_rp])
|
|
422
|
+
classification_list.append(infer_classifications(problem, current_objectives, reference_points[dm_rp]))
|
|
423
|
+
print(classification_list)
|
|
424
|
+
gnimbus_scala = add_group_nimbus_diff if problem.is_twice_differentiable else add_group_nimbus
|
|
425
|
+
add_nimbus_sf = gnimbus_scala
|
|
426
|
+
|
|
427
|
+
problem_w_nimbus, nimbus_target = add_nimbus_sf(
|
|
428
|
+
problem,
|
|
429
|
+
"nimbus_sf",
|
|
430
|
+
classification_list,
|
|
431
|
+
current_objectives,
|
|
432
|
+
agg_bounds,
|
|
433
|
+
delta,
|
|
434
|
+
**(scalarization_options or {}),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if _solver_options:
|
|
438
|
+
nimbus_solver = init_solver(problem_w_nimbus, _solver_options) # type:ignore
|
|
439
|
+
else:
|
|
440
|
+
nimbus_solver = init_solver(problem_w_nimbus) # type:ignore
|
|
441
|
+
|
|
442
|
+
solutions.append(nimbus_solver.solve(nimbus_target))
|
|
443
|
+
|
|
444
|
+
""" SOLVING Group Scals with scaled delta, agg. aspirations and hard_constraints """
|
|
445
|
+
|
|
446
|
+
add_stom_sf2 = add_group_stom_agg_diff if problem.is_twice_differentiable else add_group_stom_agg
|
|
447
|
+
|
|
448
|
+
problem_g_stom, stomg_target = add_stom_sf2(
|
|
449
|
+
problem, "stom_sf2", agg_aspirations, agg_bounds, delta, **(scalarization_options or {})
|
|
450
|
+
)
|
|
451
|
+
if _solver_options:
|
|
452
|
+
stomg_solver = init_solver(problem_g_stom, _solver_options) # type:ignore
|
|
453
|
+
else:
|
|
454
|
+
stomg_solver = init_solver(problem_g_stom) # type:ignore
|
|
455
|
+
|
|
456
|
+
solutions.append(stomg_solver.solve(stomg_target))
|
|
457
|
+
|
|
458
|
+
add_asf2 = add_group_asf_agg_diff if problem.is_twice_differentiable else add_group_asf_agg
|
|
459
|
+
problem_g_asf, asfg_target = add_asf2(
|
|
460
|
+
problem, "asf2", agg_aspirations, agg_bounds, delta, **(scalarization_options or {})
|
|
461
|
+
)
|
|
462
|
+
if _solver_options:
|
|
463
|
+
asfg_solver = init_solver(problem_g_asf, _solver_options) # type:ignore
|
|
464
|
+
else:
|
|
465
|
+
asfg_solver = init_solver(problem_g_asf) # type:ignore
|
|
466
|
+
|
|
467
|
+
solutions.append(asfg_solver.solve(asfg_target))
|
|
468
|
+
|
|
469
|
+
add_guess_sf2 = add_group_guess_agg_diff if problem.is_twice_differentiable else add_group_guess_agg
|
|
470
|
+
|
|
471
|
+
problem_g_guess, guess2_target = add_guess_sf2(
|
|
472
|
+
problem, "guess_sf2", agg_aspirations, agg_bounds, delta, **(scalarization_options or {})
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if _solver_options:
|
|
476
|
+
guess2_solver = init_solver(problem_g_guess, _solver_options) # type:ignore
|
|
477
|
+
else:
|
|
478
|
+
guess2_solver = init_solver(problem_g_guess) # type:ignore
|
|
479
|
+
|
|
480
|
+
solutions.append(guess2_solver.solve(guess2_target))
|
|
481
|
+
|
|
482
|
+
infer_group_classifications(problem, current_objectives, reference_points, silent=False)
|
|
483
|
+
|
|
484
|
+
return solutions
|
|
@@ -303,8 +303,8 @@ def solve_reachable_solution(
|
|
|
303
303
|
# Note: We do not solve the global problem. Instead, we solve this constrained problem:
|
|
304
304
|
constraints = [
|
|
305
305
|
Constraint(
|
|
306
|
-
name=f"_const_{i+1}",
|
|
307
|
-
symbol=f"_const_{i+1}",
|
|
306
|
+
name=f"_const_{i + 1}",
|
|
307
|
+
symbol=f"_const_{i + 1}",
|
|
308
308
|
func=f"{obj.symbol}_min - {previous_nav_point[obj.symbol] * (-1 if obj.maximize else 1)}",
|
|
309
309
|
cons_type=ConstraintTypeEnum.LTE,
|
|
310
310
|
is_linear=obj.is_linear,
|
|
@@ -317,8 +317,8 @@ def solve_reachable_solution(
|
|
|
317
317
|
if bounds is not None:
|
|
318
318
|
constraints += [
|
|
319
319
|
Constraint(
|
|
320
|
-
name=f"_const_bound_{i+1}",
|
|
321
|
-
symbol=f"_const_bound_{i+1}",
|
|
320
|
+
name=f"_const_bound_{i + 1}",
|
|
321
|
+
symbol=f"_const_bound_{i + 1}",
|
|
322
322
|
cons_type=ConstraintTypeEnum.LTE,
|
|
323
323
|
func=f"{obj.symbol}_min - {bounds[obj.symbol] * (-1 if obj.maximize else 1)}",
|
|
324
324
|
is_linear=obj.is_linear,
|
|
@@ -416,8 +416,9 @@ def navigator_step( # NOQA: PLR0913
|
|
|
416
416
|
nav_point (dict): The current navigation point.
|
|
417
417
|
solver (BaseSolver | None, optional): The solver to use. Defaults to None.
|
|
418
418
|
reference_point (dict | None, optional): The reference point provided by the DM. Defaults to None, in which
|
|
419
|
-
|
|
420
|
-
|
|
419
|
+
case it is assumed that the DM has not changed their preference. The
|
|
420
|
+
algorithm uses the last reachable solution,
|
|
421
|
+
which must be provided in this case.
|
|
421
422
|
bounds (dict | None, optional): The bounds of the problem provided by the DM. Defaults to None.
|
|
422
423
|
reachable_solution (dict | None, optional): The previous reachable solution. Must only be provided if the DM
|
|
423
424
|
has not changed their preference. Defaults to None.
|
|
@@ -114,3 +114,73 @@ def rpm_solve_solutions(
|
|
|
114
114
|
|
|
115
115
|
# return the original solution and the solutions found with the perturbed reference points
|
|
116
116
|
return [initial_solution, *perturbed_solutions]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def rpm_intermediate_solutions( # noqa: PLR0913
|
|
120
|
+
problem: Problem,
|
|
121
|
+
solution_1: dict[str, float],
|
|
122
|
+
solution_2: dict[str, float],
|
|
123
|
+
num_desired: int,
|
|
124
|
+
scalarization_options: dict | None = None,
|
|
125
|
+
solver: BaseSolver | None = None,
|
|
126
|
+
solver_options: SolverOptions | None = None,
|
|
127
|
+
) -> list[SolverResults]:
|
|
128
|
+
"""Generates a desired number of intermediate solutions between two given solutions.
|
|
129
|
+
|
|
130
|
+
Generates a desires number of intermediate solutions given reference vectors.
|
|
131
|
+
The solutions are generated by taking n number of steps between the two reference points in the
|
|
132
|
+
objective space. The vectors corresponding to these values are then
|
|
133
|
+
utilized as reference points in the achievement scalarizing function. Solving the functions
|
|
134
|
+
for each reference point will a solution close the the projection of the reference point on the
|
|
135
|
+
Pareto optimal front of the problem. These solutions are then returned. Note that the
|
|
136
|
+
intermediate solutions are generated _between_ the two given reference points, this means the
|
|
137
|
+
returned solutions will not include solutions corresponding to the original reference points.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
problem (Problem): the problem being solved.
|
|
141
|
+
solution_1 (dict[str, VariableType]): the first of the reference points between which the intermediate
|
|
142
|
+
solutions are to be generated.
|
|
143
|
+
solution_2 (dict[str, VariableType]): the second of the reference points between which the intermediate
|
|
144
|
+
solutions are to be generated.
|
|
145
|
+
num_desired (int): the number of desired intermediate solutions to be generated. Must be at least `1`.
|
|
146
|
+
scalarization_options (dict | None, optional): optional kwargs passed to the scalarization function.
|
|
147
|
+
Defaults to None.
|
|
148
|
+
solver (BaseSolver | None, optional): solver used to solve the problem.
|
|
149
|
+
If not given, an appropriate solver will be automatically determined based on the features of `problem`.
|
|
150
|
+
Defaults to None.
|
|
151
|
+
solver_options (SolverOptions | None, optional): optional options passed
|
|
152
|
+
to the `solver`. Ignored if `solver` is `None`.
|
|
153
|
+
Defaults to None.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
list[SolverResults]: a list with the projected intermediate solutions as
|
|
157
|
+
`SolverResults` objects.
|
|
158
|
+
"""
|
|
159
|
+
if int(num_desired) < 1:
|
|
160
|
+
msg = f"The given number of desired intermediate ({num_desired=}) solutions must be at least 1."
|
|
161
|
+
raise ReferencePointError(msg)
|
|
162
|
+
|
|
163
|
+
init_solver = guess_best_solver(problem) if solver is None else solver
|
|
164
|
+
_solver_options = None if solver_options is None or solver is None else solver_options
|
|
165
|
+
|
|
166
|
+
# Find intermediate solutions by dividing the distance between the two Pareto points into num_desired+1 steps,
|
|
167
|
+
# calculate the solutions found in between the points, because we don't want to find the original solutions
|
|
168
|
+
intermediate_solutions = []
|
|
169
|
+
|
|
170
|
+
for i in range(num_desired):
|
|
171
|
+
rp = {
|
|
172
|
+
key: ((i + 1) * solution_1[key] + (num_desired - i) * solution_2[key]) / (num_desired + 1)
|
|
173
|
+
for key in solution_1.keys() # noqa: SIM118
|
|
174
|
+
}
|
|
175
|
+
# add scalarization
|
|
176
|
+
add_asf = add_asf_diff if problem.is_twice_differentiable else add_asf_nondiff
|
|
177
|
+
asf_problem, target = add_asf(problem, "target", rp, **(scalarization_options or {}))
|
|
178
|
+
|
|
179
|
+
solver = init_solver(asf_problem, _solver_options)
|
|
180
|
+
|
|
181
|
+
# solve and store results
|
|
182
|
+
result: SolverResults = solver.solve(target)
|
|
183
|
+
|
|
184
|
+
intermediate_solutions.append(result)
|
|
185
|
+
|
|
186
|
+
return intermediate_solutions
|
desdeo/problem/__init__.py
CHANGED
|
@@ -7,33 +7,36 @@ __all__ = [
|
|
|
7
7
|
"DiscreteRepresentation",
|
|
8
8
|
"Evaluator",
|
|
9
9
|
"ExtraFunction",
|
|
10
|
-
"flatten_variable_dict",
|
|
11
10
|
"FormatEnum",
|
|
12
11
|
"GurobipyEvaluator",
|
|
13
|
-
"get_nadir_dict",
|
|
14
|
-
"get_ideal_dict",
|
|
15
12
|
"InfixExpressionParser",
|
|
16
13
|
"MathParser",
|
|
17
|
-
"numpy_array_to_objective_dict",
|
|
18
|
-
"objective_dict_to_numpy_array",
|
|
19
14
|
"Objective",
|
|
20
15
|
"ObjectiveTypeEnum",
|
|
21
|
-
"Problem",
|
|
22
|
-
"PyomoEvaluator",
|
|
23
|
-
"SympyEvaluator",
|
|
24
|
-
"tensor_constant_from_dataframe",
|
|
25
16
|
"PolarsEvaluator",
|
|
26
17
|
"PolarsEvaluatorModesEnum",
|
|
18
|
+
"Problem",
|
|
19
|
+
"PyomoEvaluator",
|
|
27
20
|
"ScalarizationFunction",
|
|
28
21
|
"Simulator",
|
|
22
|
+
"SimulatorEvaluator",
|
|
23
|
+
"SympyEvaluator",
|
|
24
|
+
"Tensor",
|
|
29
25
|
"TensorConstant",
|
|
30
26
|
"TensorVariable",
|
|
31
|
-
"
|
|
27
|
+
"Url",
|
|
32
28
|
"Variable",
|
|
33
29
|
"VariableDimensionEnum",
|
|
34
30
|
"VariableDomainTypeEnum",
|
|
35
31
|
"VariableType",
|
|
36
32
|
"VariableTypeEnum",
|
|
33
|
+
"flatten_variable_dict",
|
|
34
|
+
"get_ideal_dict",
|
|
35
|
+
"get_nadir_dict",
|
|
36
|
+
"numpy_array_to_objective_dict",
|
|
37
|
+
"objective_dict_to_numpy_array",
|
|
38
|
+
"tensor_constant_from_dataframe",
|
|
39
|
+
"unflatten_variable_array",
|
|
37
40
|
"variable_dimension_enumerate",
|
|
38
41
|
]
|
|
39
42
|
|
|
@@ -59,14 +62,16 @@ from .schema import (
|
|
|
59
62
|
Problem,
|
|
60
63
|
ScalarizationFunction,
|
|
61
64
|
Simulator,
|
|
65
|
+
Tensor,
|
|
62
66
|
TensorConstant,
|
|
63
67
|
TensorVariable,
|
|
68
|
+
Url,
|
|
64
69
|
Variable,
|
|
65
70
|
VariableDomainTypeEnum,
|
|
66
71
|
VariableType,
|
|
67
72
|
VariableTypeEnum,
|
|
68
73
|
)
|
|
69
|
-
from .simulator_evaluator import
|
|
74
|
+
from .simulator_evaluator import SimulatorEvaluator
|
|
70
75
|
from .sympy_evaluator import SympyEvaluator
|
|
71
76
|
from .utils import (
|
|
72
77
|
flatten_variable_dict,
|