desdeo 1.1.3__py3-none-any.whl → 2.0.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/api/README.md +73 -0
- desdeo/api/__init__.py +15 -0
- desdeo/api/app.py +40 -0
- desdeo/api/config.py +69 -0
- desdeo/api/config.toml +53 -0
- desdeo/api/db.py +25 -0
- desdeo/api/db_init.py +79 -0
- desdeo/api/db_models.py +164 -0
- desdeo/api/malaga_db_init.py +27 -0
- desdeo/api/models/__init__.py +66 -0
- desdeo/api/models/archive.py +34 -0
- desdeo/api/models/preference.py +90 -0
- desdeo/api/models/problem.py +507 -0
- desdeo/api/models/reference_point_method.py +18 -0
- desdeo/api/models/session.py +46 -0
- desdeo/api/models/state.py +96 -0
- desdeo/api/models/user.py +51 -0
- desdeo/api/routers/_NAUTILUS.py +245 -0
- desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
- desdeo/api/routers/_NIMBUS.py +762 -0
- desdeo/api/routers/__init__.py +5 -0
- desdeo/api/routers/problem.py +110 -0
- desdeo/api/routers/reference_point_method.py +117 -0
- desdeo/api/routers/session.py +76 -0
- desdeo/api/routers/test.py +16 -0
- desdeo/api/routers/user_authentication.py +366 -0
- desdeo/api/schema.py +94 -0
- desdeo/api/tests/__init__.py +0 -0
- desdeo/api/tests/conftest.py +59 -0
- desdeo/api/tests/test_models.py +701 -0
- desdeo/api/tests/test_routes.py +216 -0
- desdeo/api/utils/database.py +274 -0
- desdeo/api/utils/logger.py +29 -0
- desdeo/core.py +27 -0
- desdeo/emo/__init__.py +29 -0
- desdeo/emo/hooks/archivers.py +172 -0
- desdeo/emo/methods/EAs.py +418 -0
- desdeo/emo/methods/__init__.py +0 -0
- desdeo/emo/methods/bases.py +59 -0
- desdeo/emo/operators/__init__.py +1 -0
- desdeo/emo/operators/crossover.py +780 -0
- desdeo/emo/operators/evaluator.py +118 -0
- desdeo/emo/operators/generator.py +356 -0
- desdeo/emo/operators/mutation.py +1053 -0
- desdeo/emo/operators/selection.py +1036 -0
- desdeo/emo/operators/termination.py +178 -0
- desdeo/explanations/__init__.py +6 -0
- desdeo/explanations/explainer.py +100 -0
- desdeo/explanations/utils.py +90 -0
- desdeo/mcdm/__init__.py +19 -0
- desdeo/mcdm/nautili.py +345 -0
- desdeo/mcdm/nautilus.py +477 -0
- desdeo/mcdm/nautilus_navigator.py +655 -0
- desdeo/mcdm/nimbus.py +417 -0
- desdeo/mcdm/pareto_navigator.py +269 -0
- desdeo/mcdm/reference_point_method.py +116 -0
- desdeo/problem/__init__.py +79 -0
- desdeo/problem/evaluator.py +561 -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 +468 -0
- desdeo/problem/schema.py +1808 -0
- desdeo/problem/simulator_evaluator.py +298 -0
- desdeo/problem/sympy_evaluator.py +244 -0
- desdeo/problem/testproblems/__init__.py +73 -0
- desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
- desdeo/problem/testproblems/dtlz2_problem.py +102 -0
- desdeo/problem/testproblems/forest_problem.py +275 -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/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_problem.py +434 -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/spanish_sustainability_problem.py +945 -0
- desdeo/problem/testproblems/zdt_problem.py +271 -0
- desdeo/problem/utils.py +245 -0
- desdeo/tools/GenerateReferencePoints.py +181 -0
- desdeo/tools/__init__.py +102 -0
- desdeo/tools/generics.py +145 -0
- desdeo/tools/gurobipy_solver_interfaces.py +258 -0
- desdeo/tools/indicators_binary.py +11 -0
- desdeo/tools/indicators_unary.py +375 -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 +234 -0
- desdeo/tools/ng_solver_interfaces.py +199 -0
- desdeo/tools/non_dominated_sorting.py +133 -0
- desdeo/tools/patterns.py +281 -0
- desdeo/tools/proximal_solver.py +99 -0
- desdeo/tools/pyomo_solver_interfaces.py +464 -0
- desdeo/tools/reference_vectors.py +462 -0
- desdeo/tools/scalarization.py +3138 -0
- desdeo/tools/scipy_solver_interfaces.py +454 -0
- desdeo/tools/score_bands.py +464 -0
- desdeo/tools/utils.py +320 -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.0.0.dist-info/LICENSE +21 -0
- desdeo-2.0.0.dist-info/METADATA +168 -0
- desdeo-2.0.0.dist-info/RECORD +120 -0
- {desdeo-1.1.3.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
- desdeo-1.1.3.dist-info/METADATA +0 -18
- desdeo-1.1.3.dist-info/RECORD +0 -4
desdeo/problem/schema.py
ADDED
|
@@ -0,0 +1,1808 @@
|
|
|
1
|
+
"""Schema for the problem definition.
|
|
2
|
+
|
|
3
|
+
The problem definition is a JSON file that contains the following information:
|
|
4
|
+
|
|
5
|
+
- Constants
|
|
6
|
+
- Variables
|
|
7
|
+
- Objectives
|
|
8
|
+
- Extra functions
|
|
9
|
+
- Scalarization functions
|
|
10
|
+
- Evaluated solutions and their info
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from itertools import product
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeAliasType
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from pydantic import (
|
|
23
|
+
BaseModel,
|
|
24
|
+
ConfigDict,
|
|
25
|
+
Field,
|
|
26
|
+
PrivateAttr,
|
|
27
|
+
ValidationError,
|
|
28
|
+
ValidationInfo,
|
|
29
|
+
ValidatorFunctionWrapHandler,
|
|
30
|
+
WrapValidator,
|
|
31
|
+
field_validator,
|
|
32
|
+
model_validator,
|
|
33
|
+
)
|
|
34
|
+
from pydantic_core import PydanticCustomError
|
|
35
|
+
|
|
36
|
+
from desdeo.problem.infix_parser import InfixExpressionParser
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from desdeo.api.models import ProblemDB
|
|
40
|
+
|
|
41
|
+
VariableType = float | int | bool
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def tensor_custom_error_validator(value: Any, handler: ValidatorFunctionWrapHandler, _info: ValidationInfo) -> Any:
|
|
45
|
+
"""Custom error handler to simplify error messages related to recursive tensor types.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
value (Any): input value to be validated.
|
|
49
|
+
handler (ValidatorFunctionWrapHandler): handler to check the values.
|
|
50
|
+
_info (ValidationInfo): info related to the validation of the value.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
PydanticCustomError: when the value is an invalid tensor type.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Any: a valid tensor.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
return handler(value)
|
|
60
|
+
except ValidationError as exc:
|
|
61
|
+
raise PydanticCustomError("invalid tensor", "Input is not a valid tensor") from exc
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Tensor = TypeAliasType(
|
|
65
|
+
"Tensor",
|
|
66
|
+
Annotated[
|
|
67
|
+
list["Tensor"] | list[VariableType] | VariableType | Literal["List"] | None,
|
|
68
|
+
WrapValidator(tensor_custom_error_validator),
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_infix_to_func(cls: "Problem", v: str | list) -> list:
|
|
74
|
+
"""Validator that checks if the 'func' field is of type str or list.
|
|
75
|
+
|
|
76
|
+
If str, then it is assumed the string represents the func in infix notation. The string
|
|
77
|
+
is parsed in the validator. If list, then the func is assumed to be represented in Math JSON format.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
cls: the class of the pydantic model the validator is applied to.
|
|
81
|
+
v (str | list): The func to be validated.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: v is neither an instance of str or a list.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
list: The func represented in Math JSON format.
|
|
88
|
+
"""
|
|
89
|
+
if v is None:
|
|
90
|
+
return v
|
|
91
|
+
# Check if v is a string (infix expression), then parse it
|
|
92
|
+
if isinstance(v, str):
|
|
93
|
+
parser = InfixExpressionParser()
|
|
94
|
+
return parser.parse(v)
|
|
95
|
+
# If v is already in the correct format (a list), just return it
|
|
96
|
+
if isinstance(v, list):
|
|
97
|
+
return v
|
|
98
|
+
|
|
99
|
+
# Raise an error if v is neither a string nor a list
|
|
100
|
+
msg = f"The function expressions must be a string (infix expression) or a list. Got {type(v)}."
|
|
101
|
+
raise ValueError(msg)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_scenario_key_singleton_to_list(cls: "Problem", v: str | list[str]) -> list[str] | None:
|
|
105
|
+
"""Validator that checks the type of a scenario key.
|
|
106
|
+
|
|
107
|
+
If the type is a list, it will be returned as it is. If it is a string,
|
|
108
|
+
then a list with the single string is returned. Else, a ValueError is raised.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
cls: the class of the pydantic model the validator is applied to.
|
|
112
|
+
v (str | list[str]): the scenario key, or keys, to be validated.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: raised when `v` it neither a string or a list.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
list[str]: a list with scenario keys.
|
|
119
|
+
"""
|
|
120
|
+
if v is None:
|
|
121
|
+
return v
|
|
122
|
+
if isinstance(v, str):
|
|
123
|
+
return [v]
|
|
124
|
+
if isinstance(v, list):
|
|
125
|
+
return v
|
|
126
|
+
|
|
127
|
+
msg = f"The scenario keys must be either a list of strings, or a single string. Got {type(v)}."
|
|
128
|
+
raise ValueError(msg)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_list_to_mathjson(cls: "TensorVariable", v: Tensor | VariableType | None) -> list:
|
|
132
|
+
"""Validator that makes sure a nested Python list is represented as tensor following the MathJSON convention.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
cls (TensorVariable): the class of the pydantic model the validator is applied to.
|
|
136
|
+
v (Tensor | VariableType | None): the nested lists to be validated.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
list: a tensor following the MathJSON conventions; or a single value or None,
|
|
140
|
+
if v was assigned to one of these types.
|
|
141
|
+
"""
|
|
142
|
+
if v is None or isinstance(v, VariableType):
|
|
143
|
+
return v
|
|
144
|
+
|
|
145
|
+
# Check if the input is already in MathJSON format
|
|
146
|
+
if isinstance(v, list) and len(v) > 0 and v[0] == "List":
|
|
147
|
+
return v
|
|
148
|
+
|
|
149
|
+
# recursively parse into a MathJSON representation
|
|
150
|
+
if isinstance(v, list) and len(v) > 0:
|
|
151
|
+
if v[0] == "List":
|
|
152
|
+
# assumed to be already in MathJson format, just return the list
|
|
153
|
+
return v
|
|
154
|
+
if isinstance(v[0], list):
|
|
155
|
+
# recursive case, encountered list
|
|
156
|
+
return ["List", *[parse_list_to_mathjson(TensorVariable, v_element) for v_element in v]]
|
|
157
|
+
if isinstance(v[0], VariableType | None):
|
|
158
|
+
# terminal case, encountered a VariableType
|
|
159
|
+
return ["List", *v]
|
|
160
|
+
|
|
161
|
+
# if anything else is encountered, raise an error
|
|
162
|
+
msg = "Encountered value that is not a valid VariableType nor a list."
|
|
163
|
+
raise ValueError(msg)
|
|
164
|
+
|
|
165
|
+
msg = f"The tensor must a Python list (of lists) or a single value of type VariableType. Got {type(v)}."
|
|
166
|
+
raise ValueError(msg)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_tensor_values(
|
|
170
|
+
values: Iterable[VariableType | Iterable[VariableType]] | VariableType | None,
|
|
171
|
+
) -> Iterable[VariableType | Iterable[VariableType]] | VariableType | None:
|
|
172
|
+
"""Return the values for a given attribute as a nested Python list or single value.
|
|
173
|
+
|
|
174
|
+
Removes the 'List' entries from the JSON format to give a Python compatible list.
|
|
175
|
+
If the values are a single value or None, then a single value or None is returned
|
|
176
|
+
instead, respectively.
|
|
177
|
+
|
|
178
|
+
Arguments:
|
|
179
|
+
values (Iterable[VariableType | Iterable[VariableType]] | VariableType | None):
|
|
180
|
+
the values that should be extracted as a Python list.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
list[VariableType] | Iterable[list[VariableType]] | VariableType| None: a list with shape `self.shape` with the
|
|
184
|
+
values defined for the variable. If a single values consisted of a single value or None instead, then
|
|
185
|
+
a single valuer or None are returned, respectively.
|
|
186
|
+
"""
|
|
187
|
+
if values is None or isinstance(values, VariableType):
|
|
188
|
+
return values
|
|
189
|
+
|
|
190
|
+
if isinstance(values, list) and len(values) > 1:
|
|
191
|
+
if values[0] == "List" and isinstance(values[1], list):
|
|
192
|
+
# recursive case, encountered list
|
|
193
|
+
return [get_tensor_values(v_element) for v_element in values[1:]]
|
|
194
|
+
if values[0] == "List":
|
|
195
|
+
# terminal case, encountered a VariableType
|
|
196
|
+
return [*values[1:]]
|
|
197
|
+
|
|
198
|
+
# if anything else is encountered, raise an error
|
|
199
|
+
msg = "Encountered value that is not a valid VariableType nor a list."
|
|
200
|
+
raise ValueError(msg)
|
|
201
|
+
|
|
202
|
+
msg = f"Values must be a valid MathJSON vector. Got {type(values)}."
|
|
203
|
+
raise ValueError(msg)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class VariableTypeEnum(str, Enum):
|
|
207
|
+
"""An enumerator for possible variable types."""
|
|
208
|
+
|
|
209
|
+
real = "real"
|
|
210
|
+
"""A continuous variable."""
|
|
211
|
+
integer = "integer"
|
|
212
|
+
"""An integer variable."""
|
|
213
|
+
binary = "binary"
|
|
214
|
+
"""A binary variable."""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class VariableDomainTypeEnum(str, Enum):
|
|
218
|
+
"""An enumerator for the possible variable type domains of a problem."""
|
|
219
|
+
|
|
220
|
+
continuous = "continuous"
|
|
221
|
+
"""All variables are real valued."""
|
|
222
|
+
binary = "binary"
|
|
223
|
+
"""All variables are binary valued."""
|
|
224
|
+
integer = "integer"
|
|
225
|
+
"""All variables are integer or binary valued."""
|
|
226
|
+
mixed = "mixed"
|
|
227
|
+
"""Some variables are continuos, some are integer or binary."""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class ConstraintTypeEnum(str, Enum):
|
|
231
|
+
"""An enumerator for supported constraint expression types."""
|
|
232
|
+
|
|
233
|
+
EQ = "="
|
|
234
|
+
"""An equality constraint."""
|
|
235
|
+
LTE = "<=" # less than or equal
|
|
236
|
+
"""An inequality constraint of type 'less than or equal'."""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ObjectiveTypeEnum(str, Enum):
|
|
240
|
+
"""An enumerator for supported objective function types."""
|
|
241
|
+
|
|
242
|
+
analytical = "analytical"
|
|
243
|
+
"""An objective function with an analytical formulation. E.g., it can be
|
|
244
|
+
expressed with mathematical expressions, such as x_1 + x_2."""
|
|
245
|
+
data_based = "data_based"
|
|
246
|
+
"""A data-based objective function. It is assumed that when such an
|
|
247
|
+
objective is present in a `Problem`, then there is a
|
|
248
|
+
`DiscreteRepresentation` available with values representing the objective
|
|
249
|
+
function."""
|
|
250
|
+
simulator = "simulator"
|
|
251
|
+
"""A simulator based objective function. It is assumed that a Path (str)
|
|
252
|
+
to a simulator file that connects a simulator to DESDEO is present in
|
|
253
|
+
the `Objective` and also in the list of simulators in the `Problem`."""
|
|
254
|
+
surrogate = "surrogate"
|
|
255
|
+
"""A surrogate based objective function. It is assumed that a Path (str)
|
|
256
|
+
to a surrogate saved on the disk is present in the `Objective` and also in
|
|
257
|
+
the list of simulators in the `Problem`."""
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Constant(BaseModel):
|
|
261
|
+
"""Model for a constant."""
|
|
262
|
+
|
|
263
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
264
|
+
|
|
265
|
+
name: str = Field(
|
|
266
|
+
description=(
|
|
267
|
+
"Descriptive name of the constant. This can be used in UI and visualizations. Example: 'maximum cost'."
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
"""Descriptive name of the constant. This can be used in UI and visualizations." " Example: 'maximum cost'."""
|
|
271
|
+
symbol: str = Field(
|
|
272
|
+
description=(
|
|
273
|
+
"Symbol to represent the constant. This will be used in the rest of the problem definition."
|
|
274
|
+
" It may also be used in UIs and visualizations. Example: 'c_1'."
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
""" Symbol to represent the constant. This will be used in the rest of the
|
|
278
|
+
problem definition. It may also be used in UIs and visualizations. Example:
|
|
279
|
+
'c_1'."""
|
|
280
|
+
value: VariableType = Field(description="The value of the constant.")
|
|
281
|
+
"""The value of the constant."""
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class TensorConstant(BaseModel):
|
|
285
|
+
"""Model for a tensor containing constant values."""
|
|
286
|
+
|
|
287
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True)
|
|
288
|
+
|
|
289
|
+
name: str = Field(description="Descriptive name of the tensor representing the values. E.g., 'distances'")
|
|
290
|
+
"""Descriptive name of the tensor representing the values. E.g., 'distances'"""
|
|
291
|
+
symbol: str = Field(
|
|
292
|
+
description=(
|
|
293
|
+
"Symbol to represent the constant. This will be used in the rest of the problem definition."
|
|
294
|
+
" Notice that the elements of the tensor will be represented with the symbol followed by"
|
|
295
|
+
" indices. E.g., the first element of the third element of a 2-dimensional tensor,"
|
|
296
|
+
" is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable."
|
|
297
|
+
" Note that indexing starts from 1."
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
"""
|
|
301
|
+
Symbol to represent the constant. This will be used in the rest of the problem definition.
|
|
302
|
+
Notice that the elements of the tensor will be represented with the symbol followed by
|
|
303
|
+
indices. E.g., the first element of the third element of a 2-dimensional tensor,
|
|
304
|
+
is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable.
|
|
305
|
+
Note that indexing starts from 1.
|
|
306
|
+
"""
|
|
307
|
+
shape: list[int] = Field(
|
|
308
|
+
description=(
|
|
309
|
+
"A list of the dimensions of the tensor, e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns."
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
"""A list of the dimensions of the tensor, e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns.
|
|
313
|
+
"""
|
|
314
|
+
values: Tensor = Field(
|
|
315
|
+
description=(
|
|
316
|
+
"A list of lists, with the elements representing the values of each constant element in the tensor. "
|
|
317
|
+
"E.g., `[[5, 22, 0], [14, 5, 44]]`."
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
"""A list of lists, with the elements representing the initial values of each constant element in the tensor.
|
|
321
|
+
E.g., `[[5, 22, 0], [14, 5, 44]]`."""
|
|
322
|
+
|
|
323
|
+
_parse_list_to_mathjson = field_validator("values", mode="before")(parse_list_to_mathjson)
|
|
324
|
+
|
|
325
|
+
def get_values(self) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None, Iterable[None]]:
|
|
326
|
+
"""Return the constant values as a Python iterable (e.g., list of list)."""
|
|
327
|
+
values = get_tensor_values(self.values)
|
|
328
|
+
if isinstance(values, VariableType | None):
|
|
329
|
+
return np.full(self.shape, values).tolist()
|
|
330
|
+
|
|
331
|
+
return values
|
|
332
|
+
|
|
333
|
+
def to_constants(self) -> list[Constant]:
|
|
334
|
+
"""Flatten the tensor into a list of Constants.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
list[Constant]: a list of Constants.
|
|
338
|
+
"""
|
|
339
|
+
constants = []
|
|
340
|
+
for indices in list(product(*[range(1, dim + 1) for dim in self.shape])):
|
|
341
|
+
constants.append(self[*indices])
|
|
342
|
+
|
|
343
|
+
return constants
|
|
344
|
+
|
|
345
|
+
def __getitem__(self, indices: int | tuple[int]) -> Constant:
|
|
346
|
+
"""Implements random access for TensorConstant.
|
|
347
|
+
|
|
348
|
+
Note:
|
|
349
|
+
Indexing is assumed to start at 1.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
indices (int | Tuple[int]): a single integer or tuple of integers.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Constant: A new instance of Constant that has been setup with
|
|
356
|
+
information found at the specified indices in the TensorConstant.
|
|
357
|
+
"""
|
|
358
|
+
if isinstance(indices, tuple):
|
|
359
|
+
# multi-dimensional indexing
|
|
360
|
+
name = f"{self.name} at position {[*indices]}"
|
|
361
|
+
symbol = f"{self.symbol}_{'_'.join(map(str, indices))}"
|
|
362
|
+
|
|
363
|
+
value = self.get_values()
|
|
364
|
+
|
|
365
|
+
for idx in indices:
|
|
366
|
+
value = value[idx - 1]
|
|
367
|
+
|
|
368
|
+
else:
|
|
369
|
+
# single indexing
|
|
370
|
+
name = f"{self.name} at position [{indices}]"
|
|
371
|
+
symbol = f"{self.symbol}_{indices}"
|
|
372
|
+
value = self.get_values()[indices - 1]
|
|
373
|
+
|
|
374
|
+
return Constant(name=name, symbol=symbol, value=value)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class Variable(BaseModel):
|
|
378
|
+
"""Model for a variable."""
|
|
379
|
+
|
|
380
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
381
|
+
|
|
382
|
+
name: str = Field(
|
|
383
|
+
description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
|
|
384
|
+
)
|
|
385
|
+
"""Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."""
|
|
386
|
+
symbol: str = Field(
|
|
387
|
+
description=(
|
|
388
|
+
"Symbol to represent the variable. This will be used in the rest of the problem definition."
|
|
389
|
+
" It may also be used in UIs and visualizations. Example: 'v_1'."
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
""" Symbol to represent the variable. This will be used in the rest of the
|
|
393
|
+
problem definition. It may also be used in UIs and visualizations. Example:
|
|
394
|
+
'v_1'."""
|
|
395
|
+
variable_type: VariableTypeEnum = Field(description="Type of the variable. Can be real, integer or binary.")
|
|
396
|
+
"""Type of the variable. Can be real, integer or binary."""
|
|
397
|
+
lowerbound: VariableType | None = Field(description="Lower bound of the variable.", default=None)
|
|
398
|
+
"""Lower bound of the variable. Defaults to `None`."""
|
|
399
|
+
upperbound: VariableType | None = Field(description="Upper bound of the variable.", default=None)
|
|
400
|
+
"""Upper bound of the variable. Defaults to `None`."""
|
|
401
|
+
initial_value: VariableType | None = Field(
|
|
402
|
+
description="Initial value of the variable. This is optional.", default=None
|
|
403
|
+
)
|
|
404
|
+
"""Initial value of the variable. This is optional. Defaults to `None`."""
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class TensorVariable(BaseModel):
|
|
408
|
+
"""Model for a tensor, e.g., vector variable."""
|
|
409
|
+
|
|
410
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True)
|
|
411
|
+
|
|
412
|
+
name: str = Field(
|
|
413
|
+
description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
|
|
414
|
+
)
|
|
415
|
+
"""Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."""
|
|
416
|
+
symbol: str = Field(
|
|
417
|
+
description=(
|
|
418
|
+
"Symbol to represent the variable. This will be used in the rest of the problem definition."
|
|
419
|
+
" Notice that the elements of the tensor will be represented with the symbol followed by"
|
|
420
|
+
" indices. E.g., the first element of the third element of a 2-dimensional tensor,"
|
|
421
|
+
" is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable."
|
|
422
|
+
" Note that indexing starts from 1."
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
"""
|
|
426
|
+
Symbol to represent the variable. This will be used in the rest of the problem definition.
|
|
427
|
+
Notice that the elements of the tensor will be represented with the symbol followed by
|
|
428
|
+
indices. E.g., the first element of the third element of a 2-dimensional tensor,
|
|
429
|
+
is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable.
|
|
430
|
+
Note that indexing starts from 1.
|
|
431
|
+
"""
|
|
432
|
+
variable_type: VariableTypeEnum = Field(
|
|
433
|
+
description=(
|
|
434
|
+
"Type of the variable. Can be real, integer, or binary. "
|
|
435
|
+
"Note that each element of a TensorVariable is assumed to be of the same type."
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
"""Type of the variable. Can be real, integer, or binary.
|
|
439
|
+
Note that each element of a TensorVariable is assumed to be of the same type."""
|
|
440
|
+
|
|
441
|
+
shape: list[int] = Field(
|
|
442
|
+
description=(
|
|
443
|
+
"A list of the dimensions of the tensor, e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns."
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
"""A list of the dimensions of the tensor,
|
|
447
|
+
e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns.
|
|
448
|
+
"""
|
|
449
|
+
lowerbounds: Tensor | None = Field(
|
|
450
|
+
description=(
|
|
451
|
+
"A list of lists, with the elements representing the lower bounds of each element. "
|
|
452
|
+
"E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied, that value is assumed to be the lower "
|
|
453
|
+
"bound of each element. Defaults to None."
|
|
454
|
+
),
|
|
455
|
+
default=None,
|
|
456
|
+
)
|
|
457
|
+
"""A list of lists, with the elements representing the lower bounds of each
|
|
458
|
+
element. E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied,
|
|
459
|
+
that value is assumed to be the lower bound of each element. Defaults to
|
|
460
|
+
None."""
|
|
461
|
+
upperbounds: Tensor | VariableType | None = Field(
|
|
462
|
+
description=(
|
|
463
|
+
"A list of lists, with the elements representing the upper bounds of each "
|
|
464
|
+
"element. E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied, "
|
|
465
|
+
"that value is assumed to be the upper bound of each element. Defaults to "
|
|
466
|
+
"None."
|
|
467
|
+
),
|
|
468
|
+
default=None,
|
|
469
|
+
)
|
|
470
|
+
"""A list of lists, with the elements representing the upper bounds of each
|
|
471
|
+
element. E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied,
|
|
472
|
+
that value is assumed to be the upper bound of each element. Defaults to
|
|
473
|
+
None."""
|
|
474
|
+
initial_values: Tensor | VariableType | None = Field(
|
|
475
|
+
description=(
|
|
476
|
+
"A list of lists, with the elements representing the initial values of "
|
|
477
|
+
"each element. E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is "
|
|
478
|
+
"supplied, that value is assumed to be the initial value of each element. "
|
|
479
|
+
"Defaults to None."
|
|
480
|
+
),
|
|
481
|
+
default=None,
|
|
482
|
+
)
|
|
483
|
+
"""A list of lists, with the elements representing the initial values of
|
|
484
|
+
each element. E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is
|
|
485
|
+
supplied, that value is assumed to be the initial value of each element.
|
|
486
|
+
Defaults to None."""
|
|
487
|
+
|
|
488
|
+
_parse_list_to_mathjson = field_validator("lowerbounds", "upperbounds", "initial_values", mode="before")(
|
|
489
|
+
parse_list_to_mathjson
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def get_lowerbound_values(
|
|
493
|
+
self,
|
|
494
|
+
) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
|
|
495
|
+
"""Return the lowerbounds values, if any, as a Python iterable (list of list)."""
|
|
496
|
+
lowerbounds = get_tensor_values(self.lowerbounds)
|
|
497
|
+
if isinstance(lowerbounds, VariableType | None):
|
|
498
|
+
# single value, construct list with the correct dimensions
|
|
499
|
+
return np.full(self.shape, lowerbounds).tolist()
|
|
500
|
+
|
|
501
|
+
return lowerbounds
|
|
502
|
+
|
|
503
|
+
def get_upperbound_values(
|
|
504
|
+
self,
|
|
505
|
+
) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
|
|
506
|
+
"""Return the upperbounds values, if any, as a Python iterable (list of list)."""
|
|
507
|
+
upperbounds = get_tensor_values(self.upperbounds)
|
|
508
|
+
if isinstance(upperbounds, VariableType | None):
|
|
509
|
+
# single value, construct list with the correct dimensions
|
|
510
|
+
return np.full(self.shape, upperbounds).tolist()
|
|
511
|
+
|
|
512
|
+
return upperbounds
|
|
513
|
+
|
|
514
|
+
def get_initial_values(self) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
|
|
515
|
+
"""Return the initial values, if any, as a Python iterable (list of list)."""
|
|
516
|
+
values = get_tensor_values(self.initial_values)
|
|
517
|
+
if isinstance(values, VariableType | None):
|
|
518
|
+
# single value, construct list with the correct dimensions
|
|
519
|
+
return np.full(self.shape, values).tolist()
|
|
520
|
+
|
|
521
|
+
return values
|
|
522
|
+
|
|
523
|
+
def to_variables(self) -> list[Variable]:
|
|
524
|
+
"""Flatten the tensor into a list of Variables.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
list[Variable]: a list of Variables.
|
|
528
|
+
"""
|
|
529
|
+
variables = []
|
|
530
|
+
for indices in list(product(*[range(1, dim + 1) for dim in self.shape])):
|
|
531
|
+
variables.append(self[*indices])
|
|
532
|
+
|
|
533
|
+
return variables
|
|
534
|
+
|
|
535
|
+
def __getitem__(self, indices: int | tuple[int]) -> Variable:
|
|
536
|
+
"""Implements random access for TensorVariable.
|
|
537
|
+
|
|
538
|
+
Note:
|
|
539
|
+
Indexing is assumed to start at 1.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
indices (int | Tuple[int]): a single integer or tuple of integers.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
Variable: A new instance of Variable that has been setup with
|
|
546
|
+
information found at the specified indices in the TensorVariable.
|
|
547
|
+
"""
|
|
548
|
+
if isinstance(indices, tuple):
|
|
549
|
+
# multi-dimensional indexing
|
|
550
|
+
name = f"{self.name} at position {[*indices]}"
|
|
551
|
+
symbol = f"{self.symbol}_{'_'.join(map(str, indices))}"
|
|
552
|
+
|
|
553
|
+
lowerbound = self.get_lowerbound_values()
|
|
554
|
+
upperbound = self.get_upperbound_values()
|
|
555
|
+
initial_value = self.get_initial_values()
|
|
556
|
+
|
|
557
|
+
for idx in indices:
|
|
558
|
+
lowerbound = lowerbound[idx - 1]
|
|
559
|
+
upperbound = upperbound[idx - 1]
|
|
560
|
+
initial_value = initial_value[idx - 1]
|
|
561
|
+
|
|
562
|
+
else:
|
|
563
|
+
# single indexing
|
|
564
|
+
name = f"{self.name} at position [{indices}]"
|
|
565
|
+
symbol = f"{self.symbol}_{indices}"
|
|
566
|
+
|
|
567
|
+
lowerbound = self.get_lowerbound_values()[indices - 1]
|
|
568
|
+
upperbound = self.get_upperbound_values()[indices - 1]
|
|
569
|
+
initial_value = self.get_initial_values()[indices - 1]
|
|
570
|
+
|
|
571
|
+
return Variable(
|
|
572
|
+
name=name,
|
|
573
|
+
symbol=symbol,
|
|
574
|
+
variable_type=self.variable_type,
|
|
575
|
+
lowerbound=lowerbound,
|
|
576
|
+
upperbound=upperbound,
|
|
577
|
+
initial_value=initial_value,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
class ExtraFunction(BaseModel):
|
|
582
|
+
"""Model for extra functions.
|
|
583
|
+
|
|
584
|
+
These functions can, e.g., be functions that are re-used in the problem formulation, or
|
|
585
|
+
they are needed for other computations related to the problem.
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
589
|
+
|
|
590
|
+
name: str = Field(
|
|
591
|
+
description=("Descriptive name of the function. Example: 'normalization'."),
|
|
592
|
+
)
|
|
593
|
+
"""Descriptive name of the function. Example: 'normalization'."""
|
|
594
|
+
symbol: str = Field(
|
|
595
|
+
description=(
|
|
596
|
+
"Symbol to represent the function. This will be used in the rest of the problem definition."
|
|
597
|
+
" It may also be used in UIs and visualizations. Example: 'avg'."
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
""" Symbol to represent the function. This will be used in the rest of the
|
|
601
|
+
problem definition. It may also be used in UIs and visualizations. Example:
|
|
602
|
+
'avg'."""
|
|
603
|
+
func: list | None = Field(
|
|
604
|
+
description=(
|
|
605
|
+
"The string representing the function. This is a JSON object that can be parsed into a function."
|
|
606
|
+
"Must be a valid MathJSON object."
|
|
607
|
+
" The symbols in the function must match symbols defined for objective/variable/constant."
|
|
608
|
+
"Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'. "
|
|
609
|
+
"If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."
|
|
610
|
+
),
|
|
611
|
+
default=None,
|
|
612
|
+
)
|
|
613
|
+
""" The string representing the function. This is a JSON object that can be
|
|
614
|
+
parsed into a function. Must be a valid MathJSON object. The symbols in
|
|
615
|
+
the function must match symbols defined for objective/variable/constant.
|
|
616
|
+
Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'.
|
|
617
|
+
If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."""
|
|
618
|
+
simulator_path: Path | None = Field(
|
|
619
|
+
description=(
|
|
620
|
+
"Path to a python file with the connection to simulators. Must be a valid Path."
|
|
621
|
+
"Can be 'None' for 'analytical', 'data_based' or 'surrogate' functions."
|
|
622
|
+
"If 'None', either 'func' or 'surrogates' must not be 'None'."
|
|
623
|
+
),
|
|
624
|
+
default=None,
|
|
625
|
+
)
|
|
626
|
+
"""Path to a python file with the connection to simulators. Must be a valid Path.
|
|
627
|
+
Can be 'None' for 'analytical', 'data_based' or 'surrogate' functions.
|
|
628
|
+
If 'None', either 'func' or 'surrogates' must not be 'None'."""
|
|
629
|
+
surrogates: list[Path] | None = Field(
|
|
630
|
+
description=(
|
|
631
|
+
"A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based "
|
|
632
|
+
"or 'simulator' functions. If 'None', either 'func' or 'simulator_path' must "
|
|
633
|
+
"not be 'None'."
|
|
634
|
+
),
|
|
635
|
+
default=None,
|
|
636
|
+
)
|
|
637
|
+
"""A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based
|
|
638
|
+
or 'simulator' functions. If 'None', either 'func' or 'simulator_path' must
|
|
639
|
+
not be 'None'."""
|
|
640
|
+
is_linear: bool = Field(
|
|
641
|
+
description="Whether the function expression is linear or not. Defaults to `False`.", default=False
|
|
642
|
+
)
|
|
643
|
+
"""Whether the function expression is linear or not. Defaults to `False`."""
|
|
644
|
+
is_convex: bool = Field(
|
|
645
|
+
description="Whether the function expression is convex or not (non-convex). Defaults to `False`.", default=False
|
|
646
|
+
)
|
|
647
|
+
"""Whether the function expression is convex or not (non-convex). Defaults to `False`."""
|
|
648
|
+
is_twice_differentiable: bool = Field(
|
|
649
|
+
description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
|
|
650
|
+
)
|
|
651
|
+
"""Whether the function expression is twice differentiable or not. Defaults to `False`"""
|
|
652
|
+
scenario_keys: list[str] | None = Field(
|
|
653
|
+
description="Optional. The keys of the scenario the extra functions belongs to.", default=None
|
|
654
|
+
)
|
|
655
|
+
"""Optional. The keys of the scenarios the extra functions belongs to."""
|
|
656
|
+
|
|
657
|
+
_parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
|
|
658
|
+
_parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
|
|
659
|
+
parse_scenario_key_singleton_to_list
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class ScalarizationFunction(BaseModel):
|
|
664
|
+
"""Model for scalarization of the problem."""
|
|
665
|
+
|
|
666
|
+
model_config = ConfigDict(from_attributes=True)
|
|
667
|
+
|
|
668
|
+
name: str = Field(description=("Name of the scalarization function."))
|
|
669
|
+
"""Name of the scalarization function."""
|
|
670
|
+
symbol: str | None = Field(
|
|
671
|
+
description=(
|
|
672
|
+
"Optional symbol to represent the scalarization function. This may be used in UIs and visualizations."
|
|
673
|
+
),
|
|
674
|
+
default=None,
|
|
675
|
+
)
|
|
676
|
+
"""Optional symbol to represent the scalarization function. This may be used
|
|
677
|
+
in UIs and visualizations. Defaults to `None`."""
|
|
678
|
+
func: list = Field(
|
|
679
|
+
description=(
|
|
680
|
+
"Function representation of the scalarization. This is a JSON object that can be parsed into a function."
|
|
681
|
+
"Must be a valid MathJSON object."
|
|
682
|
+
" The symbols in the function must match the symbols defined for objective/variable/constant/extra"
|
|
683
|
+
" function."
|
|
684
|
+
),
|
|
685
|
+
)
|
|
686
|
+
""" Function representation of the scalarization. This is a JSON object that
|
|
687
|
+
can be parsed into a function. Must be a valid MathJSON object. The
|
|
688
|
+
symbols in the function must match the symbols defined for
|
|
689
|
+
objective/variable/constant/extra function."""
|
|
690
|
+
is_linear: bool = Field(
|
|
691
|
+
description="Whether the function expression is linear or not. Defaults to `False`.", default=False
|
|
692
|
+
)
|
|
693
|
+
"""Whether the function expression is linear or not. Defaults to `False`."""
|
|
694
|
+
is_convex: bool = Field(
|
|
695
|
+
description="Whether the function expression is convex or not (non-convex). Defaults to `False`.",
|
|
696
|
+
default=False,
|
|
697
|
+
)
|
|
698
|
+
"""Whether the function expression is convex or not (non-convex). Defaults to `False`."""
|
|
699
|
+
is_twice_differentiable: bool = Field(
|
|
700
|
+
description="Whether the function expression is twice differentiable or not. Defaults to `False`",
|
|
701
|
+
default=False,
|
|
702
|
+
)
|
|
703
|
+
"""Whether the function expression is twice differentiable or not. Defaults to `False`"""
|
|
704
|
+
scenario_keys: list[str] = Field(
|
|
705
|
+
description="Optional. The keys of the scenarios the scalarization function belongs to.", default=None
|
|
706
|
+
)
|
|
707
|
+
"""Optional. The keys of the scenarios the scalarization function belongs to."""
|
|
708
|
+
|
|
709
|
+
_parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
|
|
710
|
+
_parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
|
|
711
|
+
parse_scenario_key_singleton_to_list
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class Simulator(BaseModel):
|
|
716
|
+
"""Model for simulator data."""
|
|
717
|
+
|
|
718
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
719
|
+
|
|
720
|
+
name: str = Field(
|
|
721
|
+
description=("Descriptive name of the simulator. This can be used in UI and visualizations."),
|
|
722
|
+
)
|
|
723
|
+
"""Descriptive name of the simulator. This can be used in UI and visualizations."""
|
|
724
|
+
symbol: str = Field(
|
|
725
|
+
description=(
|
|
726
|
+
"Symbol to represent the simulator. This will be used in the rest of the problem definition."
|
|
727
|
+
" It may also be used in UIs and visualizations."
|
|
728
|
+
),
|
|
729
|
+
)
|
|
730
|
+
file: Path = Field(
|
|
731
|
+
description=("Path to a python file with the connection to simulators."),
|
|
732
|
+
)
|
|
733
|
+
"""Path to a python file with the connection to simulators."""
|
|
734
|
+
parameter_options: dict | None = Field(
|
|
735
|
+
description=(
|
|
736
|
+
"Parameters to the simulator that are not decision variables, but affect the results."
|
|
737
|
+
"Format is similar to decision variables. Can be 'None'."
|
|
738
|
+
),
|
|
739
|
+
default=None,
|
|
740
|
+
)
|
|
741
|
+
"""Parameters to the simulator that are not decision variables, but affect the results.
|
|
742
|
+
Format is similar to decision variables. Can be 'None'."""
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
class Objective(BaseModel):
|
|
746
|
+
"""Model for an objective function."""
|
|
747
|
+
|
|
748
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
749
|
+
|
|
750
|
+
name: str = Field(
|
|
751
|
+
description=(
|
|
752
|
+
"Descriptive name of the objective function. This can be used in UI and visualizations. Example: 'time'."
|
|
753
|
+
),
|
|
754
|
+
)
|
|
755
|
+
"""Descriptive name of the objective function. This can be used in UI and visualizations."""
|
|
756
|
+
symbol: str = Field(
|
|
757
|
+
description=(
|
|
758
|
+
"Symbol to represent the objective function. This will be used in the rest of the problem definition."
|
|
759
|
+
" It may also be used in UIs and visualizations. Example: 'f_1'."
|
|
760
|
+
),
|
|
761
|
+
)
|
|
762
|
+
""" Symbol to represent the objective function. This will be used in the
|
|
763
|
+
rest of the problem definition. It may also be used in UIs and
|
|
764
|
+
visualizations. Example: 'f_1'."""
|
|
765
|
+
unit: str | None = Field(
|
|
766
|
+
description=(
|
|
767
|
+
"The unit of the objective function. This is optional. Used in UIs and visualizations. Example: 'seconds'"
|
|
768
|
+
" or 'millions of hectares'."
|
|
769
|
+
),
|
|
770
|
+
default=None,
|
|
771
|
+
)
|
|
772
|
+
"""The unit of the objective function. This is optional. Used in UIs and visualizations. Example: 'seconds' or
|
|
773
|
+
'millions of hectares'. Defaults to `None`."""
|
|
774
|
+
func: list | None = Field(
|
|
775
|
+
description=(
|
|
776
|
+
"The objective function. This is a JSON object that can be parsed into a function."
|
|
777
|
+
"Must be a valid MathJSON object. The symbols in the function must match the symbols defined for "
|
|
778
|
+
"variable/constant/extra function. Can be 'None' for 'data_based', 'simulator' or "
|
|
779
|
+
"'surrogate' objective functions. If 'None', either 'simulator_path' or 'surrogates' must "
|
|
780
|
+
"not be 'None'."
|
|
781
|
+
),
|
|
782
|
+
default=None,
|
|
783
|
+
)
|
|
784
|
+
""" The objective function. This is a JSON object that can be parsed into a function.
|
|
785
|
+
Must be a valid MathJSON object. The symbols in the function must match the symbols defined for
|
|
786
|
+
variable/constant/extra function. Can be 'None' for 'data_based', 'simulator' or
|
|
787
|
+
'surrogate' objective functions. If 'None', either 'simulator_path' or 'surrogates' must
|
|
788
|
+
not be 'None'."""
|
|
789
|
+
simulator_path: Path | None = Field(
|
|
790
|
+
description=(
|
|
791
|
+
"Path to a python file with the connection to simulators. Must be a valid Path."
|
|
792
|
+
"Can be 'None' for 'analytical', 'data_based' or 'surrogate' objective functions."
|
|
793
|
+
"If 'None', either 'func' or 'surrogates' must not be 'None'."
|
|
794
|
+
),
|
|
795
|
+
default=None,
|
|
796
|
+
)
|
|
797
|
+
"""Path to a python file with the connection to simulators. Must be a valid Path.
|
|
798
|
+
Can be 'None' for 'analytical', 'data_based' or 'surrogate' objective functions.
|
|
799
|
+
If 'None', either 'func' or 'surrogates' must not be 'None'."""
|
|
800
|
+
surrogates: list[Path] | None = Field(
|
|
801
|
+
description=(
|
|
802
|
+
"A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based "
|
|
803
|
+
"or 'simulator' objective functions. If 'None', either 'func' or 'simulator_path' must "
|
|
804
|
+
"not be 'None'."
|
|
805
|
+
),
|
|
806
|
+
default=None,
|
|
807
|
+
)
|
|
808
|
+
"""A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based
|
|
809
|
+
or 'simulator' objective functions. If 'None', either 'func' or 'simulator_path' must
|
|
810
|
+
not be 'None'."""
|
|
811
|
+
maximize: bool = Field(
|
|
812
|
+
description="Whether the objective function is to be maximized or minimized.",
|
|
813
|
+
default=False,
|
|
814
|
+
)
|
|
815
|
+
"""Whether the objective function is to be maximized or minimized. Defaults to `False`."""
|
|
816
|
+
ideal: float | None = Field(description="Ideal value of the objective. This is optional.", default=None)
|
|
817
|
+
"""Ideal value of the objective. This is optional. Defaults to `None`."""
|
|
818
|
+
nadir: float | None = Field(description="Nadir value of the objective. This is optional.", default=None)
|
|
819
|
+
"""Nadir value of the objective. This is optional. Defaults to `None`."""
|
|
820
|
+
|
|
821
|
+
objective_type: ObjectiveTypeEnum = Field(
|
|
822
|
+
description=(
|
|
823
|
+
"The type of objective function. 'analytical' means the objective function value is calculated "
|
|
824
|
+
"based on 'func'. 'data_based' means the objective function value should be retrieved from a table. "
|
|
825
|
+
"In case of 'data_based' objective function, the 'func' field is ignored. Defaults to 'analytical'."
|
|
826
|
+
),
|
|
827
|
+
default=ObjectiveTypeEnum.analytical,
|
|
828
|
+
)
|
|
829
|
+
""" The type of objective function. 'analytical' means the objective
|
|
830
|
+
function value is calculated based on 'func'. 'data_based' means the
|
|
831
|
+
objective function value should be retrieved from a table. In case of
|
|
832
|
+
'data_based' objective function, the 'func' field is ignored. Defaults to
|
|
833
|
+
'analytical'. Defaults to 'analytical'."""
|
|
834
|
+
is_linear: bool = Field(
|
|
835
|
+
description="Whether the function expression is linear or not. Defaults to `False`.", default=False
|
|
836
|
+
)
|
|
837
|
+
"""Whether the function expression is linear or not. Defaults to `False`."""
|
|
838
|
+
is_convex: bool = Field(
|
|
839
|
+
description="Whether the function expression is convex or not (non-convex). Defaults to `False`.", default=False
|
|
840
|
+
)
|
|
841
|
+
"""Whether the function expression is convex or not (non-convex). Defaults to `False`."""
|
|
842
|
+
is_twice_differentiable: bool = Field(
|
|
843
|
+
description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
|
|
844
|
+
)
|
|
845
|
+
"""Whether the function expression is twice differentiable or not. Defaults to `False`"""
|
|
846
|
+
scenario_keys: list[str] | None = Field(
|
|
847
|
+
description="Optional. The keys of the scenarios the objective function belongs to.", default=None
|
|
848
|
+
)
|
|
849
|
+
"""Optional. The keys of the scenarios the objective function belongs to."""
|
|
850
|
+
|
|
851
|
+
_parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
|
|
852
|
+
_parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
|
|
853
|
+
parse_scenario_key_singleton_to_list
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
class Constraint(BaseModel):
|
|
858
|
+
"""Model for a constraint function."""
|
|
859
|
+
|
|
860
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
861
|
+
|
|
862
|
+
name: str = Field(
|
|
863
|
+
description=(
|
|
864
|
+
"Descriptive name of the constraint. This can be used in UI and visualizations. Example: 'maximum length'."
|
|
865
|
+
),
|
|
866
|
+
)
|
|
867
|
+
""" Descriptive name of the constraint. This can be used in UI and
|
|
868
|
+
visualizations. Example: 'maximum length'"""
|
|
869
|
+
symbol: str = Field(
|
|
870
|
+
description=(
|
|
871
|
+
"Symbol to represent the constraint. This will be used in the rest of the problem definition."
|
|
872
|
+
" It may also be used in UIs and visualizations. Example: 'g_1'."
|
|
873
|
+
),
|
|
874
|
+
)
|
|
875
|
+
""" Symbol to represent the constraint. This will be used in the rest of the
|
|
876
|
+
problem definition. It may also be used in UIs and visualizations. Example:
|
|
877
|
+
'g_1'. """
|
|
878
|
+
cons_type: ConstraintTypeEnum = Field(
|
|
879
|
+
description=(
|
|
880
|
+
"The type of the constraint. Constraints are assumed to be in a standard form where the supplied 'func'"
|
|
881
|
+
" expression is on the left hand side of the constraint's expression, and on the right hand side a zero"
|
|
882
|
+
" value is assume. The comparison between the left hand side and right hand side is either and quality"
|
|
883
|
+
" comparison ('=') or lesser than equal comparison ('<=')."
|
|
884
|
+
)
|
|
885
|
+
)
|
|
886
|
+
""" The type of the constraint. Constraints are assumed to be in a standard
|
|
887
|
+
form where the supplied 'func' expression is on the left hand side of the
|
|
888
|
+
constraint's expression, and on the right hand side a zero value is assume.
|
|
889
|
+
The comparison between the left hand side and right hand side is either and
|
|
890
|
+
quality comparison ('=') or lesser than equal comparison ('<=')."""
|
|
891
|
+
func: list | None = Field(
|
|
892
|
+
description=(
|
|
893
|
+
"Function of the constraint. This is a JSON object that can be parsed into a function."
|
|
894
|
+
"Must be a valid MathJSON object."
|
|
895
|
+
" The symbols in the function must match objective/variable/constant symbols."
|
|
896
|
+
"Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'. "
|
|
897
|
+
"If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."
|
|
898
|
+
),
|
|
899
|
+
default=None,
|
|
900
|
+
)
|
|
901
|
+
""" Function of the constraint. This is a JSON object that can be parsed
|
|
902
|
+
into a function. Must be a valid MathJSON object. The symbols in the
|
|
903
|
+
function must match objective/variable/constant symbols.
|
|
904
|
+
Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'.
|
|
905
|
+
If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."""
|
|
906
|
+
simulator_path: Path | None = Field(
|
|
907
|
+
description=(
|
|
908
|
+
"Path to a python file with the connection to simulators. Must be a valid Path."
|
|
909
|
+
"Can be 'None' for if either 'func' or 'surrogates' is not 'None'."
|
|
910
|
+
"If 'None', either 'func' or 'surrogates' must not be 'None'."
|
|
911
|
+
),
|
|
912
|
+
default=None,
|
|
913
|
+
)
|
|
914
|
+
"""Path to a python file with the connection to simulators. Must be a valid Path.
|
|
915
|
+
Can be 'None' for if either 'func' or 'surrogates' is not 'None'.
|
|
916
|
+
If 'None', either 'func' or 'surrogates' must not be 'None'."""
|
|
917
|
+
surrogates: list[Path] | None = Field(
|
|
918
|
+
description=(
|
|
919
|
+
"A list of paths to models saved on disk. Can be 'None' for if either 'func' or 'simulator_path' "
|
|
920
|
+
"is not 'None'. If 'None', either 'func' or 'simulator_path' must not be 'None'."
|
|
921
|
+
),
|
|
922
|
+
default=None,
|
|
923
|
+
)
|
|
924
|
+
"""A list of paths to models saved on disk. Can be 'None' for if either 'func' or 'simulator_path'
|
|
925
|
+
is not 'None'. If 'None', either 'func' or 'simulator_path' must not be 'None'."""
|
|
926
|
+
is_linear: bool = Field(
|
|
927
|
+
description="Whether the constraint is linear or not. Defaults to True, e.g., a linear constraint is assumed.",
|
|
928
|
+
default=True,
|
|
929
|
+
)
|
|
930
|
+
"""Whether the constraint is linear or not. Defaults to True, e.g., a linear
|
|
931
|
+
constraint is assumed. Defaults to `True`."""
|
|
932
|
+
is_convex: bool = Field(
|
|
933
|
+
description="Whether the function expression is convex or not (non-convex). Defaults to `False`.", default=False
|
|
934
|
+
)
|
|
935
|
+
"""Whether the function expression is convex or not (non-convex). Defaults to `False`."""
|
|
936
|
+
is_twice_differentiable: bool = Field(
|
|
937
|
+
description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
|
|
938
|
+
)
|
|
939
|
+
"""Whether the function expression is twice differentiable or not. Defaults to `False`"""
|
|
940
|
+
scenario_keys: list[str] | None = Field(
|
|
941
|
+
description="Optional. The keys of the scenarios the constraint belongs to.", default=None
|
|
942
|
+
)
|
|
943
|
+
"""Optional. The keys of the scenarios the constraint belongs to."""
|
|
944
|
+
|
|
945
|
+
_parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
|
|
946
|
+
_parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
|
|
947
|
+
parse_scenario_key_singleton_to_list
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
class DiscreteRepresentation(BaseModel):
|
|
952
|
+
"""Model to represent discrete objective function and decision variable pairs.
|
|
953
|
+
|
|
954
|
+
Can be used alongside an analytical representation as well.
|
|
955
|
+
|
|
956
|
+
Used with Objectives of type 'data_based' by default. Each of the decision
|
|
957
|
+
variable values and objective functions values are ordered in their
|
|
958
|
+
respective dict entries. This means that the decision variable values found
|
|
959
|
+
at `variable_values['x_i'][j]` correspond to the objective function values
|
|
960
|
+
found at `objective_values['f_i'][j]` for all `i` and some `j`.
|
|
961
|
+
"""
|
|
962
|
+
|
|
963
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
964
|
+
|
|
965
|
+
variable_values: dict[str, list[VariableType]] = Field(
|
|
966
|
+
description=(
|
|
967
|
+
"A dictionary with decision variable values. Each dict key points to a list of all the decision "
|
|
968
|
+
"variable values available for the decision variable given in the key. "
|
|
969
|
+
"The keys must match the 'symbols' defined for the decision variables."
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
""" A dictionary with decision variable values. Each dict key points to a
|
|
973
|
+
list of all the decision variable values available for the decision variable
|
|
974
|
+
given in the key. The keys must match the 'symbols' defined for the
|
|
975
|
+
decision variables."""
|
|
976
|
+
objective_values: dict[str, list[float]] = Field(
|
|
977
|
+
description=(
|
|
978
|
+
"A dictionary with objective function values. Each dict key points to a list of all the objective "
|
|
979
|
+
"function values available for the objective function given in the key. The keys must match the 'symbols' "
|
|
980
|
+
"defined for the objective functions."
|
|
981
|
+
)
|
|
982
|
+
)
|
|
983
|
+
""" A dictionary with objective function values. Each dict key points to a
|
|
984
|
+
list of all the objective function values available for the objective
|
|
985
|
+
function given in the key. The keys must match the 'symbols' defined for the
|
|
986
|
+
objective functions."""
|
|
987
|
+
non_dominated: bool = Field(
|
|
988
|
+
description=(
|
|
989
|
+
"Indicates whether the representation consists of non-dominated points or not."
|
|
990
|
+
"If False, some method can employ non-dominated sorting, which might slow an interactive method down."
|
|
991
|
+
),
|
|
992
|
+
default=False,
|
|
993
|
+
)
|
|
994
|
+
""" Indicates whether the representation consists of non-dominated points or
|
|
995
|
+
not. If False, some method can employ non-dominated sorting, which might
|
|
996
|
+
slow an interactive method down. Defaults to `False`."""
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
class Problem(BaseModel):
|
|
1000
|
+
"""Model for a problem definition."""
|
|
1001
|
+
|
|
1002
|
+
model_config = ConfigDict(frozen=True)
|
|
1003
|
+
|
|
1004
|
+
_scalarization_index: int = PrivateAttr(default=1)
|
|
1005
|
+
# TODO: make init to communicate the _scalarization_index to a new model
|
|
1006
|
+
|
|
1007
|
+
@classmethod
|
|
1008
|
+
def from_problemdb(cls, db_instance: "ProblemDB") -> "Problem":
|
|
1009
|
+
"""."""
|
|
1010
|
+
constants = [Constant.model_validate(const) for const in db_instance.constants] + [
|
|
1011
|
+
TensorConstant.model_validate(const) for const in db_instance.tensor_constants
|
|
1012
|
+
]
|
|
1013
|
+
|
|
1014
|
+
return cls(
|
|
1015
|
+
name=db_instance.name,
|
|
1016
|
+
description=db_instance.description,
|
|
1017
|
+
is_convex=db_instance.is_convex,
|
|
1018
|
+
is_linear=db_instance.is_linear,
|
|
1019
|
+
is_twice_differentiable=db_instance.is_twice_differentiable,
|
|
1020
|
+
variable_domain=db_instance.variable_domain,
|
|
1021
|
+
scenario_keys=db_instance.scenario_keys,
|
|
1022
|
+
constants=constants if constants != [] else None,
|
|
1023
|
+
variables=[Variable.model_validate(var) for var in db_instance.variables]
|
|
1024
|
+
+ [TensorVariable.model_validate(var) for var in db_instance.tensor_variables],
|
|
1025
|
+
objectives=[Objective.model_validate(obj) for obj in db_instance.objectives],
|
|
1026
|
+
constraints=[Constraint.model_validate(const) for const in db_instance.constraints]
|
|
1027
|
+
if db_instance.constraints != []
|
|
1028
|
+
else None,
|
|
1029
|
+
scalarization_funcs=[ScalarizationFunction.model_validate(scal) for scal in db_instance.scalarization_funcs]
|
|
1030
|
+
if db_instance.scalarization_funcs != []
|
|
1031
|
+
else None,
|
|
1032
|
+
extra_funcs=[ExtraFunction.model_validate(extra) for extra in db_instance.extra_funcs]
|
|
1033
|
+
if db_instance.extra_funcs != []
|
|
1034
|
+
else None,
|
|
1035
|
+
discrete_representation=DiscreteRepresentation.model_validate(db_instance.discrete_representation)
|
|
1036
|
+
if db_instance.discrete_representation is not None
|
|
1037
|
+
else None,
|
|
1038
|
+
simulators=[Simulator.model_validate(sim) for sim in db_instance.simulators]
|
|
1039
|
+
if db_instance.simulators != []
|
|
1040
|
+
else None,
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
@model_validator(mode="after")
|
|
1044
|
+
def set_default_scalarization_names(self) -> "Problem":
|
|
1045
|
+
"""Check the scalarization functions for symbols with value 'None'.
|
|
1046
|
+
|
|
1047
|
+
If found, names them systematically
|
|
1048
|
+
'scal_i', where 'i' is a running index stored in an instance attribute.
|
|
1049
|
+
"""
|
|
1050
|
+
if self.scalarization_funcs is None:
|
|
1051
|
+
return self
|
|
1052
|
+
|
|
1053
|
+
for func in self.scalarization_funcs:
|
|
1054
|
+
if func.symbol is None:
|
|
1055
|
+
func.symbol = f"scal_{self._scalarization_index}"
|
|
1056
|
+
self._scalarization_index += 1
|
|
1057
|
+
|
|
1058
|
+
return self
|
|
1059
|
+
|
|
1060
|
+
@model_validator(mode="after")
|
|
1061
|
+
def check_for_non_unique_symbols(self) -> "Problem":
|
|
1062
|
+
"""Check that all the symbols defined in the different fields are unique."""
|
|
1063
|
+
symbols = self.get_all_symbols()
|
|
1064
|
+
|
|
1065
|
+
# symbol is always populated
|
|
1066
|
+
symbol_counts = Counter(symbols)
|
|
1067
|
+
|
|
1068
|
+
# collect duplicates, if they exist
|
|
1069
|
+
duplicates = {symbol: count for symbol, count in symbol_counts.items() if count > 1}
|
|
1070
|
+
|
|
1071
|
+
if duplicates:
|
|
1072
|
+
# if any duplicates are found, raise a value error and report the duplicate symbols.
|
|
1073
|
+
msg = "Non-unique symbols found in the Problem model."
|
|
1074
|
+
for symbol, count in duplicates.items():
|
|
1075
|
+
msg += f" Symbol '{symbol}' occurs {count} times."
|
|
1076
|
+
|
|
1077
|
+
raise ValueError(msg)
|
|
1078
|
+
|
|
1079
|
+
return self
|
|
1080
|
+
|
|
1081
|
+
def get_all_symbols(self) -> list[str]:
|
|
1082
|
+
"""Collects and returns all the symbols symbols currently defined in the model."""
|
|
1083
|
+
# collect all symbols
|
|
1084
|
+
symbols = [variable.symbol for variable in self.variables]
|
|
1085
|
+
symbols += [objective.symbol for objective in self.objectives]
|
|
1086
|
+
if self.constants is not None:
|
|
1087
|
+
symbols += [constant.symbol for constant in self.constants]
|
|
1088
|
+
if self.constraints is not None:
|
|
1089
|
+
symbols += [constraint.symbol for constraint in self.constraints]
|
|
1090
|
+
if self.extra_funcs is not None:
|
|
1091
|
+
symbols += [extra.symbol for extra in self.extra_funcs]
|
|
1092
|
+
if self.scalarization_funcs is not None:
|
|
1093
|
+
symbols += [scalarization.symbol for scalarization in self.scalarization_funcs]
|
|
1094
|
+
|
|
1095
|
+
return symbols
|
|
1096
|
+
|
|
1097
|
+
def add_scalarization(self, new_scal: ScalarizationFunction) -> "Problem":
|
|
1098
|
+
"""Adds a new scalarization function to the model.
|
|
1099
|
+
|
|
1100
|
+
If no symbol is defined, adds a name with the format 'scal_i'.
|
|
1101
|
+
|
|
1102
|
+
Does not modify the original problem model, but instead returns a copy of it with the added
|
|
1103
|
+
scalarization function.
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
new_scal (ScalarizationFunction): Scalarization functions to be added to the model.
|
|
1107
|
+
|
|
1108
|
+
Raises:
|
|
1109
|
+
ValueError: Raised when a ScalarizationFunction is given with a symbol that already exists in the model.
|
|
1110
|
+
|
|
1111
|
+
Returns:
|
|
1112
|
+
Problem: a copy of the problem with the added scalarization function.
|
|
1113
|
+
"""
|
|
1114
|
+
if new_scal.symbol is None:
|
|
1115
|
+
new_scal.symbol = f"scal_{self._scalarization_index}"
|
|
1116
|
+
self._scalarization_index += 1
|
|
1117
|
+
|
|
1118
|
+
if self.scalarization_funcs is None:
|
|
1119
|
+
return self.model_copy(update={"scalarization_funcs": [new_scal]})
|
|
1120
|
+
symbols = self.get_all_symbols()
|
|
1121
|
+
symbols.append(new_scal.symbol)
|
|
1122
|
+
symbol_counts = Counter(symbols)
|
|
1123
|
+
duplicates = {symbol: count for symbol, count in symbol_counts.items() if count > 1}
|
|
1124
|
+
|
|
1125
|
+
if duplicates:
|
|
1126
|
+
msg = "Non-unique symbols found in the Problem model."
|
|
1127
|
+
for symbol, count in duplicates.items():
|
|
1128
|
+
msg += f" Symbol '{symbol}' occurs {count} times."
|
|
1129
|
+
|
|
1130
|
+
raise ValueError(msg)
|
|
1131
|
+
|
|
1132
|
+
return self.model_copy(update={"scalarization_funcs": [*self.scalarization_funcs, new_scal]})
|
|
1133
|
+
|
|
1134
|
+
def update_ideal_and_nadir(
|
|
1135
|
+
self,
|
|
1136
|
+
new_ideal: dict[str, VariableType | None] | None = None,
|
|
1137
|
+
new_nadir: dict[str, VariableType | None] | None = None,
|
|
1138
|
+
) -> "Problem":
|
|
1139
|
+
"""Update the ideal and nadir values of the problem.
|
|
1140
|
+
|
|
1141
|
+
Args:
|
|
1142
|
+
new_ideal (dict[str, VariableType | None] | None): _description_
|
|
1143
|
+
new_nadir (dict[str, VariableType | None] | None): _description_
|
|
1144
|
+
"""
|
|
1145
|
+
updated_objectives = []
|
|
1146
|
+
for objective in self.objectives:
|
|
1147
|
+
new_objective = objective.model_copy(
|
|
1148
|
+
update={
|
|
1149
|
+
**(
|
|
1150
|
+
{"ideal": new_ideal[objective.symbol]}
|
|
1151
|
+
if new_ideal is not None and objective.symbol in new_ideal
|
|
1152
|
+
else {}
|
|
1153
|
+
),
|
|
1154
|
+
**(
|
|
1155
|
+
{"nadir": new_nadir[objective.symbol]}
|
|
1156
|
+
if new_nadir is not None and objective.symbol in new_nadir
|
|
1157
|
+
else {}
|
|
1158
|
+
),
|
|
1159
|
+
}
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
updated_objectives.append(new_objective)
|
|
1163
|
+
|
|
1164
|
+
return self.model_copy(update={"objectives": updated_objectives})
|
|
1165
|
+
|
|
1166
|
+
def add_constraints(self, new_constraints: list[Constraint]) -> "Problem":
|
|
1167
|
+
"""Adds new constraints to the problem model.
|
|
1168
|
+
|
|
1169
|
+
Does not modify the original problem model, but instead returns a copy of it with
|
|
1170
|
+
the added constraints. The symbols of the new constraints to be added must be
|
|
1171
|
+
unique.
|
|
1172
|
+
|
|
1173
|
+
Args:
|
|
1174
|
+
new_constraints (list[Constraint]): the new `Constraint`s to be added to the model.
|
|
1175
|
+
|
|
1176
|
+
Raises:
|
|
1177
|
+
TypeError: when the `new_constraints` is not a list.
|
|
1178
|
+
ValueError: when duplicate symbols are found among the new_constraints, or
|
|
1179
|
+
any of the new constraints utilized an existing symbol in the problem's model.
|
|
1180
|
+
|
|
1181
|
+
Returns:
|
|
1182
|
+
Problem: a copy of the problem with the added constraints.
|
|
1183
|
+
"""
|
|
1184
|
+
if not isinstance(new_constraints, list):
|
|
1185
|
+
# not a list
|
|
1186
|
+
msg = "The argument `new_constraints` must be a list."
|
|
1187
|
+
raise TypeError(msg)
|
|
1188
|
+
|
|
1189
|
+
all_symbols = self.get_all_symbols()
|
|
1190
|
+
new_symbols = [const.symbol for const in new_constraints]
|
|
1191
|
+
|
|
1192
|
+
if len(new_symbols) > len(set(new_symbols)):
|
|
1193
|
+
# duplicate symbols in the new constraint functions
|
|
1194
|
+
msg = "Duplicate symbols found in the new constraint functions to be added."
|
|
1195
|
+
raise ValueError(msg)
|
|
1196
|
+
|
|
1197
|
+
for s in new_symbols:
|
|
1198
|
+
if s in all_symbols:
|
|
1199
|
+
# symbol already exists
|
|
1200
|
+
msg = "A symbol was provided for a new constraint that already exists in the problem definition."
|
|
1201
|
+
raise ValueError(msg)
|
|
1202
|
+
|
|
1203
|
+
# proceed to add the new constraints
|
|
1204
|
+
return self.model_copy(
|
|
1205
|
+
update={
|
|
1206
|
+
"constraints": new_constraints if self.constraints is None else [*self.constraints, *new_constraints]
|
|
1207
|
+
}
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
def add_variables(self, new_variables: list[Variable | TensorVariable]) -> "Problem":
|
|
1211
|
+
"""Adds new variables to the problem model.
|
|
1212
|
+
|
|
1213
|
+
Does not modify the original problem model, but instead returns a copy of it with
|
|
1214
|
+
the added variables. The symbols of the new variables to be added must be
|
|
1215
|
+
unique.
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
new_variables (list[Variable | TensorVariable]): the new variables to be added to the model.
|
|
1219
|
+
|
|
1220
|
+
Raises:
|
|
1221
|
+
TypeError: when the `new_variables` is not a list.
|
|
1222
|
+
ValueError: when duplicate symbols are found among the new_variables, or
|
|
1223
|
+
any of the new variables utilized an existing symbol in the problem's model.
|
|
1224
|
+
|
|
1225
|
+
Returns:
|
|
1226
|
+
Problem: a copy of the problem with the added variables.
|
|
1227
|
+
"""
|
|
1228
|
+
if not isinstance(new_variables, list):
|
|
1229
|
+
# not a list
|
|
1230
|
+
msg = "The argument `new_variables` must be a list."
|
|
1231
|
+
raise TypeError(msg)
|
|
1232
|
+
|
|
1233
|
+
all_symbols = self.get_all_symbols()
|
|
1234
|
+
new_symbols = [const.symbol for const in new_variables]
|
|
1235
|
+
|
|
1236
|
+
if len(new_symbols) > len(set(new_symbols)):
|
|
1237
|
+
# duplicate symbols in the new variables
|
|
1238
|
+
msg = "Duplicate symbols found in the new variables to be added."
|
|
1239
|
+
raise ValueError(msg)
|
|
1240
|
+
|
|
1241
|
+
for s in new_symbols:
|
|
1242
|
+
if s in all_symbols:
|
|
1243
|
+
# symbol already exists
|
|
1244
|
+
msg = "A symbol was provided for a new variable that already exists in the problem definition."
|
|
1245
|
+
raise ValueError(msg)
|
|
1246
|
+
|
|
1247
|
+
# proceed to add the new variables, assumed existing variables are defined
|
|
1248
|
+
return self.model_copy(update={"variables": [*self.variables, *new_variables]})
|
|
1249
|
+
|
|
1250
|
+
def get_flattened_variables(self) -> list[Variable]:
|
|
1251
|
+
"""Return a list of the (flattened) variables of the problem.
|
|
1252
|
+
|
|
1253
|
+
Returns a list of the variables defined for the problem so that any TensorVariables are flattened.
|
|
1254
|
+
|
|
1255
|
+
Returns:
|
|
1256
|
+
list[Variable]: list of (flattened) variables.
|
|
1257
|
+
"""
|
|
1258
|
+
return [
|
|
1259
|
+
item
|
|
1260
|
+
for var in self.variables
|
|
1261
|
+
for item in (var.to_variables() if isinstance(var, TensorVariable) else [var])
|
|
1262
|
+
]
|
|
1263
|
+
|
|
1264
|
+
def get_constraint(self, symbol: str) -> Constraint | None:
|
|
1265
|
+
"""Return a copy of a `Constant` with the given symbol.
|
|
1266
|
+
|
|
1267
|
+
Args:
|
|
1268
|
+
symbol (str): the symbol of the constraint.
|
|
1269
|
+
|
|
1270
|
+
Returns:
|
|
1271
|
+
Constant | None: the copy of the constraint with the given symbol, or `None` if the constraint is not found.
|
|
1272
|
+
Also return `None` if no constraints have been defined for the problem.
|
|
1273
|
+
"""
|
|
1274
|
+
if self.constraints is None:
|
|
1275
|
+
# no constraints defined
|
|
1276
|
+
return None
|
|
1277
|
+
for constraint in self.constraints:
|
|
1278
|
+
if constraint.symbol == symbol:
|
|
1279
|
+
return constraint.model_copy()
|
|
1280
|
+
|
|
1281
|
+
# did not find symbol
|
|
1282
|
+
return None
|
|
1283
|
+
|
|
1284
|
+
def get_variable(self, symbol: str) -> Variable | TensorVariable | None:
|
|
1285
|
+
"""Return a copy of a `Variable` with the given symbol.
|
|
1286
|
+
|
|
1287
|
+
Args:
|
|
1288
|
+
symbol (str): the symbol of the variable.
|
|
1289
|
+
|
|
1290
|
+
Returns:
|
|
1291
|
+
Variable | TensorVariable | None: the copy of the variable with the given symbol,
|
|
1292
|
+
or `None` if the variable is not found.
|
|
1293
|
+
"""
|
|
1294
|
+
for variable in self.variables:
|
|
1295
|
+
if variable.symbol == symbol:
|
|
1296
|
+
# variable found
|
|
1297
|
+
return variable.model_copy()
|
|
1298
|
+
|
|
1299
|
+
# variable not found
|
|
1300
|
+
return None
|
|
1301
|
+
|
|
1302
|
+
def get_objective(self, symbol: str, *, copy: bool = True) -> Objective | None:
|
|
1303
|
+
"""Return a copy of an `Objective` with the given symbol.
|
|
1304
|
+
|
|
1305
|
+
Args:
|
|
1306
|
+
symbol (str): the symbol of the objective.
|
|
1307
|
+
copy (bool): if True, return a copy of the objective, otherwise, return a reference. Defaults to True.
|
|
1308
|
+
|
|
1309
|
+
Returns:
|
|
1310
|
+
Objective | None: the copy of the objective with the given symbol, or `None` if the objective is not found.
|
|
1311
|
+
"""
|
|
1312
|
+
for objective in self.objectives:
|
|
1313
|
+
if objective.symbol == symbol:
|
|
1314
|
+
# objective found
|
|
1315
|
+
if copy:
|
|
1316
|
+
# return a copy of the objective
|
|
1317
|
+
return objective.model_copy()
|
|
1318
|
+
|
|
1319
|
+
# return a reference instead
|
|
1320
|
+
return objective
|
|
1321
|
+
|
|
1322
|
+
# objective not found
|
|
1323
|
+
return None
|
|
1324
|
+
|
|
1325
|
+
def get_scalarization(self, symbol: str) -> ScalarizationFunction | None:
|
|
1326
|
+
"""Return a copy of a `ScalarizationFunction` with the given symbol.
|
|
1327
|
+
|
|
1328
|
+
Args:
|
|
1329
|
+
symbol (str): the symbol of the scalarization function.
|
|
1330
|
+
|
|
1331
|
+
Returns:
|
|
1332
|
+
ScalarizationFunction | None: the copy of the scalarization function with the given symbol, or `None` if the
|
|
1333
|
+
scalarization function is not found. Returns `None` also when no scalarization functions have been
|
|
1334
|
+
defined for the problem.
|
|
1335
|
+
"""
|
|
1336
|
+
if self.scalarization_funcs is None:
|
|
1337
|
+
# no scalarization functions defined
|
|
1338
|
+
return None
|
|
1339
|
+
|
|
1340
|
+
for scal in self.scalarization_funcs:
|
|
1341
|
+
if scal.symbol == symbol:
|
|
1342
|
+
# scalarization function found
|
|
1343
|
+
return scal.model_copy()
|
|
1344
|
+
|
|
1345
|
+
# scalarization function is not found
|
|
1346
|
+
return None
|
|
1347
|
+
|
|
1348
|
+
def get_ideal_point(self) -> dict[str, float | None]:
|
|
1349
|
+
"""Get the ideal point of the problem as an objective dict.
|
|
1350
|
+
|
|
1351
|
+
Returns an objective dict containing the ideal values of the
|
|
1352
|
+
the problem for each objective function. These values may be `None`.
|
|
1353
|
+
|
|
1354
|
+
Returns:
|
|
1355
|
+
dict[str, float | None] | None: an objective dict with the ideal
|
|
1356
|
+
point values (which may be `None`), or `None`.
|
|
1357
|
+
"""
|
|
1358
|
+
return {f"{obj.symbol}": obj.ideal for obj in self.objectives}
|
|
1359
|
+
|
|
1360
|
+
def get_nadir_point(self) -> dict[str, float | None]:
|
|
1361
|
+
"""Get the nadir point of the problem as an objective dict.
|
|
1362
|
+
|
|
1363
|
+
Returns an objective dict containing the nadir values of the
|
|
1364
|
+
the problem for each objective function. These values may be `None`.
|
|
1365
|
+
|
|
1366
|
+
Returns:
|
|
1367
|
+
dict[str, float | None] | None: an objective dict with the nadir
|
|
1368
|
+
point values (which may be `None`), or `None`.
|
|
1369
|
+
"""
|
|
1370
|
+
return {f"{obj.symbol}": obj.nadir for obj in self.objectives}
|
|
1371
|
+
|
|
1372
|
+
@property
|
|
1373
|
+
def variable_domain(self) -> VariableDomainTypeEnum:
|
|
1374
|
+
"""Check the variables defined for the problem and returns the type of their domain.
|
|
1375
|
+
|
|
1376
|
+
Checks the variable types defined for the problem and tells if the
|
|
1377
|
+
problem is continuous, integer, binary, or mixed-integer.
|
|
1378
|
+
|
|
1379
|
+
Returns:
|
|
1380
|
+
VariableDomainEnum: whether the problem is continuous, integer, binary, or mixed-integer.
|
|
1381
|
+
"""
|
|
1382
|
+
variable_types = [var.variable_type for var in self.variables]
|
|
1383
|
+
|
|
1384
|
+
if all(t == VariableTypeEnum.real for t in variable_types):
|
|
1385
|
+
# all variables are real valued -> continuous problem
|
|
1386
|
+
return VariableDomainTypeEnum.continuous
|
|
1387
|
+
|
|
1388
|
+
if all(t == VariableTypeEnum.binary for t in variable_types):
|
|
1389
|
+
# all variables are binary valued -> binary problem
|
|
1390
|
+
return VariableDomainTypeEnum.binary
|
|
1391
|
+
|
|
1392
|
+
if all(t in [VariableTypeEnum.integer, VariableTypeEnum.binary] for t in variable_types):
|
|
1393
|
+
# all variables are integer or binary -> integer problem
|
|
1394
|
+
return VariableDomainTypeEnum.integer
|
|
1395
|
+
|
|
1396
|
+
# mixed problem
|
|
1397
|
+
return VariableDomainTypeEnum.mixed
|
|
1398
|
+
|
|
1399
|
+
@property
|
|
1400
|
+
def is_convex(self) -> bool:
|
|
1401
|
+
"""Check if all the functions expressions in the problem are convex.
|
|
1402
|
+
|
|
1403
|
+
Note:
|
|
1404
|
+
If the field "is_convex" is explicitly set, then the provided value is returned.
|
|
1405
|
+
|
|
1406
|
+
Otherwise, this method just checks all the functions expressions present in the problem
|
|
1407
|
+
and return true if all of them are convex. For complicated problems, this might
|
|
1408
|
+
result in an incorrect results. User discretion is advised.
|
|
1409
|
+
|
|
1410
|
+
Returns:
|
|
1411
|
+
bool: whether the problem is convex or not.
|
|
1412
|
+
"""
|
|
1413
|
+
if self.is_convex_ is not None:
|
|
1414
|
+
return self.is_convex_
|
|
1415
|
+
|
|
1416
|
+
is_convex_values = (
|
|
1417
|
+
[obj.is_convex for obj in self.objectives]
|
|
1418
|
+
+ ([con.is_convex for con in self.constraints] if self.constraints is not None else [])
|
|
1419
|
+
+ ([extra.is_convex for extra in self.extra_funcs] if self.extra_funcs is not None else [])
|
|
1420
|
+
+ ([scal.is_convex for scal in self.scalarization_funcs] if self.scalarization_funcs is not None else [])
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
return all(is_convex_values)
|
|
1424
|
+
|
|
1425
|
+
@property
|
|
1426
|
+
def is_linear(self) -> bool:
|
|
1427
|
+
"""Check if all the functions expressions in the problem are linear.
|
|
1428
|
+
|
|
1429
|
+
Note:
|
|
1430
|
+
If the field "is_linear" is explicitly set, then the provided value is returned.
|
|
1431
|
+
|
|
1432
|
+
Otherwise, this method just checks all the functions expressions present in the problem
|
|
1433
|
+
and return true if all of them are linear. For complicated problems, this might
|
|
1434
|
+
result in an incorrect results. User discretion is advised.
|
|
1435
|
+
|
|
1436
|
+
Returns:
|
|
1437
|
+
bool: whether the problem is linear or not.
|
|
1438
|
+
"""
|
|
1439
|
+
if self.is_linear_ is not None:
|
|
1440
|
+
return self.is_linear_
|
|
1441
|
+
|
|
1442
|
+
is_linear_values = (
|
|
1443
|
+
[obj.is_linear for obj in self.objectives]
|
|
1444
|
+
+ ([con.is_linear for con in self.constraints] if self.constraints is not None else [])
|
|
1445
|
+
+ ([extra.is_linear for extra in self.extra_funcs] if self.extra_funcs is not None else [])
|
|
1446
|
+
+ ([scal.is_linear for scal in self.scalarization_funcs] if self.scalarization_funcs is not None else [])
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
return all(is_linear_values)
|
|
1450
|
+
|
|
1451
|
+
@property
|
|
1452
|
+
def is_twice_differentiable(self) -> bool:
|
|
1453
|
+
"""Check if all the functions expressions in the problem are twice differentiable.
|
|
1454
|
+
|
|
1455
|
+
Note:
|
|
1456
|
+
If the field "is_twice_differentiable" is explicitly set, then the provided value is returned.
|
|
1457
|
+
|
|
1458
|
+
Otherwise, this method just checks all the functions expressions present in the problem
|
|
1459
|
+
and return true if all of them are twice differentiable. For complicated problems, this might
|
|
1460
|
+
result in an incorrect results. User discretion is advised.
|
|
1461
|
+
|
|
1462
|
+
Returns:
|
|
1463
|
+
bool: whether the problem is twice differentiable or not.
|
|
1464
|
+
"""
|
|
1465
|
+
if self.is_twice_differentiable_ is not None:
|
|
1466
|
+
return self.is_twice_differentiable_
|
|
1467
|
+
|
|
1468
|
+
is_diff_values = (
|
|
1469
|
+
[obj.is_twice_differentiable for obj in self.objectives]
|
|
1470
|
+
+ ([con.is_twice_differentiable for con in self.constraints] if self.constraints is not None else [])
|
|
1471
|
+
+ ([extra.is_twice_differentiable for extra in self.extra_funcs] if self.extra_funcs is not None else [])
|
|
1472
|
+
+ (
|
|
1473
|
+
[scal.is_twice_differentiable for scal in self.scalarization_funcs]
|
|
1474
|
+
if self.scalarization_funcs is not None
|
|
1475
|
+
else []
|
|
1476
|
+
)
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
return all(is_diff_values)
|
|
1480
|
+
|
|
1481
|
+
def get_scenario_problem(self, target_keys: str | list[str]) -> "Problem":
|
|
1482
|
+
"""Returns a new Problem with fields belonging to a specified scenario.
|
|
1483
|
+
|
|
1484
|
+
The new problem will have the fields `objectives`, `constraints`, `extra_funcs`,
|
|
1485
|
+
and `scalarization_funcs` with only the entries that belong to the specified
|
|
1486
|
+
scenario. The other entries will remain unchanged.
|
|
1487
|
+
|
|
1488
|
+
Note:
|
|
1489
|
+
Fields with their `scenario_key` being `None` are assumed to belong to all scenarios,
|
|
1490
|
+
and are thus always included in each scenario.
|
|
1491
|
+
|
|
1492
|
+
Args:
|
|
1493
|
+
target_keys (str | list[str]): the key or keys of the scenario(s) we wish to get.
|
|
1494
|
+
|
|
1495
|
+
Raises:
|
|
1496
|
+
ValueError: (some of) the given `target_keys` has not been defined to be a scenario
|
|
1497
|
+
in the problem.
|
|
1498
|
+
|
|
1499
|
+
Returns:
|
|
1500
|
+
Problem: a new problem with only the field that belong to the specified scenario.
|
|
1501
|
+
"""
|
|
1502
|
+
if isinstance(target_keys, str):
|
|
1503
|
+
# if just a single key is given, make a list out of it.abs
|
|
1504
|
+
target_keys = [target_keys]
|
|
1505
|
+
|
|
1506
|
+
# the any matches any keys
|
|
1507
|
+
if self.scenario_keys is None or not any(element in target_keys for element in self.scenario_keys):
|
|
1508
|
+
# invalid scenario
|
|
1509
|
+
msg = (
|
|
1510
|
+
f"The scenario '{target_keys}' has not been defined to be a valid scenario, or the problem has no "
|
|
1511
|
+
"scenarios defined."
|
|
1512
|
+
)
|
|
1513
|
+
raise ValueError(msg)
|
|
1514
|
+
|
|
1515
|
+
# add the fields if the field has the given target_keys in its scenario_keys, or if the
|
|
1516
|
+
# target_keys is None
|
|
1517
|
+
scenario_objectives = [
|
|
1518
|
+
obj
|
|
1519
|
+
for obj in self.objectives
|
|
1520
|
+
if obj.scenario_keys is None or any(element in target_keys for element in obj.scenario_keys)
|
|
1521
|
+
]
|
|
1522
|
+
scenario_constraints = (
|
|
1523
|
+
[
|
|
1524
|
+
cons
|
|
1525
|
+
for cons in self.constraints
|
|
1526
|
+
if cons.scenario_keys is None or any(element in target_keys for element in cons.scenario_keys)
|
|
1527
|
+
]
|
|
1528
|
+
if self.constraints is not None
|
|
1529
|
+
else None
|
|
1530
|
+
)
|
|
1531
|
+
scenario_extras = (
|
|
1532
|
+
[
|
|
1533
|
+
extra
|
|
1534
|
+
for extra in self.extra_funcs
|
|
1535
|
+
if extra.scenario_keys is None or any(element in target_keys for element in extra.scenario_keys)
|
|
1536
|
+
]
|
|
1537
|
+
if self.extra_funcs is not None
|
|
1538
|
+
else None
|
|
1539
|
+
)
|
|
1540
|
+
scenario_scals = (
|
|
1541
|
+
[
|
|
1542
|
+
scal
|
|
1543
|
+
for scal in self.scalarization_funcs
|
|
1544
|
+
if scal.scenario_keys is None or any(element in target_keys for element in scal.scenario_keys)
|
|
1545
|
+
]
|
|
1546
|
+
if self.scalarization_funcs is not None
|
|
1547
|
+
else None
|
|
1548
|
+
)
|
|
1549
|
+
|
|
1550
|
+
return self.model_copy(
|
|
1551
|
+
update={
|
|
1552
|
+
"objectives": scenario_objectives,
|
|
1553
|
+
"constraints": scenario_constraints,
|
|
1554
|
+
"extra_funcs": scenario_extras,
|
|
1555
|
+
"scalarization_funcs": scenario_scals,
|
|
1556
|
+
}
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
def save_to_json(self, path: Path) -> None:
|
|
1560
|
+
"""Save the Problem model in JSON format to a file.
|
|
1561
|
+
|
|
1562
|
+
Args:
|
|
1563
|
+
path (Path): path to the file the model should be saved to.
|
|
1564
|
+
|
|
1565
|
+
"""
|
|
1566
|
+
json_content = self.model_dump_json(indent=4)
|
|
1567
|
+
path.write_text(json_content)
|
|
1568
|
+
|
|
1569
|
+
@classmethod
|
|
1570
|
+
def load_json(cls, path: Path) -> "Problem":
|
|
1571
|
+
"""Load a Problem model stored in a JSON file.
|
|
1572
|
+
|
|
1573
|
+
Args:
|
|
1574
|
+
path (Path): path to file storing a Problem model in JSON format.
|
|
1575
|
+
|
|
1576
|
+
Returns:
|
|
1577
|
+
Problem: the as defined in the data.
|
|
1578
|
+
"""
|
|
1579
|
+
json_data = path.read_text()
|
|
1580
|
+
|
|
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
|
|
1608
|
+
|
|
1609
|
+
name: str = Field(
|
|
1610
|
+
description="Name of the problem.",
|
|
1611
|
+
)
|
|
1612
|
+
"""Name of the problem."""
|
|
1613
|
+
description: str = Field(description="Description of the problem.")
|
|
1614
|
+
"""Description of the problem."""
|
|
1615
|
+
constants: list[Constant | TensorConstant] | None = Field(
|
|
1616
|
+
description="Optional list of the constants present in the problem.", default=None
|
|
1617
|
+
)
|
|
1618
|
+
"""List of the constants present in the problem. Defaults to `None`."""
|
|
1619
|
+
variables: list[Variable | TensorVariable] = Field(description="List of variables present in the problem.")
|
|
1620
|
+
"""List of variables present in the problem."""
|
|
1621
|
+
objectives: list[Objective] = Field(description="List of the objectives present in the problem.")
|
|
1622
|
+
"""List of the objectives present in the problem."""
|
|
1623
|
+
constraints: list[Constraint] | None = Field(
|
|
1624
|
+
description="Optional list of constraints present in the problem.",
|
|
1625
|
+
default=None,
|
|
1626
|
+
)
|
|
1627
|
+
"""Optional list of constraints present in the problem. Defaults to `None`."""
|
|
1628
|
+
extra_funcs: list[ExtraFunction] | None = Field(
|
|
1629
|
+
description="Optional list of extra functions. Use this if some function is repeated multiple times.",
|
|
1630
|
+
default=None,
|
|
1631
|
+
)
|
|
1632
|
+
"""Optional list of extra functions. Use this if some function is repeated multiple times. Defaults to `None`."""
|
|
1633
|
+
scalarization_funcs: list[ScalarizationFunction] | None = Field(
|
|
1634
|
+
description="Optional list of scalarization functions of the problem.", default=None
|
|
1635
|
+
)
|
|
1636
|
+
"""Optional list of scalarization functions of the problem. Defaults to `None`."""
|
|
1637
|
+
discrete_representation: DiscreteRepresentation | None = Field(
|
|
1638
|
+
description=(
|
|
1639
|
+
"Optional. Required when there are one or more 'data_based' Objectives. The corresponding values "
|
|
1640
|
+
"of the 'data_based' objective function will be fetched from this with the given variable values. "
|
|
1641
|
+
"Is also utilized for methods which require both an analytical and discrete representation of a problem."
|
|
1642
|
+
),
|
|
1643
|
+
default=None,
|
|
1644
|
+
)
|
|
1645
|
+
"""Optional. Required when there are one or more 'data_based' Objectives.
|
|
1646
|
+
The corresponding values of the 'data_based' objective function will be
|
|
1647
|
+
fetched from this with the given variable values. Is also utilized for
|
|
1648
|
+
methods which require both an analytical and discrete representation of a
|
|
1649
|
+
problem. Defaults to `None`."""
|
|
1650
|
+
scenario_keys: list[str] | None = Field(
|
|
1651
|
+
description=(
|
|
1652
|
+
"Optional. The scenario keys defined for the problem. Each key will point to a subset of objectives, "
|
|
1653
|
+
"constraints, extra functions, and scalarization functions that have the same scenario key defined to them."
|
|
1654
|
+
"If None, then the problem is assumed to not contain scenarios."
|
|
1655
|
+
),
|
|
1656
|
+
default=None,
|
|
1657
|
+
)
|
|
1658
|
+
"""Optional. The scenario keys defined for the problem. Each key will point
|
|
1659
|
+
to a subset of objectives, " "constraints, extra functions, and
|
|
1660
|
+
scalarization functions that have the same scenario key defined to them."
|
|
1661
|
+
"If None, then the problem is assumed to not contain scenarios."""
|
|
1662
|
+
simulators: list[Simulator] | None = Field(
|
|
1663
|
+
description=(
|
|
1664
|
+
"Optional. The simulators used by the problem. Required when there are one or more "
|
|
1665
|
+
"Objectives defined by simulators. The corresponding values of the 'simulator' objective "
|
|
1666
|
+
"function will be fetched from these simulators with the given variable values."
|
|
1667
|
+
),
|
|
1668
|
+
default=None,
|
|
1669
|
+
)
|
|
1670
|
+
"""Optional. The simulators used by the problem. Required when there are one or more
|
|
1671
|
+
Objectives defined by simulators. The corresponding values of the 'simulator' objective
|
|
1672
|
+
function will be fetched from these simulators with the given variable values.
|
|
1673
|
+
Defaults to `None`."""
|
|
1674
|
+
is_convex_: bool | None = Field(
|
|
1675
|
+
description=(
|
|
1676
|
+
"Optional. Used to manually indicate if the problem, as a whole, can be considered to be convex. "
|
|
1677
|
+
"If set to `None`, this property will be automatically inferred from the "
|
|
1678
|
+
"respective properties of other attributes."
|
|
1679
|
+
),
|
|
1680
|
+
default=None,
|
|
1681
|
+
alias="is_convex",
|
|
1682
|
+
)
|
|
1683
|
+
"""Optional. Used to manually indicate if the problem, as a whole, can be considered to be convex. "
|
|
1684
|
+
"If set to `None`, this property will be automatically inferred from the "
|
|
1685
|
+
"respective properties of other attributes."""
|
|
1686
|
+
is_linear_: bool | None = Field(
|
|
1687
|
+
description=(
|
|
1688
|
+
"Optional. Used to manually indicate if the problem, as a whole, can be considered to be linear. "
|
|
1689
|
+
"If set to `None`, this property will be automatically inferred from the "
|
|
1690
|
+
"respective properties of other attributes."
|
|
1691
|
+
),
|
|
1692
|
+
default=None,
|
|
1693
|
+
alias="is_linear",
|
|
1694
|
+
)
|
|
1695
|
+
"""Optional. Used to manually indicate if the problem, as a whole, can be considered to be linear. "
|
|
1696
|
+
"If set to `None`, this property will be automatically inferred from the "
|
|
1697
|
+
"respective properties of other attributes."""
|
|
1698
|
+
is_twice_differentiable_: bool | None = Field(
|
|
1699
|
+
description=(
|
|
1700
|
+
"Optional. Used to manually indicate if the problem, as a whole, can be considered to be twice "
|
|
1701
|
+
"differentiable. If set to `None`, this property will be automatically inferred from the "
|
|
1702
|
+
"respective properties of other attributes."
|
|
1703
|
+
),
|
|
1704
|
+
default=None,
|
|
1705
|
+
alias="is_twice_differentiable",
|
|
1706
|
+
)
|
|
1707
|
+
"""Optional. Used to manually indicate if the problem, as a whole, can be considered to be twice "
|
|
1708
|
+
"differentiable. If set to `None`, this property will be automatically inferred from the "
|
|
1709
|
+
"respective properties of other attributes."""
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
if __name__ == "__main__":
|
|
1713
|
+
import erdantic as erd
|
|
1714
|
+
|
|
1715
|
+
diagram = erd.create(Problem)
|
|
1716
|
+
diagram.draw("problem_map.png")
|
|
1717
|
+
|
|
1718
|
+
constant_model = Constant(name="constant example", symbol="c", value=42.1)
|
|
1719
|
+
# print(Constant.schema_json(indent=2))
|
|
1720
|
+
# print(constant_model.model_dump_json(indent=2))
|
|
1721
|
+
|
|
1722
|
+
variable_model_1 = Variable(
|
|
1723
|
+
name="example variable",
|
|
1724
|
+
symbol="x_1",
|
|
1725
|
+
variable_type=VariableTypeEnum.real,
|
|
1726
|
+
lowerbound=-0.75,
|
|
1727
|
+
upperbound=11.3,
|
|
1728
|
+
initial_value=4.2,
|
|
1729
|
+
)
|
|
1730
|
+
variable_model_2 = Variable(
|
|
1731
|
+
name="example variable",
|
|
1732
|
+
symbol="x_2",
|
|
1733
|
+
variable_type=VariableTypeEnum.real,
|
|
1734
|
+
lowerbound=-0.75,
|
|
1735
|
+
upperbound=11.3,
|
|
1736
|
+
initial_value=4.2,
|
|
1737
|
+
)
|
|
1738
|
+
variable_model_3 = Variable(
|
|
1739
|
+
name="example variable",
|
|
1740
|
+
symbol="x_3",
|
|
1741
|
+
variable_type=VariableTypeEnum.real,
|
|
1742
|
+
lowerbound=-0.75,
|
|
1743
|
+
upperbound=11.3,
|
|
1744
|
+
initial_value=4.2,
|
|
1745
|
+
)
|
|
1746
|
+
# print(Variable.schema_json(indent=2))
|
|
1747
|
+
# print(variable_model.model_dump_json(indent=2))
|
|
1748
|
+
|
|
1749
|
+
objective_model_1 = Objective(
|
|
1750
|
+
name="example objective",
|
|
1751
|
+
symbol="f_1",
|
|
1752
|
+
func=["Divide", ["Add", "x_1", 3], 2],
|
|
1753
|
+
maximize=False,
|
|
1754
|
+
ideal=-3.3,
|
|
1755
|
+
nadir=5.2,
|
|
1756
|
+
)
|
|
1757
|
+
objective_model_2 = Objective(
|
|
1758
|
+
name="example objective",
|
|
1759
|
+
symbol="f_2",
|
|
1760
|
+
func=["Divide", ["Add", "x_1", 3], 2],
|
|
1761
|
+
maximize=False,
|
|
1762
|
+
ideal=-3.3,
|
|
1763
|
+
nadir=5.2,
|
|
1764
|
+
)
|
|
1765
|
+
objective_model_3 = Objective(
|
|
1766
|
+
name="example objective",
|
|
1767
|
+
symbol="f_3",
|
|
1768
|
+
func=["Divide", ["Add", "x_1", 3], 2],
|
|
1769
|
+
maximize=False,
|
|
1770
|
+
ideal=-3.3,
|
|
1771
|
+
nadir=5.2,
|
|
1772
|
+
)
|
|
1773
|
+
# print(Objective.schema_json(indent=2))
|
|
1774
|
+
# print(objective_model.model_dump_json(indent=2))
|
|
1775
|
+
|
|
1776
|
+
constraint_model = Constraint(
|
|
1777
|
+
name="example constraint",
|
|
1778
|
+
symbol="g_1",
|
|
1779
|
+
func=["Add", ["Add", ["Divide", "x_1", 2], "c"], -4.2],
|
|
1780
|
+
cons_type=ConstraintTypeEnum.LTE,
|
|
1781
|
+
)
|
|
1782
|
+
# print(Constraint.schema_json(indent=2))
|
|
1783
|
+
# print(constraint_model.model_dump_json(indent=2))
|
|
1784
|
+
|
|
1785
|
+
extra_func_model = ExtraFunction(name="example extra function", symbol="m", func=["Divide", "f_1", 100])
|
|
1786
|
+
# print(ExtraFunction.schema_json(indent=2))
|
|
1787
|
+
# print(extra_func_model.model_dump_json(indent=2))
|
|
1788
|
+
|
|
1789
|
+
scalarization_function_model = ScalarizationFunction(
|
|
1790
|
+
name="Achievement scalarizing function",
|
|
1791
|
+
symbol="S",
|
|
1792
|
+
func=["Max", ["Multiply", "w_1", ["Add", "f_1", -1.1]], ["Multiply", "w_2", ["Add", "f_2", -2.2]]],
|
|
1793
|
+
)
|
|
1794
|
+
# print(ScalarizationFunction.schema_json(indent=2))
|
|
1795
|
+
# print(scalarization_function_model.model_dump_json(indent=2))
|
|
1796
|
+
|
|
1797
|
+
problem_model = Problem(
|
|
1798
|
+
name="Example problem",
|
|
1799
|
+
description="This is an example of a the JSON object of the 'Problem' model.",
|
|
1800
|
+
constants=[constant_model],
|
|
1801
|
+
variables=[variable_model_1, variable_model_2, variable_model_3],
|
|
1802
|
+
objectives=[objective_model_1, objective_model_2, objective_model_3],
|
|
1803
|
+
constraints=[constraint_model],
|
|
1804
|
+
extra_funcs=[extra_func_model],
|
|
1805
|
+
scalarization_funcs=[scalarization_function_model],
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
# print(problem_model.model_dump_json(indent=2))
|