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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Runtime which owns a general ProviderResolver singleton and exposes functions to manage it."""
|
|
2
|
+
|
|
3
|
+
from .core import Provider, ProviderRegistry, ProviderResolver
|
|
4
|
+
|
|
5
|
+
_registry = ProviderRegistry()
|
|
6
|
+
_resolver = ProviderResolver(_registry)
|
|
7
|
+
|
|
8
|
+
# if uri's of other type than 'desdeo://...' are to be supported, update this list
|
|
9
|
+
supported_schemes = ["desdeo"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_registry() -> ProviderRegistry:
|
|
13
|
+
"""Get the runtime registry."""
|
|
14
|
+
return _registry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_resolver() -> ProviderResolver:
|
|
18
|
+
"""Get the runtime provider resolver."""
|
|
19
|
+
return _resolver
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register_provider(name: str, provider: Provider, *, overwrite: bool = False, clear_cache: bool = True) -> None:
|
|
23
|
+
"""Register a provider to the current runtime resolver.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
name (str): name of the provider.
|
|
27
|
+
provider (Provider): the instance of the provider.
|
|
28
|
+
overwrite (bool, optional): should an existing provider with the same
|
|
29
|
+
name be overwritten, if it already exits? Defaults to False.
|
|
30
|
+
clear_cache (bool, optional): should the resolver's cache be cleared? Defaults to True.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
KeyError: if overwrite is 'False' and a provider with the given `name`
|
|
34
|
+
already exists in the register of the resolver.
|
|
35
|
+
"""
|
|
36
|
+
reg = get_registry()
|
|
37
|
+
|
|
38
|
+
if reg.has(name) and not overwrite:
|
|
39
|
+
raise KeyError(f"Provider {name!r} already registered")
|
|
40
|
+
|
|
41
|
+
reg.register(name, provider)
|
|
42
|
+
|
|
43
|
+
if clear_cache:
|
|
44
|
+
get_resolver().clear_caches()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Defines an evaluator compatible with the Problem JSON format and transforms it into a GurobipyModel."""
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from operator import eq as _eq
|
|
4
5
|
from operator import le as _le
|
|
5
|
-
import warnings
|
|
6
6
|
|
|
7
7
|
import gurobipy as gp
|
|
8
8
|
import numpy as np
|
|
@@ -18,8 +18,7 @@ from desdeo.problem.schema import (
|
|
|
18
18
|
TensorConstant,
|
|
19
19
|
TensorVariable,
|
|
20
20
|
Variable,
|
|
21
|
-
VariableTypeEnum
|
|
22
|
-
|
|
21
|
+
VariableTypeEnum,
|
|
23
22
|
)
|
|
24
23
|
|
|
25
24
|
|
|
@@ -125,8 +124,16 @@ class GurobipyEvaluator:
|
|
|
125
124
|
|
|
126
125
|
elif isinstance(var, TensorVariable):
|
|
127
126
|
# handle tensor variables, i.e., vectors etc..
|
|
128
|
-
lowerbounds =
|
|
129
|
-
|
|
127
|
+
lowerbounds = (
|
|
128
|
+
var.get_lowerbound_values()
|
|
129
|
+
if var.lowerbounds is not None
|
|
130
|
+
else np.full(var.shape, float("-inf")).tolist()
|
|
131
|
+
)
|
|
132
|
+
upperbounds = (
|
|
133
|
+
var.get_upperbound_values()
|
|
134
|
+
if var.upperbounds is not None
|
|
135
|
+
else np.full(var.shape, float("inf")).tolist()
|
|
136
|
+
)
|
|
130
137
|
|
|
131
138
|
# figure out the variable type
|
|
132
139
|
match var.variable_type:
|
|
@@ -143,13 +150,18 @@ class GurobipyEvaluator:
|
|
|
143
150
|
raise GurobipyEvaluatorError(msg)
|
|
144
151
|
|
|
145
152
|
# add the variable to the model
|
|
146
|
-
gvar = self.model.addMVar(
|
|
153
|
+
gvar = self.model.addMVar(
|
|
154
|
+
shape=tuple(var.shape),
|
|
155
|
+
lb=np.array(lowerbounds),
|
|
156
|
+
ub=np.array(upperbounds),
|
|
157
|
+
vtype=domain,
|
|
158
|
+
name=var.symbol,
|
|
159
|
+
)
|
|
147
160
|
# set the initial value, if one has been defined
|
|
148
161
|
if var.initial_values is not None:
|
|
149
162
|
gvar.setAttr("Start", np.array(var.get_initial_values()))
|
|
150
163
|
self.mvars[var.symbol] = gvar
|
|
151
164
|
|
|
152
|
-
|
|
153
165
|
# update the model before returning, so that other expressions can reference the variables
|
|
154
166
|
self.model.update()
|
|
155
167
|
|
|
@@ -413,8 +425,16 @@ class GurobipyEvaluator:
|
|
|
413
425
|
gvar.setAttr("Start", var.initial_value)
|
|
414
426
|
elif isinstance(var, TensorVariable):
|
|
415
427
|
# handle tensor variables, i.e., vectors etc..
|
|
416
|
-
lowerbounds =
|
|
417
|
-
|
|
428
|
+
lowerbounds = (
|
|
429
|
+
var.get_lowerbound_values()
|
|
430
|
+
if var.lowerbounds is not None
|
|
431
|
+
else np.full(var.shape, float("-inf")).tolist()
|
|
432
|
+
)
|
|
433
|
+
upperbounds = (
|
|
434
|
+
var.get_upperbound_values()
|
|
435
|
+
if var.upperbounds is not None
|
|
436
|
+
else np.full(var.shape, float("inf")).tolist()
|
|
437
|
+
)
|
|
418
438
|
|
|
419
439
|
# figure out the variable type
|
|
420
440
|
match var.variable_type:
|
|
@@ -431,7 +451,13 @@ class GurobipyEvaluator:
|
|
|
431
451
|
raise GurobipyEvaluatorError(msg)
|
|
432
452
|
|
|
433
453
|
# add the variable to the model
|
|
434
|
-
gvar = self.model.addMVar(
|
|
454
|
+
gvar = self.model.addMVar(
|
|
455
|
+
shape=tuple(var.shape),
|
|
456
|
+
lb=np.array(lowerbounds),
|
|
457
|
+
ub=np.array(upperbounds),
|
|
458
|
+
vtype=domain,
|
|
459
|
+
name=var.symbol,
|
|
460
|
+
)
|
|
435
461
|
# set the initial value, if one has been defined
|
|
436
462
|
if var.initial_values is not None:
|
|
437
463
|
gvar.setAttr("Start", np.array(var.get_initial_values()))
|
|
@@ -470,8 +496,7 @@ class GurobipyEvaluator:
|
|
|
470
496
|
expression = self.constants[name]
|
|
471
497
|
return expression
|
|
472
498
|
|
|
473
|
-
|
|
474
|
-
def get_values(self) -> dict[str, float | int | bool | list[float] | list[int]]: # noqa: C901
|
|
499
|
+
def get_values(self) -> dict[str, float | int | bool | list[float] | list[int]]:
|
|
475
500
|
"""Get the values from the Gurobipy Model in a dict.
|
|
476
501
|
|
|
477
502
|
The keys of the dict will be the symbols defined in the problem utilized to initialize the evaluator.
|
desdeo/problem/infix_parser.py
CHANGED
|
@@ -20,7 +20,7 @@ from pyparsing import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
# Enable Packrat for better performance in recursive parsing
|
|
23
|
-
ParserElement.
|
|
23
|
+
ParserElement.enable_packrat(None)
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class InfixExpressionParser:
|
|
@@ -324,18 +324,3 @@ class InfixExpressionParser:
|
|
|
324
324
|
# simple expressions, like 'x_1', are parsed into just a string after removing any extra
|
|
325
325
|
# brackets, so we add them back there in case it is needed
|
|
326
326
|
return expr if isinstance(expr, list) else [expr]
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if __name__ == "__main__":
|
|
330
|
-
# Parse and convert
|
|
331
|
-
test = "(x_1 - c_2) ** 2 + x_2 ** 2 - 25"
|
|
332
|
-
|
|
333
|
-
ohh_no = "['Add', ['Negate', ['Square', ['Subtract', 'x_1', 8]]], ['Negate', ['Square', ['Add', 'x_2', 3]]], 7.7]"
|
|
334
|
-
|
|
335
|
-
to_json_parser = InfixExpressionParser(target="MathJSON")
|
|
336
|
-
|
|
337
|
-
parsed_expression = to_json_parser.parse(test)
|
|
338
|
-
|
|
339
|
-
print(f"Expresion:\n{test}")
|
|
340
|
-
print(f"Parsed:\n{to_json_parser._pre_parse(test)}")
|
|
341
|
-
print(f"MathJSON:\n{parsed_expression}")
|
desdeo/problem/json_parser.py
CHANGED
|
@@ -35,7 +35,7 @@ class MathParser:
|
|
|
35
35
|
Currently only parses MathJSON to polars expressions. Pyomo WIP.
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
def __init__(self, to_format: FormatEnum = "polars"):
|
|
38
|
+
def __init__(self, to_format: FormatEnum = "polars"): # noqa: C901
|
|
39
39
|
"""Create a parser instance for parsing MathJSON notation into polars expressions.
|
|
40
40
|
|
|
41
41
|
Args:
|
|
@@ -135,7 +135,7 @@ class MathParser:
|
|
|
135
135
|
acc = acc.to_numpy()
|
|
136
136
|
x = x.to_numpy()
|
|
137
137
|
|
|
138
|
-
if len(acc.shape) == 2 and len(x.shape) == 2:
|
|
138
|
+
if len(acc.shape) == 2 and len(x.shape) == 2: # noqa: PLR2004
|
|
139
139
|
# Row vectors, just return the dot product, polars does not handle
|
|
140
140
|
# "column" vectors anyway
|
|
141
141
|
return pl.Series(values=np.einsum("ij,ij->i", acc, x, optimize=True))
|
|
@@ -373,7 +373,7 @@ class MathParser:
|
|
|
373
373
|
hasattr(x, "is_indexed")
|
|
374
374
|
and x.is_indexed()
|
|
375
375
|
and x.dim() > 0
|
|
376
|
-
and (not hasattr(y, "is_indexed") or not y.is_indexed() or y.is_indexed() and y.dim() == 0)
|
|
376
|
+
and (not hasattr(y, "is_indexed") or not y.is_indexed() or (y.is_indexed() and y.dim() == 0))
|
|
377
377
|
):
|
|
378
378
|
# x is a tensor, y is scalar
|
|
379
379
|
expr = pyomo.Expression(
|
|
@@ -385,7 +385,7 @@ class MathParser:
|
|
|
385
385
|
hasattr(y, "is_indexed")
|
|
386
386
|
and y.is_indexed()
|
|
387
387
|
and y.dim() > 0
|
|
388
|
-
and (not hasattr(x, "is_indexed") or not x.is_indexed() or x.is_indexed() and x.dim() == 0)
|
|
388
|
+
and (not hasattr(x, "is_indexed") or not x.is_indexed() or (x.is_indexed() and x.dim() == 0))
|
|
389
389
|
):
|
|
390
390
|
# y is a tensor, x is scalar
|
|
391
391
|
expr = pyomo.Expression(
|
|
@@ -534,16 +534,13 @@ class MathParser:
|
|
|
534
534
|
def _sympy_matmul(*args):
|
|
535
535
|
"""Sympy matrix multiplication."""
|
|
536
536
|
msg = (
|
|
537
|
-
"Matrix multiplication '@' has not been implemented for the Sympy parser yet."
|
|
538
|
-
" Feel free to contribute!"
|
|
537
|
+
"Matrix multiplication '@' has not been implemented for the Sympy parser yet. Feel free to contribute!"
|
|
539
538
|
)
|
|
540
539
|
raise NotImplementedError(msg)
|
|
541
540
|
|
|
542
541
|
def _sympy_summation(summand):
|
|
543
542
|
"""Sympy matrix summation."""
|
|
544
|
-
msg =
|
|
545
|
-
"Matrix summation 'Sum' has not been implemented for the Sympy parser yet." " Feel free to contribute!"
|
|
546
|
-
)
|
|
543
|
+
msg = "Matrix summation 'Sum' has not been implemented for the Sympy parser yet. Feel free to contribute!"
|
|
547
544
|
raise NotImplementedError(msg)
|
|
548
545
|
|
|
549
546
|
def _sympy_random_access(*args):
|
|
@@ -627,8 +624,7 @@ class MathParser:
|
|
|
627
624
|
|
|
628
625
|
return _sum(summand)
|
|
629
626
|
msg = (
|
|
630
|
-
"Matrix summation 'Sum' has not been implemented for the Gurobipy parser yet."
|
|
631
|
-
" Feel free to contribute!"
|
|
627
|
+
"Matrix summation 'Sum' has not been implemented for the Gurobipy parser yet. Feel free to contribute!"
|
|
632
628
|
)
|
|
633
629
|
raise NotImplementedError(msg)
|
|
634
630
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import itertools
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from operator import eq as _eq
|
|
6
|
-
from operator import le as
|
|
6
|
+
from operator import le as _python_le
|
|
7
7
|
|
|
8
8
|
import pyomo.environ as pyomo
|
|
9
9
|
|
|
@@ -19,6 +19,18 @@ from desdeo.problem.schema import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _le(expr, rhs, name):
|
|
23
|
+
# scalar → single constraint
|
|
24
|
+
if not expr.is_indexed():
|
|
25
|
+
tmp_expr = _python_le(expr, rhs)
|
|
26
|
+
return pyomo.Constraint(expr=tmp_expr, name=name)
|
|
27
|
+
|
|
28
|
+
# indexed → one row per index
|
|
29
|
+
tmp = pyomo.Constraint(expr.index_set(), rule=lambda m, *idx: expr[idx] <= rhs)
|
|
30
|
+
tmp.construct()
|
|
31
|
+
return tmp
|
|
32
|
+
|
|
33
|
+
|
|
22
34
|
class PyomoEvaluatorError(Exception):
|
|
23
35
|
"""Raised when an error within the PyomoEvaluator class is encountered."""
|
|
24
36
|
|
|
@@ -159,7 +171,7 @@ class PyomoEvaluator:
|
|
|
159
171
|
else:
|
|
160
172
|
initial_value = (
|
|
161
173
|
var.initial_value
|
|
162
|
-
if var.lowerbound is None
|
|
174
|
+
if var.lowerbound is None or var.upperbound is None
|
|
163
175
|
else (var.lowerbound + var.upperbound) / 2
|
|
164
176
|
)
|
|
165
177
|
|
|
@@ -321,16 +333,17 @@ class PyomoEvaluator:
|
|
|
321
333
|
match con_type := cons.cons_type:
|
|
322
334
|
case ConstraintTypeEnum.LTE:
|
|
323
335
|
# constraints in DESDEO are defined such that they must be less than zero
|
|
324
|
-
pyomo_expr = _le(pyomo_expr, 0)
|
|
336
|
+
pyomo_expr = _le(pyomo_expr, 0, cons.name)
|
|
325
337
|
case ConstraintTypeEnum.EQ:
|
|
326
338
|
pyomo_expr = _eq(pyomo_expr, 0)
|
|
327
339
|
case _:
|
|
328
340
|
msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
|
|
329
341
|
raise PyomoEvaluatorError(msg)
|
|
330
342
|
|
|
331
|
-
cons_expr = pyomo.Constraint(expr=pyomo_expr, name=cons.name)
|
|
343
|
+
# cons_expr = pyomo.Constraint(expr=pyomo_expr, name=cons.name)
|
|
332
344
|
|
|
333
|
-
setattr(model, cons.symbol,
|
|
345
|
+
setattr(model, cons.symbol, pyomo_expr)
|
|
346
|
+
# getattr(model, cons.symbol).construct()
|
|
334
347
|
|
|
335
348
|
return model
|
|
336
349
|
|
|
@@ -429,9 +442,15 @@ class PyomoEvaluator:
|
|
|
429
442
|
for extra in self.problem.extra_funcs:
|
|
430
443
|
result_dict[extra.symbol] = pyomo.value(getattr(self.model, extra.symbol))
|
|
431
444
|
|
|
445
|
+
# TODO: after implementing TensorConstraint, fix this
|
|
432
446
|
if self.problem.constraints is not None:
|
|
433
447
|
for const in self.problem.constraints:
|
|
434
|
-
|
|
448
|
+
obj = getattr(self.model, const.symbol)
|
|
449
|
+
|
|
450
|
+
if obj.is_indexed():
|
|
451
|
+
result_dict[const.symbol] = {k: pyomo.value(obj[k]) for k in obj}
|
|
452
|
+
else:
|
|
453
|
+
result_dict[const.symbol] = pyomo.value(obj)
|
|
435
454
|
|
|
436
455
|
if self.problem.scalarization_funcs is not None:
|
|
437
456
|
for scal in self.problem.scalarization_funcs:
|
desdeo/problem/schema.py
CHANGED
|
@@ -16,7 +16,7 @@ from collections.abc import Iterable
|
|
|
16
16
|
from enum import Enum
|
|
17
17
|
from itertools import product
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import TYPE_CHECKING, Annotated, Any, Literal,
|
|
19
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Self
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
from pydantic import (
|
|
@@ -61,13 +61,10 @@ def tensor_custom_error_validator(value: Any, handler: ValidatorFunctionWrapHand
|
|
|
61
61
|
raise PydanticCustomError("invalid tensor", "Input is not a valid tensor") from exc
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
Tensor =
|
|
65
|
-
"Tensor",
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
WrapValidator(tensor_custom_error_validator),
|
|
69
|
-
],
|
|
70
|
-
)
|
|
64
|
+
type Tensor = Annotated[
|
|
65
|
+
list["Tensor"] | list[VariableType] | VariableType | Literal["List"] | None,
|
|
66
|
+
WrapValidator(tensor_custom_error_validator),
|
|
67
|
+
]
|
|
71
68
|
|
|
72
69
|
|
|
73
70
|
def parse_infix_to_func(cls: "Problem", v: str | list) -> list:
|
|
@@ -260,7 +257,7 @@ class ObjectiveTypeEnum(str, Enum):
|
|
|
260
257
|
class Constant(BaseModel):
|
|
261
258
|
"""Model for a constant."""
|
|
262
259
|
|
|
263
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
260
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
264
261
|
|
|
265
262
|
name: str = Field(
|
|
266
263
|
description=(
|
|
@@ -284,7 +281,7 @@ class Constant(BaseModel):
|
|
|
284
281
|
class TensorConstant(BaseModel):
|
|
285
282
|
"""Model for a tensor containing constant values."""
|
|
286
283
|
|
|
287
|
-
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True)
|
|
284
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True, extra="forbid")
|
|
288
285
|
|
|
289
286
|
name: str = Field(description="Descriptive name of the tensor representing the values. E.g., 'distances'")
|
|
290
287
|
"""Descriptive name of the tensor representing the values. E.g., 'distances'"""
|
|
@@ -377,7 +374,7 @@ class TensorConstant(BaseModel):
|
|
|
377
374
|
class Variable(BaseModel):
|
|
378
375
|
"""Model for a variable."""
|
|
379
376
|
|
|
380
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
377
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
381
378
|
|
|
382
379
|
name: str = Field(
|
|
383
380
|
description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
|
|
@@ -407,7 +404,7 @@ class Variable(BaseModel):
|
|
|
407
404
|
class TensorVariable(BaseModel):
|
|
408
405
|
"""Model for a tensor, e.g., vector variable."""
|
|
409
406
|
|
|
410
|
-
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True)
|
|
407
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True, extra="forbid")
|
|
411
408
|
|
|
412
409
|
name: str = Field(
|
|
413
410
|
description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
|
|
@@ -585,7 +582,7 @@ class ExtraFunction(BaseModel):
|
|
|
585
582
|
they are needed for other computations related to the problem.
|
|
586
583
|
"""
|
|
587
584
|
|
|
588
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
585
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
589
586
|
|
|
590
587
|
name: str = Field(
|
|
591
588
|
description=("Descriptive name of the function. Example: 'normalization'."),
|
|
@@ -663,7 +660,7 @@ class ExtraFunction(BaseModel):
|
|
|
663
660
|
class ScalarizationFunction(BaseModel):
|
|
664
661
|
"""Model for scalarization of the problem."""
|
|
665
662
|
|
|
666
|
-
model_config = ConfigDict(from_attributes=True)
|
|
663
|
+
model_config = ConfigDict(from_attributes=True, extra="forbid")
|
|
667
664
|
|
|
668
665
|
name: str = Field(description=("Name of the scalarization function."))
|
|
669
666
|
"""Name of the scalarization function."""
|
|
@@ -712,10 +709,35 @@ class ScalarizationFunction(BaseModel):
|
|
|
712
709
|
)
|
|
713
710
|
|
|
714
711
|
|
|
712
|
+
class Url(BaseModel):
|
|
713
|
+
"""Model for a URL."""
|
|
714
|
+
|
|
715
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
716
|
+
|
|
717
|
+
url: str = Field(
|
|
718
|
+
description=(
|
|
719
|
+
"A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
"""A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."""
|
|
723
|
+
|
|
724
|
+
auth: tuple[str, str] | None = Field(
|
|
725
|
+
description=(
|
|
726
|
+
"Optional. A tuple of username and password to be used for authentication when making requests to the URL."
|
|
727
|
+
),
|
|
728
|
+
default=None,
|
|
729
|
+
)
|
|
730
|
+
"""Optional. A tuple of username and password to be used for authentication when making requests to the URL."""
|
|
731
|
+
# Add headers and stuff for a proper HTTP request if needed in the future idk
|
|
732
|
+
|
|
733
|
+
|
|
715
734
|
class Simulator(BaseModel):
|
|
716
|
-
"""Model for simulator data.
|
|
735
|
+
"""Model for simulator data.
|
|
736
|
+
|
|
737
|
+
One of `file` or `url` must be provided, but not both.
|
|
738
|
+
"""
|
|
717
739
|
|
|
718
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
740
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
719
741
|
|
|
720
742
|
name: str = Field(
|
|
721
743
|
description=("Descriptive name of the simulator. This can be used in UI and visualizations."),
|
|
@@ -727,10 +749,15 @@ class Simulator(BaseModel):
|
|
|
727
749
|
" It may also be used in UIs and visualizations."
|
|
728
750
|
),
|
|
729
751
|
)
|
|
730
|
-
file: Path = Field(
|
|
731
|
-
description=("Path to a python file with the connection to simulators."),
|
|
732
|
-
)
|
|
752
|
+
file: Path | None = Field(description=("Path to a python file with the connection to simulators."), default=None)
|
|
733
753
|
"""Path to a python file with the connection to simulators."""
|
|
754
|
+
url: Url | None = Field(
|
|
755
|
+
description=(
|
|
756
|
+
"Optional. URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."
|
|
757
|
+
),
|
|
758
|
+
default=None,
|
|
759
|
+
)
|
|
760
|
+
"""Optional. A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."""
|
|
734
761
|
parameter_options: dict | None = Field(
|
|
735
762
|
description=(
|
|
736
763
|
"Parameters to the simulator that are not decision variables, but affect the results."
|
|
@@ -741,12 +768,30 @@ class Simulator(BaseModel):
|
|
|
741
768
|
"""Parameters to the simulator that are not decision variables, but affect the results.
|
|
742
769
|
Format is similar to decision variables. Can be 'None'."""
|
|
743
770
|
|
|
771
|
+
# Check that either file or url is provided, but not both
|
|
772
|
+
@model_validator(mode="after")
|
|
773
|
+
def check_file_or_url(self) -> Self:
|
|
774
|
+
"""Ensure that either file or url is provided, but not both."""
|
|
775
|
+
if self.file is None and self.url is None:
|
|
776
|
+
raise ValueError("Either 'file' or 'url' must be provided.")
|
|
777
|
+
if self.file is not None and self.url is not None:
|
|
778
|
+
raise ValueError("Only one of 'file' or 'url' can be provided.")
|
|
779
|
+
return self
|
|
780
|
+
|
|
744
781
|
|
|
745
782
|
class Objective(BaseModel):
|
|
746
783
|
"""Model for an objective function."""
|
|
747
784
|
|
|
748
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
785
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
749
786
|
|
|
787
|
+
"""A longer description for the objective."""
|
|
788
|
+
description: str | None = Field(
|
|
789
|
+
description=(
|
|
790
|
+
"A longer description of the objective function. This can be used in UI and visualizations. \
|
|
791
|
+
Meant to have longer text than what name should have."
|
|
792
|
+
),
|
|
793
|
+
default=None,
|
|
794
|
+
)
|
|
750
795
|
name: str = Field(
|
|
751
796
|
description=(
|
|
752
797
|
"Descriptive name of the objective function. This can be used in UI and visualizations. Example: 'time'."
|
|
@@ -786,9 +831,9 @@ class Objective(BaseModel):
|
|
|
786
831
|
variable/constant/extra function. Can be 'None' for 'data_based', 'simulator' or
|
|
787
832
|
'surrogate' objective functions. If 'None', either 'simulator_path' or 'surrogates' must
|
|
788
833
|
not be 'None'."""
|
|
789
|
-
simulator_path: Path | None = Field(
|
|
834
|
+
simulator_path: Path | Url | None = Field(
|
|
790
835
|
description=(
|
|
791
|
-
"Path to a python file with the connection to simulators. Must be a valid Path."
|
|
836
|
+
"Path to a python file or http server with the connection to simulators. Must be a valid Path or url."
|
|
792
837
|
"Can be 'None' for 'analytical', 'data_based' or 'surrogate' objective functions."
|
|
793
838
|
"If 'None', either 'func' or 'surrogates' must not be 'None'."
|
|
794
839
|
),
|
|
@@ -857,7 +902,7 @@ class Objective(BaseModel):
|
|
|
857
902
|
class Constraint(BaseModel):
|
|
858
903
|
"""Model for a constraint function."""
|
|
859
904
|
|
|
860
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
905
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
861
906
|
|
|
862
907
|
name: str = Field(
|
|
863
908
|
description=(
|
|
@@ -903,7 +948,7 @@ class Constraint(BaseModel):
|
|
|
903
948
|
function must match objective/variable/constant symbols.
|
|
904
949
|
Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'.
|
|
905
950
|
If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."""
|
|
906
|
-
simulator_path: Path | None = Field(
|
|
951
|
+
simulator_path: Path | Url | None = Field(
|
|
907
952
|
description=(
|
|
908
953
|
"Path to a python file with the connection to simulators. Must be a valid Path."
|
|
909
954
|
"Can be 'None' for if either 'func' or 'surrogates' is not 'None'."
|
|
@@ -960,7 +1005,7 @@ class DiscreteRepresentation(BaseModel):
|
|
|
960
1005
|
found at `objective_values['f_i'][j]` for all `i` and some `j`.
|
|
961
1006
|
"""
|
|
962
1007
|
|
|
963
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
1008
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
964
1009
|
|
|
965
1010
|
variable_values: dict[str, list[VariableType]] = Field(
|
|
966
1011
|
description=(
|
|
@@ -999,7 +1044,7 @@ class DiscreteRepresentation(BaseModel):
|
|
|
999
1044
|
class Problem(BaseModel):
|
|
1000
1045
|
"""Model for a problem definition."""
|
|
1001
1046
|
|
|
1002
|
-
model_config = ConfigDict(frozen=True)
|
|
1047
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
1003
1048
|
|
|
1004
1049
|
_scalarization_index: int = PrivateAttr(default=1)
|
|
1005
1050
|
# TODO: make init to communicate the _scalarization_index to a new model
|
|
@@ -1017,7 +1062,6 @@ class Problem(BaseModel):
|
|
|
1017
1062
|
is_convex=db_instance.is_convex,
|
|
1018
1063
|
is_linear=db_instance.is_linear,
|
|
1019
1064
|
is_twice_differentiable=db_instance.is_twice_differentiable,
|
|
1020
|
-
variable_domain=db_instance.variable_domain,
|
|
1021
1065
|
scenario_keys=db_instance.scenario_keys,
|
|
1022
1066
|
constants=constants if constants != [] else None,
|
|
1023
1067
|
variables=[Variable.model_validate(var) for var in db_instance.variables]
|
|
@@ -1564,7 +1608,7 @@ class Problem(BaseModel):
|
|
|
1564
1608
|
|
|
1565
1609
|
"""
|
|
1566
1610
|
json_content = self.model_dump_json(indent=4)
|
|
1567
|
-
path.write_text(json_content)
|
|
1611
|
+
path.write_text(json_content, encoding="utf-8")
|
|
1568
1612
|
|
|
1569
1613
|
@classmethod
|
|
1570
1614
|
def load_json(cls, path: Path) -> "Problem":
|
|
@@ -1578,33 +1622,7 @@ class Problem(BaseModel):
|
|
|
1578
1622
|
"""
|
|
1579
1623
|
json_data = path.read_text()
|
|
1580
1624
|
|
|
1581
|
-
return cls.model_validate_json(json_data)
|
|
1582
|
-
|
|
1583
|
-
@model_validator(mode="after")
|
|
1584
|
-
def set_is_twice_differentiable(cls, values):
|
|
1585
|
-
"""If "is_twice_differentiable" is explicitly provided to the model, we set it to that value."""
|
|
1586
|
-
if "is_twice_differentiable" in values and values["is_twice_differentiable"] is not None:
|
|
1587
|
-
values["is_twice_differentiable_"] = values["is_twice_differentiable"]
|
|
1588
|
-
|
|
1589
|
-
return values
|
|
1590
|
-
|
|
1591
|
-
@model_validator(mode="after")
|
|
1592
|
-
@classmethod
|
|
1593
|
-
def set_is_linear(cls, values):
|
|
1594
|
-
"""If "is_linear" is explicitly provided to the model, we set it to that value."""
|
|
1595
|
-
if "is_linear" in values and values["is_linear"] is not None:
|
|
1596
|
-
values["is_linear_"] = values["is_linear"]
|
|
1597
|
-
|
|
1598
|
-
return values
|
|
1599
|
-
|
|
1600
|
-
@model_validator(mode="after")
|
|
1601
|
-
@classmethod
|
|
1602
|
-
def set_is_convex(cls, values):
|
|
1603
|
-
"""If "is_convex" is explicitly provided to the model, we set it to that value."""
|
|
1604
|
-
if "is_convex" in values and values["is_convex"] is not None:
|
|
1605
|
-
values["is_convex_"] = values["is_convex"]
|
|
1606
|
-
|
|
1607
|
-
return values
|
|
1625
|
+
return cls.model_validate_json(json_data, by_name=True)
|
|
1608
1626
|
|
|
1609
1627
|
name: str = Field(
|
|
1610
1628
|
description="Name of the problem.",
|