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
desdeo/mcdm/nautilus.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Functions related to the NAUTILUS 1/2 method are defined here.
|
|
2
|
+
|
|
3
|
+
Reference of the method:
|
|
4
|
+
|
|
5
|
+
TODO: update
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from warnings import warn
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from desdeo.mcdm.nautili import solve_reachable_bounds
|
|
14
|
+
from desdeo.mcdm.nautilus_navigator import (
|
|
15
|
+
calculate_distance_to_front,
|
|
16
|
+
calculate_navigation_point,
|
|
17
|
+
)
|
|
18
|
+
from desdeo.problem import (
|
|
19
|
+
Constraint,
|
|
20
|
+
ConstraintTypeEnum,
|
|
21
|
+
Problem,
|
|
22
|
+
get_ideal_dict,
|
|
23
|
+
get_nadir_dict,
|
|
24
|
+
)
|
|
25
|
+
from desdeo.tools.generics import BaseSolver, SolverResults
|
|
26
|
+
from desdeo.tools.scalarization import ( # create_asf, should be add_asf_nondiff probably
|
|
27
|
+
add_asf_generic_diff,
|
|
28
|
+
add_asf_generic_nondiff,
|
|
29
|
+
)
|
|
30
|
+
from desdeo.tools.utils import guess_best_solver
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# TODO: check if need all of these, eg. distance to front? and do I need to change some of them?
|
|
34
|
+
class NAUTILUS_Response(BaseModel): # NOQA: N801
|
|
35
|
+
"""The response of the NAUTILUS method."""
|
|
36
|
+
|
|
37
|
+
step_number: int = Field(description="The step number associted with this response.")
|
|
38
|
+
distance_to_front: float = Field(
|
|
39
|
+
description=(
|
|
40
|
+
"The distance travelled to the Pareto front. "
|
|
41
|
+
"The distance is a ratio of the distances between the nadir and navigation point, and "
|
|
42
|
+
"the nadir and the reachable objective vector. The distance is given in percentage."
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
preference: dict | None = Field(
|
|
46
|
+
description="The preference used in the step. For now assumed that it is a reference point"
|
|
47
|
+
)
|
|
48
|
+
# preference_method: dict | None = Field(description="The preference method used in the step.")
|
|
49
|
+
# improvement_direction: dict | None = Field(description="The improvement direction.")
|
|
50
|
+
navigation_point: dict = Field(description="The navigation point used in the step.")
|
|
51
|
+
reachable_solution: dict | None = Field(description="The reachable solution found in the step.")
|
|
52
|
+
reachable_bounds: dict = Field(description="The reachable bounds found in the step.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NautilusError(Exception):
|
|
56
|
+
"""Raised when an exception is encountered with procedures related to NAUTILUS."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def solve_reachable_solution(
|
|
60
|
+
problem: Problem,
|
|
61
|
+
weights: dict[str, float],
|
|
62
|
+
# improvement_direction: dict[str, float],
|
|
63
|
+
previous_nav_point: dict[str, float],
|
|
64
|
+
solver: BaseSolver | None = None,
|
|
65
|
+
) -> SolverResults:
|
|
66
|
+
"""Calculates the reachable solution on the Pareto optimal front.
|
|
67
|
+
|
|
68
|
+
For the calculation to make sense in the context of NAUTILUS, the reference point
|
|
69
|
+
should be bounded by the reachable bounds present at the navigation step the
|
|
70
|
+
reference point has been given.
|
|
71
|
+
|
|
72
|
+
In practice, the reachable solution is calculated by solving an achievement
|
|
73
|
+
scalarizing function.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
problem (Problem): the problem being solved.
|
|
77
|
+
preference (dict[str, float]): the weights defining the direction of improvement. Must be calculated
|
|
78
|
+
from the preference provided by the DM (weights, ranks, or reference point).
|
|
79
|
+
previous_nav_point (dict[str, float]): the previous navigation point. The reachable solution found
|
|
80
|
+
is always better than the previous navigation point.
|
|
81
|
+
solver (BaseSolver | None, optional): solver to solve the problem.
|
|
82
|
+
If None, then a solver is utilized bases on the problem's properties. Defaults to None.
|
|
83
|
+
bounds (dict[str, float] | None, optional): the bounds of the problem. Defaults to None.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
SolverResults: the results of the projection.
|
|
87
|
+
"""
|
|
88
|
+
# check solver
|
|
89
|
+
init_solver = guess_best_solver(problem) if solver is None else solver
|
|
90
|
+
|
|
91
|
+
# need to convert the preferences to preferential factors?
|
|
92
|
+
|
|
93
|
+
# create and add scalarization function
|
|
94
|
+
if problem.is_twice_differentiable:
|
|
95
|
+
# differentiable problem
|
|
96
|
+
problem_w_asf, target = add_asf_generic_diff(
|
|
97
|
+
problem,
|
|
98
|
+
symbol="asf",
|
|
99
|
+
reference_point=previous_nav_point,
|
|
100
|
+
weights=weights,
|
|
101
|
+
reference_point_aug=previous_nav_point,
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
# non-differentiable problem
|
|
105
|
+
problem_w_asf, target = add_asf_generic_nondiff(
|
|
106
|
+
problem,
|
|
107
|
+
symbol="asf",
|
|
108
|
+
reference_point=previous_nav_point,
|
|
109
|
+
weights=weights,
|
|
110
|
+
reference_point_aug=previous_nav_point,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Note: We do not solve the global problem. Instead, we solve this constrained problem:
|
|
114
|
+
problem_w_asf = problem_w_asf.add_constraints(
|
|
115
|
+
[
|
|
116
|
+
Constraint(
|
|
117
|
+
name=f"_const_{i+1}",
|
|
118
|
+
symbol=f"_const_{i+1}",
|
|
119
|
+
func=f"{obj.symbol}_min - {previous_nav_point[obj.symbol] * (-1 if obj.maximize else 1)}",
|
|
120
|
+
cons_type=ConstraintTypeEnum.LTE,
|
|
121
|
+
is_linear=obj.is_linear,
|
|
122
|
+
is_convex=obj.is_convex,
|
|
123
|
+
is_twice_differentiable=obj.is_twice_differentiable,
|
|
124
|
+
)
|
|
125
|
+
for i, obj in enumerate(problem_w_asf.objectives)
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# solve the problem
|
|
130
|
+
solver = init_solver(problem_w_asf)
|
|
131
|
+
return solver.solve(target)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# NAUTILUS initializer and steppers
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def nautilus_init(problem: Problem, solver: BaseSolver | None = None) -> NAUTILUS_Response:
|
|
138
|
+
"""Initializes the NAUTILUS method.
|
|
139
|
+
|
|
140
|
+
Creates the initial response of the method, which sets the navigation point to the nadir point
|
|
141
|
+
and the reachable bounds to the ideal and nadir points.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
problem (Problem): The problem to be solved.
|
|
145
|
+
solver (BaseSolver | None, optional): The solver to use. Defaults to None.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
NAUTILUS_Response: The initial response of the method.
|
|
149
|
+
"""
|
|
150
|
+
nav_point = get_nadir_dict(problem)
|
|
151
|
+
lower_bounds, upper_bounds = solve_reachable_bounds(problem, nav_point, solver=solver)
|
|
152
|
+
return NAUTILUS_Response(
|
|
153
|
+
distance_to_front=0,
|
|
154
|
+
navigation_point=nav_point,
|
|
155
|
+
reachable_bounds={"lower_bounds": lower_bounds, "upper_bounds": upper_bounds},
|
|
156
|
+
reachable_solution=None,
|
|
157
|
+
preference=None,
|
|
158
|
+
step_number=0,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def nautilus_step( # NOQA: PLR0913
|
|
163
|
+
problem: Problem,
|
|
164
|
+
steps_remaining: int,
|
|
165
|
+
step_number: int,
|
|
166
|
+
nav_point: dict,
|
|
167
|
+
solver: BaseSolver | None = None,
|
|
168
|
+
points: dict[str, float] | None = None,
|
|
169
|
+
ranks: dict[str, int] | None = None,
|
|
170
|
+
) -> NAUTILUS_Response:
|
|
171
|
+
"""Performs a step of the NAUTILUS method.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
problem (Problem): The problem to be solved.
|
|
175
|
+
steps_remaining (int): The number of steps remaining.
|
|
176
|
+
step_number (int): The current step number. Just used for the response.
|
|
177
|
+
nav_point (dict): The current navigation point.
|
|
178
|
+
solver (BaseSolver | None, optional): The solver to use. Defaults to None.
|
|
179
|
+
points (dict[str, float] | None, optional): The points of the objectives. Defaults to None.
|
|
180
|
+
ranks (dict[str, int] | None, optional): The ranks of the objectives. Defaults to None.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
NautilusError: If neither preference nor reachable_solution is provided.
|
|
184
|
+
NautilusError: If both preference and reachable_solution are provided.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
NAUTILUS_Response: The response of the method after the step.
|
|
188
|
+
"""
|
|
189
|
+
if points is None and ranks is None:
|
|
190
|
+
raise NautilusError("Either points or ranks must be provided.")
|
|
191
|
+
if points is not None and ranks is not None:
|
|
192
|
+
raise NautilusError("Both points and ranks cannot be provided.")
|
|
193
|
+
|
|
194
|
+
# get weights
|
|
195
|
+
if points is not None: # noqa: SIM108
|
|
196
|
+
weights = points_to_weights(points, problem)
|
|
197
|
+
else:
|
|
198
|
+
weights = ranks_to_weights(ranks, problem)
|
|
199
|
+
|
|
200
|
+
# calculate reachable solution (direction).
|
|
201
|
+
# This is inefficient as it is recalculated even if preferences do not change.
|
|
202
|
+
opt_result = solve_reachable_solution(problem, weights, nav_point, solver)
|
|
203
|
+
|
|
204
|
+
if not opt_result.success:
|
|
205
|
+
warn(message="The solver did not converge.", stacklevel=2)
|
|
206
|
+
|
|
207
|
+
reachable_point = opt_result.optimal_objectives
|
|
208
|
+
|
|
209
|
+
# update nav point
|
|
210
|
+
new_nav_point = calculate_navigation_point(problem, nav_point, reachable_point, steps_remaining)
|
|
211
|
+
|
|
212
|
+
# update_bounds
|
|
213
|
+
lower_bounds, upper_bounds = solve_reachable_bounds(problem, new_nav_point, solver)
|
|
214
|
+
|
|
215
|
+
distance = calculate_distance_to_front(problem, new_nav_point, reachable_point)
|
|
216
|
+
|
|
217
|
+
return NAUTILUS_Response(
|
|
218
|
+
step_number=step_number,
|
|
219
|
+
distance_to_front=distance,
|
|
220
|
+
navigation_point=new_nav_point,
|
|
221
|
+
reachable_solution=reachable_point,
|
|
222
|
+
preference=ranks if ranks is not None else points,
|
|
223
|
+
reachable_bounds={"lower_bounds": lower_bounds, "upper_bounds": upper_bounds},
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def __nautilus_all_steps(
|
|
228
|
+
problem: Problem,
|
|
229
|
+
steps_remaining: int,
|
|
230
|
+
preference: dict,
|
|
231
|
+
previous_responses: list[NAUTILUS_Response],
|
|
232
|
+
solver: BaseSolver | None = None,
|
|
233
|
+
):
|
|
234
|
+
"""Performs all steps of the NAUTILUS method.
|
|
235
|
+
|
|
236
|
+
NAUTILUS needs to be initialized before calling this function. Once initialized, this function performs all
|
|
237
|
+
steps of the method. However, this method need not start from the beginning. The method conducts "steps_remaining"
|
|
238
|
+
number of steps from the last navigation point. The last navigation point is taken from the last response in
|
|
239
|
+
"previous_responses" list. The first step in this algorithm always involves recalculating the reachable solution.
|
|
240
|
+
All subsequest steps are precalculated without recalculating the reachable solution, with the assumption that the
|
|
241
|
+
reference point has not changed. It is up to the user to only show the steps that the DM thinks they have taken.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
problem (Problem): The problem to be solved.
|
|
245
|
+
steps_remaining (int): The number of steps remaining.
|
|
246
|
+
preference (dict): The reference point provided by the DM.
|
|
247
|
+
bounds (dict): The bounds of the problem provided by the DM.
|
|
248
|
+
previous_responses (list[NAUTILUS_Response]): The previous responses of the method.
|
|
249
|
+
solver (BaseSolver | None, optional): The solver to use. Defaults to None, in which case the
|
|
250
|
+
algorithm will guess the best solver for the problem.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
list[NAUTILUS_Response]: The new responses of the method after all steps. Note, as only new responses are
|
|
254
|
+
returned, the length of the list is equal to "steps_remaining". The analyst should append these responses
|
|
255
|
+
to the "previous_responses" list to keep track of the entire process.
|
|
256
|
+
"""
|
|
257
|
+
responses: list[NAUTILUS_Response] = []
|
|
258
|
+
nav_point = previous_responses[-1].navigation_point
|
|
259
|
+
step_number = previous_responses[-1].step_number + 1
|
|
260
|
+
first_iteration = True
|
|
261
|
+
reachable_solution = dict
|
|
262
|
+
while steps_remaining > 0:
|
|
263
|
+
if first_iteration:
|
|
264
|
+
response = nautilus_step(
|
|
265
|
+
problem,
|
|
266
|
+
steps_remaining=steps_remaining,
|
|
267
|
+
step_number=step_number,
|
|
268
|
+
nav_point=nav_point,
|
|
269
|
+
preference=preference,
|
|
270
|
+
solver=solver,
|
|
271
|
+
)
|
|
272
|
+
first_iteration = False
|
|
273
|
+
else:
|
|
274
|
+
response = nautilus_step(
|
|
275
|
+
problem,
|
|
276
|
+
steps_remaining=steps_remaining,
|
|
277
|
+
step_number=step_number,
|
|
278
|
+
nav_point=nav_point,
|
|
279
|
+
reachable_solution=reachable_solution,
|
|
280
|
+
solver=solver,
|
|
281
|
+
)
|
|
282
|
+
response.preference = preference
|
|
283
|
+
responses.append(response)
|
|
284
|
+
reachable_solution = response.reachable_solution
|
|
285
|
+
nav_point = response.navigation_point
|
|
286
|
+
steps_remaining -= 1
|
|
287
|
+
step_number += 1
|
|
288
|
+
return responses
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# implement preferential factors for other preference types
|
|
292
|
+
def calculate_preferential_factors():
|
|
293
|
+
"""TODO: implement"""
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def step_back_index(responses: list[NAUTILUS_Response], step_number: int) -> int:
|
|
298
|
+
"""Find the index of the response with the given step number.
|
|
299
|
+
|
|
300
|
+
Note, multiple responses can have the same step
|
|
301
|
+
number. This may happen if the DM takes a step back. In this case, the latest response with the given step number
|
|
302
|
+
is returned. Note, as we precalculate all the responses, it is up to the analyst to show the steps that the DM
|
|
303
|
+
thinks they have taken. Without this, the DM may be confused. In the worst case, the DM may take a step "back to
|
|
304
|
+
the future".
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
responses (list[NAUTILUS_Response]): Responses returned by the NAUTILUS method.
|
|
308
|
+
step_number (int): The step number to go back to.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
int : The index of the response with the given step number.
|
|
312
|
+
"""
|
|
313
|
+
relevant_indices = [i for i, response in enumerate(responses) if response.step_number == step_number]
|
|
314
|
+
# Choose latest index
|
|
315
|
+
return relevant_indices[-1]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_current_path(all_responses: list[NAUTILUS_Response]) -> list[int]:
|
|
319
|
+
"""Get the path of the current responses.
|
|
320
|
+
|
|
321
|
+
All responses may contain steps that the DM has gone back on. This function returns the path of the current active
|
|
322
|
+
path being followed by the DM. The path is a list of indices of the responses in the "all_responses" list. Note that
|
|
323
|
+
the path includes all steps until reaching the Pareto front (or whatever the last response is). It is up to the
|
|
324
|
+
analyst/GUI to only show the steps that the DM has taken.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
all_responses (list[NAUTILUS_Response]): All responses returned by the NAUTILUS method.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
list[int]: The path of the current active responses.
|
|
331
|
+
"""
|
|
332
|
+
total_steps = all_responses[-1].step_number
|
|
333
|
+
current_index = len(all_responses) - 1
|
|
334
|
+
path: list[int] = [current_index]
|
|
335
|
+
total_steps -= 1
|
|
336
|
+
|
|
337
|
+
while total_steps >= 0:
|
|
338
|
+
found_step = False
|
|
339
|
+
while not found_step:
|
|
340
|
+
current_index -= 1
|
|
341
|
+
if all_responses[current_index].step_number == total_steps:
|
|
342
|
+
path.append(current_index)
|
|
343
|
+
found_step = True
|
|
344
|
+
total_steps -= 1
|
|
345
|
+
return list(reversed(path))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def ranks_to_weights(ranks: dict[str, int], problem: Problem) -> dict[str, float]:
|
|
349
|
+
"""Convert ranks to weights.
|
|
350
|
+
|
|
351
|
+
The ranks are converted to weights using the following formula:
|
|
352
|
+
weight = rank * (nadir - utopian). Note that this means that a lower rank is worse.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
ranks (dict[str, int]): The ranks of the objectives.
|
|
356
|
+
problem (Problem): The problem being solved.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
dict[str, float]: The weights calculated from the ranks.
|
|
360
|
+
"""
|
|
361
|
+
nadir = get_nadir_dict(problem)
|
|
362
|
+
ideal = get_ideal_dict(problem)
|
|
363
|
+
tol = 1e-10
|
|
364
|
+
weights = {}
|
|
365
|
+
for key, rank in ranks.items():
|
|
366
|
+
max_mult = [obj.maximize for obj in problem.objectives if obj.symbol == key][0]
|
|
367
|
+
max_mult = -1 if max_mult else 1
|
|
368
|
+
weights[key] = rank * (nadir[key] * max_mult - ideal[key] * max_mult + tol)
|
|
369
|
+
return weights
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def points_to_weights(points: dict[str, float], problem: Problem) -> dict[str, float]:
|
|
373
|
+
"""Convert points to weights.
|
|
374
|
+
|
|
375
|
+
The points are converted to weights using the following formula:
|
|
376
|
+
weight = point * (nadir - utopian).
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
points (dict[str, float]): The points of the objectives.
|
|
380
|
+
problem (Problem): The problem being solved.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
dict[str, float]: The weights calculated from the points.
|
|
384
|
+
"""
|
|
385
|
+
nadir = get_nadir_dict(problem)
|
|
386
|
+
ideal = get_ideal_dict(problem)
|
|
387
|
+
tol = 1e-6
|
|
388
|
+
weights = {}
|
|
389
|
+
check_sum = 0
|
|
390
|
+
for key, point in points.items():
|
|
391
|
+
max_mult = [obj.maximize for obj in problem.objectives if obj.symbol == key][0]
|
|
392
|
+
max_mult = -1 if max_mult else 1
|
|
393
|
+
weights[key] = point / 100 * (nadir[key] * max_mult - ideal[key] * max_mult + tol)
|
|
394
|
+
check_sum += point
|
|
395
|
+
if check_sum != 100:
|
|
396
|
+
raise ValueError(f"The sum of the points must be 100. The sum is {check_sum}.")
|
|
397
|
+
return weights
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
from desdeo.problem import binh_and_korn
|
|
402
|
+
|
|
403
|
+
problem = binh_and_korn()
|
|
404
|
+
|
|
405
|
+
# initialization
|
|
406
|
+
nav_point = get_nadir_dict(problem)
|
|
407
|
+
lower_bounds = get_ideal_dict(problem)
|
|
408
|
+
upper_bounds = get_nadir_dict(problem)
|
|
409
|
+
|
|
410
|
+
step = 1
|
|
411
|
+
steps_remaining = 100
|
|
412
|
+
|
|
413
|
+
# get reference point
|
|
414
|
+
ranks = {"f_1": 1, "f_2": 2}
|
|
415
|
+
weights = ranks_to_weights(ranks, problem)
|
|
416
|
+
|
|
417
|
+
# get ranking
|
|
418
|
+
# "preference_method": 1,
|
|
419
|
+
# "preference_info": np.array([2, 2, 1, 1]),
|
|
420
|
+
# preference = {"f_1": 100.0, "f_2": 8.0}
|
|
421
|
+
|
|
422
|
+
# calculate reachable solution (direction)
|
|
423
|
+
opt_result = solve_reachable_solution(problem, weights, nav_point)
|
|
424
|
+
|
|
425
|
+
assert opt_result.success
|
|
426
|
+
|
|
427
|
+
reachable_point = opt_result.optimal_objectives
|
|
428
|
+
|
|
429
|
+
# update nav point
|
|
430
|
+
nav_point = calculate_navigation_point(problem, nav_point, reachable_point, steps_remaining)
|
|
431
|
+
print(f"{nav_point=}")
|
|
432
|
+
|
|
433
|
+
# update_bounds
|
|
434
|
+
lower_bounds, upper_bounds = solve_reachable_bounds(problem, nav_point)
|
|
435
|
+
|
|
436
|
+
distance = calculate_distance_to_front(problem, nav_point, reachable_point)
|
|
437
|
+
|
|
438
|
+
step += 1
|
|
439
|
+
steps_remaining -= 1
|
|
440
|
+
|
|
441
|
+
# no new preference, reachable point (direction) stays the same
|
|
442
|
+
# update nav point
|
|
443
|
+
nav_point = calculate_navigation_point(problem, nav_point, reachable_point, steps_remaining)
|
|
444
|
+
print(f"{nav_point=}")
|
|
445
|
+
|
|
446
|
+
# update bounds
|
|
447
|
+
lower_bounds, upper_bounds = solve_reachable_bounds(problem, nav_point)
|
|
448
|
+
|
|
449
|
+
distance = calculate_distance_to_front(problem, nav_point, reachable_point)
|
|
450
|
+
|
|
451
|
+
step += 1
|
|
452
|
+
steps_remaining -= 1
|
|
453
|
+
|
|
454
|
+
# new reference point
|
|
455
|
+
points = {"f_1": 80, "f_2": 20} # Now f_1 is more important
|
|
456
|
+
weights = points_to_weights(points, problem)
|
|
457
|
+
|
|
458
|
+
# calculate reachable solution (direction)
|
|
459
|
+
opt_result = solve_reachable_solution(problem, weights, nav_point)
|
|
460
|
+
|
|
461
|
+
assert opt_result.success
|
|
462
|
+
|
|
463
|
+
reachable_point = opt_result.optimal_objectives
|
|
464
|
+
|
|
465
|
+
# update nav point
|
|
466
|
+
nav_point = calculate_navigation_point(problem, nav_point, reachable_point, steps_remaining)
|
|
467
|
+
print(f"{nav_point=}")
|
|
468
|
+
|
|
469
|
+
# update_bounds
|
|
470
|
+
lower_bounds, upper_bounds = solve_reachable_bounds(problem, nav_point)
|
|
471
|
+
|
|
472
|
+
distance = calculate_distance_to_front(problem, nav_point, reachable_point)
|
|
473
|
+
|
|
474
|
+
step += 1
|
|
475
|
+
steps_remaining -= 1
|
|
476
|
+
|
|
477
|
+
# etc...
|