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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) 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 +5 -1
  87. desdeo/problem/external/__init__.py +18 -0
  88. desdeo/problem/external/core.py +356 -0
  89. desdeo/problem/external/pymoo_provider.py +266 -0
  90. desdeo/problem/external/runtime.py +44 -0
  91. desdeo/problem/infix_parser.py +2 -2
  92. desdeo/problem/pyomo_evaluator.py +25 -6
  93. desdeo/problem/schema.py +69 -48
  94. desdeo/problem/simulator_evaluator.py +65 -15
  95. desdeo/problem/testproblems/__init__.py +26 -11
  96. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  97. desdeo/problem/testproblems/cake_problem.py +185 -0
  98. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  99. desdeo/problem/testproblems/forest_problem.py +77 -69
  100. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  101. desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
  102. desdeo/problem/testproblems/single_objective.py +289 -0
  103. desdeo/problem/testproblems/zdt_problem.py +4 -1
  104. desdeo/tools/__init__.py +39 -21
  105. desdeo/tools/desc_gen.py +22 -0
  106. desdeo/tools/generics.py +22 -2
  107. desdeo/tools/group_scalarization.py +3090 -0
  108. desdeo/tools/indicators_binary.py +107 -1
  109. desdeo/tools/indicators_unary.py +3 -16
  110. desdeo/tools/message.py +33 -2
  111. desdeo/tools/non_dominated_sorting.py +4 -3
  112. desdeo/tools/patterns.py +9 -7
  113. desdeo/tools/pyomo_solver_interfaces.py +48 -35
  114. desdeo/tools/reference_vectors.py +118 -351
  115. desdeo/tools/scalarization.py +340 -1413
  116. desdeo/tools/score_bands.py +491 -328
  117. desdeo/tools/utils.py +117 -49
  118. desdeo/tools/visualizations.py +67 -0
  119. desdeo/utopia_stuff/utopia_problem.py +1 -1
  120. desdeo/utopia_stuff/utopia_problem_old.py +1 -1
  121. {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info}/METADATA +46 -28
  122. desdeo-2.1.0.dist-info/RECORD +180 -0
  123. {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  124. desdeo-2.0.0.dist-info/RECORD +0 -120
  125. /desdeo/api/utils/{logger.py → _logger.py} +0 -0
  126. {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info/licenses}/LICENSE +0 -0
@@ -8,4 +8,110 @@ If these conditions are not met, the results of the indicators will not be meani
8
8
  Additionally, the set may be assumed to only contain mutually non-dominated solutions, depending on the indicator.
9
9
 
10
10
  For now, we rely on pymoo for the implementation of many of the indicators.
11
- """
11
+ """
12
+
13
+ from typing import Literal
14
+
15
+ import numpy as np
16
+ from moocore import epsilon_additive, epsilon_mult
17
+ from numba import njit
18
+ from desdeo.tools.non_dominated_sorting import dominates
19
+ from desdeo.tools.indicators_unary import hv
20
+
21
+ """
22
+ Note that the moocore package includes a more complex implementation for calculating the epsilon_indicator for two
23
+ solution *sets*.
24
+ """
25
+
26
+
27
+ @njit()
28
+ def epsilon_component(solution1: np.ndarray, solution2: np.ndarray) -> float:
29
+ """Computes the additive epsilon-indicator between two solutions.
30
+
31
+ Basically, returns the minimum amount by which the values in solution1 must be translated (minimization assumed)
32
+ such that it (weakly) dominates solution2. If solution1 already dominates solution2, returns 0.0.
33
+
34
+ Args:
35
+ solution1 (np.ndarray): Should be an one-dimensional array, where each value is normalized between [0, 1]
36
+ solution2 (np.ndarray): Should be an one-dimensional array, where each value is normalized between [0, 1]
37
+
38
+ Returns:
39
+ float: The maximum distance between the values in s1 and s2.
40
+ """
41
+ return max(0.0, max(solution1 - solution2))
42
+
43
+
44
+ @njit()
45
+ def self_epsilon(solution_set: np.ndarray) -> np.ndarray:
46
+ """Computes the pairwise additive epsilon-indicator for a solution set.
47
+
48
+ Args:
49
+ solution_set (np.ndarray): Should be a two-dimensional array, where each row is a
50
+ solution normalized between [0, 1].
51
+
52
+ Returns:
53
+ np.ndarray: A two-dimensional array where the entry at (i, j) is the
54
+ additive epsilon-indicator between the i-th and j-th solution in the set.
55
+ """
56
+ n_solutions = solution_set.shape[0]
57
+ eps_matrix = np.zeros((n_solutions, n_solutions), dtype=np.float64)
58
+ for i in range(n_solutions):
59
+ for j in range(n_solutions):
60
+ eps_matrix[i, j] = epsilon_component(solution_set[i], solution_set[j])
61
+ return eps_matrix
62
+
63
+
64
+ def epsilon_indicator(
65
+ set1: np.ndarray, set2: np.ndarray, kind: Literal["additive", "multiplicative"] = "additive"
66
+ ) -> float:
67
+ """Computes the additive epsilon-indicator between two solution sets.
68
+
69
+ Args:
70
+ set1 (np.ndarray): Should be a two-dimensional array, where each row is a solution normalized between [0, 1]
71
+ set2 (np.ndarray): Should be a two-dimensional array, where each row is a solution normalized between [0, 1]
72
+ kind (Literal["additive", "multiplicative"]): The kind of epsilon-indicator to compute. Defaults to "additive".
73
+
74
+ Returns:
75
+ float: the epsilon-indicator between the two sets.
76
+ """
77
+ if kind == "additive":
78
+ return epsilon_additive(set1, ref=set2)
79
+ if kind == "multiplicative":
80
+ return epsilon_mult(set1, ref=set2)
81
+ raise ValueError(f"Unknown kind: {kind}. Use 'additive' or 'multiplicative'.")
82
+
83
+
84
+ def hv_component(solution1: np.ndarray, solution2: np.ndarray, ref: float = 2.0) -> float:
85
+ """Computes the hypervolume contribution of solution1 with respect to solution2.
86
+
87
+ Args:
88
+ solution1 (np.ndarray): Should be an one-dimensional array, where each value is normalized between [0, 1]
89
+ solution2 (np.ndarray): Should be an one-dimensional array, where each value is normalized between [0, 1]
90
+ ref (float): The reference point for the hypervolume calculation. Defaults to 2.0.
91
+
92
+ Returns:
93
+ float: The hypervolume contribution of solution1 with respect to solution2.
94
+ """
95
+ if dominates(solution1, solution2):
96
+ return np.prod(ref - solution2) - np.prod(ref - solution1)
97
+ return hv(solution_set=np.array([solution1, solution2]), reference_point_component=ref)
98
+
99
+
100
+ def self_hv(solution_set: np.ndarray, ref: float = 2.0) -> np.ndarray:
101
+ """Computes the pairwise hypervolume contribution for a solution set.
102
+
103
+ Args:
104
+ solution_set (np.ndarray): Should be a two-dimensional array, where each row is a
105
+ solution normalized between [0, 1].
106
+ ref (float): The reference point for the hypervolume calculation. Defaults to 2.0.
107
+
108
+ Returns:
109
+ np.ndarray: A two-dimensional array where the entry at (i, j) is the
110
+ hypervolume contribution of the i-th solution with respect to the j-th solution in the set.
111
+ """
112
+ n_solutions = solution_set.shape[0]
113
+ hv_matrix = np.zeros((n_solutions, n_solutions), dtype=np.float64)
114
+ for i in range(n_solutions):
115
+ for j in range(n_solutions):
116
+ hv_matrix[i, j] = hv_component(solution_set[i], solution_set[j], ref=ref)
117
+ return hv_matrix
@@ -18,7 +18,7 @@ from warnings import warn
18
18
 
19
19
  import numpy as np
20
20
  from pydantic import BaseModel, Field
21
- from pymoo.indicators.hv import Hypervolume
21
+ from moocore import Hypervolume
22
22
  from pymoo.indicators.rmetric import RMetric
23
23
  from scipy.spatial.distance import cdist
24
24
  from typing import Dict
@@ -37,15 +37,8 @@ def hv(solution_set: np.ndarray, reference_point_component: float) -> float:
37
37
  Returns:
38
38
  float: The hypervolume indicator value.
39
39
  """
40
- rp = np.full(solution_set.shape[1], reference_point_component, dtype=np.float64)
41
- ideal = np.zeros(solution_set.shape[1], dtype=np.float64)
42
- nadir = np.ones(solution_set.shape[1], dtype=np.float64)
43
-
44
- # Sets the ideal and nadir to (0, 0, ..., 0) and (1, 1, ..., 1) respectively.
45
- # Turns of non-domination checks.
46
- # Turns of normalization of the reference point
47
- hv = Hypervolume(ref_point=rp, ideal=ideal, nadir=nadir, nds=False, norm_ref_point=False)
48
40
 
41
+ hv = Hypervolume(reference_point_component)
49
42
  ind = hv(solution_set)
50
43
 
51
44
  if ind is None:
@@ -80,13 +73,7 @@ def hv_batch(
80
73
  num_objs = solution_sets[next(iter(solution_sets.keys()))].shape[1]
81
74
 
82
75
  for rp in reference_points_component:
83
- hv = Hypervolume(
84
- ref_point=np.full(num_objs, rp, dtype=np.float64),
85
- ideal=np.zeros(num_objs, dtype=np.float64),
86
- nadir=np.ones(num_objs, dtype=np.float64),
87
- nds=False,
88
- norm_ref_point=False,
89
- )
76
+ hv = Hypervolume(rp)
90
77
  for set_name in solution_sets:
91
78
  ind = hv(solution_sets[set_name])
92
79
  if ind is None:
desdeo/tools/message.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from enum import Enum
4
4
  from typing import Any, Literal
5
5
 
6
+ import numpy as np
6
7
  from polars import DataFrame
7
8
  from pydantic import BaseModel, ConfigDict, Field, field_serializer
8
9
 
@@ -22,6 +23,8 @@ class CrossoverMessageTopics(Enum):
22
23
  """ The offsprings generated from the crossover. """
23
24
  ALPHA = "ALPHA"
24
25
  """ Alpha parameter used in crossover. """
26
+ LAMBDA = "LAMBDA"
27
+ """ Lambda parameter used in crossover. Primarily used in the bounded exponential xover. """
25
28
 
26
29
 
27
30
  class MutationMessageTopics(Enum):
@@ -100,6 +103,8 @@ class SelectorMessageTopics(Enum):
100
103
  """ The individuals selected by the selector. """
101
104
  SELECTED_OUTPUTS = "SELECTED_OUTPUTS"
102
105
  """ The targets of the selected individuals. """
106
+ SELECTED_FITNESS = "SELECTED_FITNESS"
107
+ """ The fitness of the selected individuals. This is the fitness calculated by the selector, not the objectives."""
103
108
  SELECTED_VERBOSE_OUTPUTS = "SELECTED_VERBOSE_OUTPUTS"
104
109
  """ Same as SELECTED_OUTPUTS + SELECTED_INDIVIDUALS"""
105
110
  REFERENCE_VECTORS = "REFERENCE_VECTORS"
@@ -125,6 +130,12 @@ class TerminatorMessageTopics(Enum):
125
130
  """ The maximum number of evaluations. """
126
131
 
127
132
 
133
+ class ReferenceVectorMessageTopics(Enum):
134
+ """Topics for messages related to the reference vectors."""
135
+
136
+ TEST = "TEST"
137
+
138
+
128
139
  MessageTopics = (
129
140
  CrossoverMessageTopics
130
141
  | MutationMessageTopics
@@ -132,7 +143,10 @@ MessageTopics = (
132
143
  | GeneratorMessageTopics
133
144
  | SelectorMessageTopics
134
145
  | TerminatorMessageTopics
135
- | Literal["ALL"] # Used to indicate that all topics are of interest to a subscriber.
146
+ | ReferenceVectorMessageTopics
147
+ | Literal[
148
+ "ALL"
149
+ ] # Used to indicate that all topics are of interest to a subscriber.
136
150
  )
137
151
 
138
152
 
@@ -176,7 +190,9 @@ class BoolMessage(BaseMessage):
176
190
  class DictMessage(BaseMessage):
177
191
  """A message containing a dictionary value."""
178
192
 
179
- value: dict[str, Any] = Field(..., description="The dictionary value of the message.")
193
+ value: dict[str, Any] = Field(
194
+ ..., description="The dictionary value of the message."
195
+ )
180
196
  """ The dictionary value of the message. """
181
197
 
182
198
 
@@ -200,6 +216,19 @@ class PolarsDataFrameMessage(BaseMessage):
200
216
  return value.to_dict(as_series=False)
201
217
 
202
218
 
219
+ class NumpyArrayMessage(BaseMessage):
220
+ """A message containing a numpy array value."""
221
+
222
+ value: np.ndarray = Field(..., description="The numpy array value of the message.")
223
+ """ The numpy array value of the message. """
224
+
225
+ model_config = ConfigDict(arbitrary_types_allowed=True)
226
+
227
+ @field_serializer("value")
228
+ def _serialize_value(self, value: np.ndarray) -> list[list[float]]:
229
+ return value.tolist()
230
+
231
+
203
232
  class GenericMessage(BaseMessage):
204
233
  """A message containing a generic value."""
205
234
 
@@ -216,6 +245,7 @@ Message = (
216
245
  | StringMessage
217
246
  | BoolMessage
218
247
  | PolarsDataFrameMessage
248
+ | NumpyArrayMessage
219
249
  )
220
250
 
221
251
  AllowedMessagesAtVerbosity: dict[int, tuple[type[Message], ...]] = {
@@ -230,5 +260,6 @@ AllowedMessagesAtVerbosity: dict[int, tuple[type[Message], ...]] = {
230
260
  Array2DMessage,
231
261
  GenericMessage,
232
262
  PolarsDataFrameMessage,
263
+ NumpyArrayMessage,
233
264
  ),
234
265
  }
@@ -76,10 +76,11 @@ def fast_non_dominated_sort(data: np.ndarray) -> np.ndarray:
76
76
  fronts[i] = current_front_all
77
77
 
78
78
  taken = taken + fronts[i]
79
- if not fronts[i].any():
80
- # if the current front is empty or if all the solutions have been sorted, stop
79
+ if taken.all():
80
+ # if all the solutions have been sorted, stop
81
81
  break
82
- return fronts[:i]
82
+
83
+ return fronts[: i + 1]
83
84
 
84
85
 
85
86
  def fast_non_dominated_sort_indices(data: np.ndarray) -> list[np.ndarray]:
desdeo/tools/patterns.py CHANGED
@@ -63,17 +63,18 @@ class Subscriber(ABC):
63
63
  def __init__(
64
64
  self,
65
65
  publisher: "Publisher",
66
- verbosity: int = 1,
66
+ verbosity: int,
67
67
  ) -> None:
68
68
  """Initialize a subscriber.
69
69
 
70
70
  Args:
71
71
  publisher (Callable): the publisher to send messages to.
72
- verbosity (int, optional): the verbosity level of the messages. Defaults to 1, which may mean differing
73
- amounts of information depending on the message sender. A value of 0 means no messages at all.
72
+ verbosity (int, optional): the verbosity level of the messages. A value of 0 means no messages at all.
74
73
  """
75
74
  if not isinstance(verbosity, int):
76
75
  raise TypeError("Verbosity must be an integer.")
76
+ if verbosity < 0:
77
+ raise ValueError("Verbosity must be a non-negative integer.")
77
78
  self.publisher = publisher
78
79
  self.verbosity: int = verbosity
79
80
 
@@ -196,12 +197,13 @@ class Publisher:
196
197
  else:
197
198
  self.registered_topics[topic].append(source)
198
199
 
199
- def check_consistency(self) -> bool | tuple[bool, dict[MessageTopics, list[str]]]:
200
+ def check_consistency(self) -> tuple[bool, dict[MessageTopics, list[str]]]:
200
201
  """Check if all subscribed topics have also been registered by a source.
201
202
 
202
203
  Returns:
203
- bool | tuple[bool, dict[MessageTopics, list[str]]]: True if all subscribed topics have been registered by a
204
- source. False otherwise. If False, also return the unregistered topics that have been subscribed to.
204
+ tuple[bool, dict[MessageTopics, list[str]]]: Returns a tuple. The first element is a bool. True if all
205
+ subscribed topics have been registered by a source. False otherwise. The second element is a dictionary
206
+ of unregistered topics that have been subscribed to.
205
207
  """
206
208
  unregistered_topics = {}
207
209
  for topic in self.subscribers:
@@ -209,7 +211,7 @@ class Publisher:
209
211
  unregistered_topics[topic] = [x.__class__.__name__ for x in self.subscribers[topic]]
210
212
  if unregistered_topics:
211
213
  return False, unregistered_topics
212
- return True
214
+ return True, {}
213
215
 
214
216
  def relationship_map(self):
215
217
  """Make a diagram connecting sources to subscribers based on topics."""
@@ -80,7 +80,7 @@ class IpoptOptions(BaseModel):
80
80
  max_iter: int = Field(description="Maximum number of iterations. Must be >1. Defaults to 3000.", default=3000)
81
81
  """Maximum number of iterations. Must be >1. Defaults to 3000."""
82
82
 
83
- print_level: str = Field(
83
+ print_level: int = Field(
84
84
  description="The verbosity level of the solver's output. Ranges between 0 and 12. Defaults to 5.", default=5
85
85
  )
86
86
  """The verbosity level of the solver's output. Ranges between 0 and 12."""
@@ -100,61 +100,61 @@ class CbcOptions(BaseModel):
100
100
  model_config = ConfigDict(frozen=True, populate_by_name=True)
101
101
 
102
102
  sec: int = Field(
103
- description="The maximum amount of time (in seconds) the solver should run. Defaults to None.", default=None
103
+ description="The maximum amount of time (in seconds) the solver should run. Defaults to 600.", default=600
104
104
  )
105
- """The maximum amount of time (in seconds) the solver should run. Defaults to None."""
105
+ """The maximum amount of time (in seconds) the solver should run. Defaults to 600."""
106
106
 
107
107
  threads: int = Field(
108
- description="Number of threads (cores) to use for solving the problem. Defaults to 1.", default=1
108
+ description="Number of threads (cores) to use for solving the problem. Defaults to 4.", default=4
109
109
  )
110
- """Number of threads (cores) to use for solving the problem. Defaults to 1."""
110
+ """Number of threads (cores) to use for solving the problem. Defaults to 4."""
111
111
 
112
112
  log_level: int = Field(
113
113
  alias="logLevel",
114
114
  description=(
115
115
  "Controls the level of logging output. Values range from 0 (no output) to 5 (very detailed output)."
116
- " Defaults to 1."
116
+ " Defaults to 2."
117
117
  ),
118
- default=1,
118
+ default=2,
119
119
  )
120
120
  """Controls the level of logging output. Values range from 0 (no output) to 5 (very detailed output).
121
- Defaults to 1.
121
+ Defaults to 2.
122
122
  """
123
123
 
124
124
  max_solutions: int = Field(
125
125
  alias="maxSolutions",
126
- description="Limits the number of feasible solutions found by the solver. Defaults to None.",
127
- default=None,
126
+ description="Limits the number of feasible solutions found by the solver. Defaults to 10.",
127
+ default=10,
128
128
  )
129
- """Limits the number of feasible solutions found by the solver. Defaults to None."""
129
+ """Limits the number of feasible solutions found by the solver. Defaults to 10."""
130
130
 
131
131
  max_nodes: int = Field(
132
132
  alias="maxNodes",
133
- description="Sets the maximum number of branch-and-bound nodes to explore. Defaults to None.",
134
- default=None,
133
+ description="Sets the maximum number of branch-and-bound nodes to explore. Defaults to 1000.",
134
+ default=1000,
135
135
  )
136
- """Sets the maximum number of branch-and-bound nodes to explore. Defaults to None."""
136
+ """Sets the maximum number of branch-and-bound nodes to explore. Defaults to 1000."""
137
137
 
138
138
  ratio_gap: float = Field(
139
139
  alias="ratioGap",
140
140
  description=(
141
141
  "Sets the relative MIP gap (as a fraction of the optimal solution value) at which the solver will"
142
- " terminate. Defaults to None."
142
+ " terminate. Defaults to 0.01."
143
143
  ),
144
- default=None,
144
+ default=0.01,
145
145
  )
146
146
  """Sets the relative MIP gap (as a fraction of the optimal solution value) at which the solver will terminate.
147
- Defaults to None.
147
+ Defaults to 0.01.
148
148
  """
149
149
 
150
150
  absolute_gap: float = Field(
151
151
  alias="absoluteGap",
152
152
  description=(
153
- "Sets the absolute MIP gap (an absolute value) at which the solver will terminate. Defaults to None."
153
+ "Sets the absolute MIP gap (an absolute value) at which the solver will terminate. Defaults to 1.0."
154
154
  ),
155
- default=None,
155
+ default=1.0,
156
156
  )
157
- """Sets the absolute MIP gap (an absolute value) at which the solver will terminate. Defaults to None."""
157
+ """Sets the absolute MIP gap (an absolute value) at which the solver will terminate. Defaults to 1.0."""
158
158
 
159
159
  solve: str = Field(
160
160
  description=(
@@ -168,9 +168,9 @@ class CbcOptions(BaseModel):
168
168
  """
169
169
 
170
170
  presolve: int = Field(
171
- description="Controls the presolve level (0: no presolve, 1: default, 2: aggressive). Defaults to 1.", default=1
171
+ description="Controls the presolve level (0: no presolve, 1: default, 2: aggressive). Defaults to 2.", default=2
172
172
  )
173
- """Controls the presolve level (0: no presolve, 1: default, 2: aggressive). Defaults to 1."""
173
+ """Controls the presolve level (0: no presolve, 1: default, 2: aggressive). Defaults to 2."""
174
174
 
175
175
  feasibility_tolerance: float = Field(
176
176
  alias="feasibilityTolerance",
@@ -187,19 +187,7 @@ class CbcOptions(BaseModel):
187
187
  """Sets the tolerance for integrality of integer variables. Defaults to 1e-5."""
188
188
 
189
189
 
190
- _default_cbc_options = CbcOptions(
191
- sec=600,
192
- threads=4,
193
- logLevel=2,
194
- maxSolutions=10,
195
- maxNodes=1000,
196
- ratioGap=0.01,
197
- absoluteGap=1.0,
198
- solve="branchAndCut",
199
- presolve=2,
200
- feasibilityTolerance=1e-6,
201
- integerTolerance=1e-5,
202
- )
190
+ _default_cbc_options = CbcOptions()
203
191
  """Defines CBC options with default values."""
204
192
 
205
193
  _default_bonmin_options = BonminOptions()
@@ -243,6 +231,31 @@ def parse_pyomo_optimizer_results(
243
231
  constraint_values = (
244
232
  {con.symbol: results[con.symbol] for con in problem.constraints} if problem.constraints else None
245
233
  )
234
+
235
+ # handle constraint, which might be multi-valued
236
+ if problem.constraints is not None:
237
+ constraint_values = {}
238
+
239
+ for con in problem.constraints:
240
+ result = results[con.symbol]
241
+
242
+ if isinstance(result, dict):
243
+ # multi-valued
244
+ indices = list(getattr(evaluator.model, con.symbol).keys())
245
+ shape = tuple(len({idx[k] for idx in indices}) for k in range(len(indices[0])))
246
+ values_list = np.zeros(shape)
247
+
248
+ for idx in indices:
249
+ values_list[*[i - 1 for i in idx]] = result[idx]
250
+
251
+ constraint_values[con.symbol] = values_list.tolist()
252
+
253
+ else:
254
+ # scalar-valued
255
+ constraint_values[con.symbol] = result
256
+ else:
257
+ constraint_values = None
258
+
246
259
  extra_func_values = (
247
260
  {extra.symbol: results[extra.symbol] for extra in problem.extra_funcs}
248
261
  if problem.extra_funcs is not None