desdeo 2.0.0__py3-none-any.whl → 2.1.1__py3-none-any.whl

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