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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/adm/ADMAfsar.py +551 -0
  3. desdeo/adm/ADMChen.py +414 -0
  4. desdeo/adm/BaseADM.py +119 -0
  5. desdeo/adm/__init__.py +11 -0
  6. desdeo/api/README.md +73 -0
  7. desdeo/api/__init__.py +15 -0
  8. desdeo/api/app.py +50 -0
  9. desdeo/api/config.py +90 -0
  10. desdeo/api/config.toml +64 -0
  11. desdeo/api/db.py +27 -0
  12. desdeo/api/db_init.py +85 -0
  13. desdeo/api/db_models.py +164 -0
  14. desdeo/api/malaga_db_init.py +27 -0
  15. desdeo/api/models/__init__.py +266 -0
  16. desdeo/api/models/archive.py +23 -0
  17. desdeo/api/models/emo.py +128 -0
  18. desdeo/api/models/enautilus.py +69 -0
  19. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  20. desdeo/api/models/gdm/gdm_base.py +69 -0
  21. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  22. desdeo/api/models/gdm/gnimbus.py +138 -0
  23. desdeo/api/models/generic.py +104 -0
  24. desdeo/api/models/generic_states.py +401 -0
  25. desdeo/api/models/nimbus.py +158 -0
  26. desdeo/api/models/preference.py +128 -0
  27. desdeo/api/models/problem.py +717 -0
  28. desdeo/api/models/reference_point_method.py +18 -0
  29. desdeo/api/models/session.py +49 -0
  30. desdeo/api/models/state.py +463 -0
  31. desdeo/api/models/user.py +52 -0
  32. desdeo/api/models/utopia.py +25 -0
  33. desdeo/api/routers/_EMO.backup +309 -0
  34. desdeo/api/routers/_NAUTILUS.py +245 -0
  35. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  36. desdeo/api/routers/_NIMBUS.py +765 -0
  37. desdeo/api/routers/__init__.py +5 -0
  38. desdeo/api/routers/emo.py +497 -0
  39. desdeo/api/routers/enautilus.py +237 -0
  40. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  41. desdeo/api/routers/gdm/gdm_base.py +420 -0
  42. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  43. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  44. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  45. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  46. desdeo/api/routers/generic.py +233 -0
  47. desdeo/api/routers/nimbus.py +705 -0
  48. desdeo/api/routers/problem.py +307 -0
  49. desdeo/api/routers/reference_point_method.py +93 -0
  50. desdeo/api/routers/session.py +100 -0
  51. desdeo/api/routers/test.py +16 -0
  52. desdeo/api/routers/user_authentication.py +520 -0
  53. desdeo/api/routers/utils.py +187 -0
  54. desdeo/api/routers/utopia.py +230 -0
  55. desdeo/api/schema.py +100 -0
  56. desdeo/api/tests/__init__.py +0 -0
  57. desdeo/api/tests/conftest.py +151 -0
  58. desdeo/api/tests/test_enautilus.py +330 -0
  59. desdeo/api/tests/test_models.py +1179 -0
  60. desdeo/api/tests/test_routes.py +1075 -0
  61. desdeo/api/utils/_database.py +263 -0
  62. desdeo/api/utils/_logger.py +29 -0
  63. desdeo/api/utils/database.py +36 -0
  64. desdeo/api/utils/emo_database.py +40 -0
  65. desdeo/core.py +34 -0
  66. desdeo/emo/__init__.py +159 -0
  67. desdeo/emo/hooks/archivers.py +188 -0
  68. desdeo/emo/methods/EAs.py +541 -0
  69. desdeo/emo/methods/__init__.py +0 -0
  70. desdeo/emo/methods/bases.py +12 -0
  71. desdeo/emo/methods/templates.py +111 -0
  72. desdeo/emo/operators/__init__.py +1 -0
  73. desdeo/emo/operators/crossover.py +1282 -0
  74. desdeo/emo/operators/evaluator.py +114 -0
  75. desdeo/emo/operators/generator.py +459 -0
  76. desdeo/emo/operators/mutation.py +1224 -0
  77. desdeo/emo/operators/scalar_selection.py +202 -0
  78. desdeo/emo/operators/selection.py +1778 -0
  79. desdeo/emo/operators/termination.py +286 -0
  80. desdeo/emo/options/__init__.py +108 -0
  81. desdeo/emo/options/algorithms.py +435 -0
  82. desdeo/emo/options/crossover.py +164 -0
  83. desdeo/emo/options/generator.py +131 -0
  84. desdeo/emo/options/mutation.py +260 -0
  85. desdeo/emo/options/repair.py +61 -0
  86. desdeo/emo/options/scalar_selection.py +66 -0
  87. desdeo/emo/options/selection.py +127 -0
  88. desdeo/emo/options/templates.py +383 -0
  89. desdeo/emo/options/termination.py +143 -0
  90. desdeo/explanations/__init__.py +6 -0
  91. desdeo/explanations/explainer.py +100 -0
  92. desdeo/explanations/utils.py +90 -0
  93. desdeo/gdm/__init__.py +22 -0
  94. desdeo/gdm/gdmtools.py +45 -0
  95. desdeo/gdm/score_bands.py +114 -0
  96. desdeo/gdm/voting_rules.py +50 -0
  97. desdeo/mcdm/__init__.py +41 -0
  98. desdeo/mcdm/enautilus.py +338 -0
  99. desdeo/mcdm/gnimbus.py +484 -0
  100. desdeo/mcdm/nautili.py +345 -0
  101. desdeo/mcdm/nautilus.py +477 -0
  102. desdeo/mcdm/nautilus_navigator.py +656 -0
  103. desdeo/mcdm/nimbus.py +417 -0
  104. desdeo/mcdm/pareto_navigator.py +269 -0
  105. desdeo/mcdm/reference_point_method.py +186 -0
  106. desdeo/problem/__init__.py +83 -0
  107. desdeo/problem/evaluator.py +561 -0
  108. desdeo/problem/external/__init__.py +18 -0
  109. desdeo/problem/external/core.py +356 -0
  110. desdeo/problem/external/pymoo_provider.py +266 -0
  111. desdeo/problem/external/runtime.py +44 -0
  112. desdeo/problem/gurobipy_evaluator.py +562 -0
  113. desdeo/problem/infix_parser.py +341 -0
  114. desdeo/problem/json_parser.py +944 -0
  115. desdeo/problem/pyomo_evaluator.py +487 -0
  116. desdeo/problem/schema.py +1829 -0
  117. desdeo/problem/simulator_evaluator.py +348 -0
  118. desdeo/problem/sympy_evaluator.py +244 -0
  119. desdeo/problem/testproblems/__init__.py +88 -0
  120. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  121. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  122. desdeo/problem/testproblems/cake_problem.py +185 -0
  123. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  124. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  125. desdeo/problem/testproblems/forest_problem.py +283 -0
  126. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  127. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  128. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  129. desdeo/problem/testproblems/momip_problem.py +172 -0
  130. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  131. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  132. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  133. desdeo/problem/testproblems/re_problem.py +492 -0
  134. desdeo/problem/testproblems/river_pollution_problems.py +440 -0
  135. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  136. desdeo/problem/testproblems/simple_problem.py +351 -0
  137. desdeo/problem/testproblems/simulator_problem.py +92 -0
  138. desdeo/problem/testproblems/single_objective.py +289 -0
  139. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  140. desdeo/problem/testproblems/zdt_problem.py +274 -0
  141. desdeo/problem/utils.py +245 -0
  142. desdeo/tools/GenerateReferencePoints.py +181 -0
  143. desdeo/tools/__init__.py +120 -0
  144. desdeo/tools/desc_gen.py +22 -0
  145. desdeo/tools/generics.py +165 -0
  146. desdeo/tools/group_scalarization.py +3090 -0
  147. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  148. desdeo/tools/indicators_binary.py +117 -0
  149. desdeo/tools/indicators_unary.py +362 -0
  150. desdeo/tools/interaction_schema.py +38 -0
  151. desdeo/tools/intersection.py +54 -0
  152. desdeo/tools/iterative_pareto_representer.py +99 -0
  153. desdeo/tools/message.py +265 -0
  154. desdeo/tools/ng_solver_interfaces.py +199 -0
  155. desdeo/tools/non_dominated_sorting.py +134 -0
  156. desdeo/tools/patterns.py +283 -0
  157. desdeo/tools/proximal_solver.py +99 -0
  158. desdeo/tools/pyomo_solver_interfaces.py +477 -0
  159. desdeo/tools/reference_vectors.py +229 -0
  160. desdeo/tools/scalarization.py +2065 -0
  161. desdeo/tools/scipy_solver_interfaces.py +454 -0
  162. desdeo/tools/score_bands.py +627 -0
  163. desdeo/tools/utils.py +388 -0
  164. desdeo/tools/visualizations.py +67 -0
  165. desdeo/utopia_stuff/__init__.py +0 -0
  166. desdeo/utopia_stuff/data/1.json +15 -0
  167. desdeo/utopia_stuff/data/2.json +13 -0
  168. desdeo/utopia_stuff/data/3.json +15 -0
  169. desdeo/utopia_stuff/data/4.json +17 -0
  170. desdeo/utopia_stuff/data/5.json +15 -0
  171. desdeo/utopia_stuff/from_json.py +40 -0
  172. desdeo/utopia_stuff/reinit_user.py +38 -0
  173. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  174. desdeo/utopia_stuff/utopia_problem.py +403 -0
  175. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  176. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  177. desdeo-2.1.0.dist-info/METADATA +186 -0
  178. desdeo-2.1.0.dist-info/RECORD +180 -0
  179. {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  180. desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
  181. desdeo-1.2.dist-info/METADATA +0 -16
  182. desdeo-1.2.dist-info/RECORD +0 -4
@@ -0,0 +1,283 @@
1
+ """This module contains the classes for the publisher-subscriber (ish) pattern.
2
+
3
+ The pattern is used in the evolutionary algorithms to send messages between the different components. This allows
4
+ the components to be decoupled and the messages to be sent between them without the components knowing about each
5
+ other. The pattern closely resembles the publisher-subscriber pattern, with one key difference. The subscribers can
6
+ also create messages and send them to the publisher, which then forwards the messages to the other subscribers.
7
+
8
+ The pattern is implemented with two classes: `Subscriber` and `Publisher`. The `Subscriber` class is an abstract
9
+ class that must be inherited by the classes that want to receive (or send) messages. All evolutionary operators
10
+ must inherit the `Subscriber` class. Some objects that may be interested in the messages, but otherwise unrelated
11
+ to the evolutionary operators, may also inherit the `Subscriber` class. Examples of such objects are a logging class,
12
+ an archive class, or a class that visualizes intermediate results.
13
+
14
+ The `Publisher` class is a class that stores the subscribers and forwards the messages to them. The `Publisher` class
15
+ is not connected to the evolutionary algorithms and only serves as a message router. As mentioned earlier, the
16
+ components do not know about each other, and the `Publisher` class is the only class that knows about all the
17
+ connections in between components. The user of the evolutionary algorithms is responsible for creating the connections.
18
+ However, the implementations of the operators do provide default, so called topics that the operator must subscribe to.
19
+
20
+ The way the pattern works is as follows. Each operator has a `do` method which is called by the evolutionary algorithm
21
+ when the operator is to be executed. This method has some default arguments, depending upon the class of the operator.
22
+ E.g., the `do` method of the mutation related classes may have a default arguments as `offsprings` and `parents`, where
23
+ each is a tuple of decision variables, objectives, and constraints. However, some special mutation operator may require
24
+ additional inputs. E.g., an adaptive mutation operator may require the current generation number as an input. To provide
25
+ this additional input, we do not change the signature of the `do` method.
26
+
27
+ Instead, we let the mutation operator subscribe to a topic called, e.g., `current_generation`. The publisher, is then
28
+ responsible for sending the current generation number to the mutation operator, whenever the generation number changes.
29
+ The mutation operator can then update its internal state based on the received generation number.
30
+
31
+ To be able to send this information, the `Publisher` class has a method called `notify`. Operators can call this method
32
+ to send messages to the subscribers. The idea is to do this at the end of the `do` method. That way, whenever any
33
+ operator is executed, it can send messages to the other operators (which have subscribed to the topics).
34
+
35
+ Note that the operators do not know about the other operators. The subscribers do not know the origin of the messages.
36
+ This decoupling allows for a more modular design and easier extensibility of the evolutionary algorithms.
37
+ """
38
+
39
+ from abc import ABC, abstractmethod
40
+ from collections.abc import Sequence
41
+
42
+ from desdeo.tools.message import AllowedMessagesAtVerbosity, Message, MessageTopics
43
+
44
+
45
+ class Subscriber(ABC):
46
+ """Base class for both subscriber and message sender.
47
+
48
+ These are used in the evolutionary algorithms to send messages between the different components. The pattern
49
+ closely resembles the publisher-subscriber pattern, with one key difference. The subscribers can also create
50
+ messages and send them to the publisher, which then forwards the messages to the other subscribers.
51
+ """
52
+
53
+ @property
54
+ @abstractmethod
55
+ def interested_topics(self) -> Sequence[MessageTopics]:
56
+ """Return the topics the subscriber is interested in."""
57
+
58
+ @property
59
+ @abstractmethod
60
+ def provided_topics(self) -> dict[int, Sequence[MessageTopics]]:
61
+ """Return the topics the subscriber provides to the publisher, grouped by verbosity level."""
62
+
63
+ def __init__(
64
+ self,
65
+ publisher: "Publisher",
66
+ verbosity: int,
67
+ ) -> None:
68
+ """Initialize a subscriber.
69
+
70
+ Args:
71
+ publisher (Callable): the publisher to send messages to.
72
+ verbosity (int, optional): the verbosity level of the messages. A value of 0 means no messages at all.
73
+ """
74
+ if not isinstance(verbosity, int):
75
+ raise TypeError("Verbosity must be an integer.")
76
+ if verbosity < 0:
77
+ raise ValueError("Verbosity must be a non-negative integer.")
78
+ self.publisher = publisher
79
+ self.verbosity: int = verbosity
80
+
81
+ def notify(self) -> None:
82
+ """Notify the publisher of changes in the subject.
83
+
84
+ The contents of the message (a dictionary) are defined in the `state` method. The `state` method can return
85
+ different messages depending on the verbosity level.
86
+ """
87
+ if self.verbosity not in AllowedMessagesAtVerbosity:
88
+ raise ValueError(f"Verbosity level {self.verbosity} is not allowed.")
89
+ if self.verbosity == 0:
90
+ return
91
+
92
+ state = self.state()
93
+ if all(isinstance(x, AllowedMessagesAtVerbosity[self.verbosity]) for x in state):
94
+ self.publisher.notify(messages=state)
95
+
96
+ @abstractmethod
97
+ def update(self, message: Message) -> None:
98
+ """Update self as a result of messages from the publisher.
99
+
100
+ Args:
101
+ message (Message): the message from the publisher. Note that each message is a pydantic model with a topic,
102
+ value, and a source.
103
+ """
104
+
105
+ @abstractmethod
106
+ def state(self) -> Sequence[Message]:
107
+ """Return the state of the subject. This is the list of messages to send to the publisher."""
108
+
109
+
110
+ class Publisher:
111
+ """Class for a publisher that sends messages to subscribers.
112
+
113
+ The publisher is unconnected from the evolutionary algorithms and only serves as a message router. The subscribers
114
+ can subscribe to different message keys and receive messages when the publisher receives a message with the
115
+ corresponding key.
116
+ """
117
+
118
+ def __init__(self) -> None:
119
+ """Initialize a blank publisher."""
120
+ self.subscribers = {}
121
+ self.global_subscribers = []
122
+ self.registered_topics: dict[MessageTopics, list[str]] = {}
123
+
124
+ def subscribe(self, subscriber: Subscriber, topic: MessageTopics) -> None:
125
+ """Store a subscriber for a given message key.
126
+
127
+ Whenever the publisher receives a message with the given key, it will notify the subscriber. This method can
128
+ be used to subscribe to multiple topics by calling it multiple times. Moreover, the user can force the
129
+ subscriber to receive all messages by setting the topic to "ALL".
130
+
131
+ Args:
132
+ subscriber (Subscriber): the subscriber to notify.
133
+ topic (str): the message topic (key in message dictionary) to subscribe to.
134
+ If "ALL", the subscriber is notified of all messages.
135
+ """
136
+ if topic == "ALL":
137
+ self.global_subscribers.append(subscriber)
138
+ return
139
+ if topic not in self.subscribers:
140
+ self.subscribers[topic] = []
141
+ self.subscribers[topic].append(subscriber)
142
+
143
+ def auto_subscribe(self, subscriber: Subscriber) -> None:
144
+ """Store a subscriber for multiple message keys. The subscriber must have the topics attribute.
145
+
146
+ Whenever the publisher receives a message with the given key, it will notify the subscriber.
147
+
148
+ Args:
149
+ subscriber (Subscriber): the subscriber to notify.
150
+ """
151
+ for topic in subscriber.interested_topics:
152
+ self.subscribe(subscriber, topic)
153
+
154
+ def unsubscribe(self, subscriber: Subscriber, topic: str) -> None:
155
+ """Remove a subscriber from a given message key.
156
+
157
+ Args:
158
+ subscriber (Subscriber): the subscriber to remove.
159
+ topic (str): the key of the message to unsubscribe from.
160
+ """
161
+ if topic == "ALL":
162
+ self.global_subscribers.remove(subscriber)
163
+ return
164
+ if topic in self.subscribers:
165
+ self.subscribers[topic].remove(subscriber)
166
+
167
+ def unsubscribe_multiple(self, subscriber: Subscriber, topics: list[str]) -> None:
168
+ """Remove a subscriber from multiple message keys.
169
+
170
+ Args:
171
+ subscriber (Subscriber): the subscriber to remove.
172
+ topics (list[str]): the keys of the messages to unsubscribe from.
173
+ """
174
+ for topic in topics:
175
+ self.unsubscribe(subscriber, topic)
176
+
177
+ def force_unsubscribe(self, subscriber: Subscriber) -> None:
178
+ """Remove a subscriber from all message keys.
179
+
180
+ Args:
181
+ subscriber (Subscriber): the subscriber to remove.
182
+ """
183
+ for topic in self.subscribers:
184
+ if subscriber in self.subscribers[topic]:
185
+ self.subscribers[topic].remove(subscriber)
186
+
187
+ def register_topics(self, topics: list[MessageTopics], source: str) -> None:
188
+ """Register topics provided to the publisher.
189
+
190
+ Args:
191
+ topics (list[MessageTopics]): the topics to register.
192
+ source (str): the source of the topics.
193
+ """
194
+ for topic in topics:
195
+ if topic not in self.registered_topics:
196
+ self.registered_topics[topic] = [source]
197
+ else:
198
+ self.registered_topics[topic].append(source)
199
+
200
+ def check_consistency(self) -> tuple[bool, dict[MessageTopics, list[str]]]:
201
+ """Check if all subscribed topics have also been registered by a source.
202
+
203
+ Returns:
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.
207
+ """
208
+ unregistered_topics = {}
209
+ for topic in self.subscribers:
210
+ if topic not in self.registered_topics:
211
+ unregistered_topics[topic] = [x.__class__.__name__ for x in self.subscribers[topic]]
212
+ if unregistered_topics:
213
+ return False, unregistered_topics
214
+ return True, {}
215
+
216
+ def relationship_map(self):
217
+ """Make a diagram connecting sources to subscribers based on topics."""
218
+ relationships = {}
219
+ for topic in self.subscribers:
220
+ for subscriber in self.subscribers[topic]:
221
+ if topic.value not in relationships:
222
+ relationships[topic.value] = [(subscriber.__class__.__name__, self.registered_topics[topic])]
223
+ else:
224
+ relationships[topic.value].append((subscriber.__class__.__name__, self.registered_topics[topic]))
225
+ return relationships
226
+
227
+ def notify(self, messages: Sequence[Message] | None) -> None:
228
+ """Notify subscribers of the received message/messages.
229
+
230
+ Args:
231
+ messages (Sequence[BaseMessage]): the messages to send to the subscribers. Each message is a pydantic model
232
+ with a topic, value, and a source.
233
+ """
234
+ if messages is None:
235
+ return
236
+ for message in messages:
237
+ # Notify global subscribers
238
+ for subscriber in self.global_subscribers:
239
+ subscriber.update(message)
240
+ # Notify subscribers of the given key
241
+ if message.topic in self.subscribers:
242
+ for subscriber in self.subscribers[message.topic]:
243
+ subscriber.update(message)
244
+
245
+
246
+ def createblanksubs(interested_topics):
247
+ """Create a blank subscriber for testing purposes.
248
+
249
+ Args:
250
+ interested_topics (list[MessageTopics]): the topics the subscriber is interested in.
251
+
252
+ Returns:
253
+ class: the blank subscriber class.
254
+ """
255
+
256
+ class BlankSubscriber(Subscriber):
257
+ """A simple subscriber for testing purposes."""
258
+
259
+ @property
260
+ def interested_topics(self) -> Sequence[MessageTopics]:
261
+ """Return the topics the subscriber is interested in."""
262
+ return interested_topics
263
+
264
+ @property
265
+ def provided_topics(self) -> dict[int, Sequence[MessageTopics]]:
266
+ """Return the topics the subscriber provides to the publisher, grouped by verbosity level."""
267
+ return {0: []}
268
+
269
+ def __init__(self, publisher: "Publisher", verbosity: int = 0) -> None:
270
+ """Initialize a subscriber."""
271
+ super().__init__(publisher, verbosity)
272
+ self.messages_to_send: list[Message] = []
273
+ self.messages_received: list[Message] = []
274
+
275
+ def update(self, message: Message) -> None:
276
+ """Update the internal state of the subscriber."""
277
+ self.messages_received.append(message)
278
+
279
+ def state(self) -> list[Message]:
280
+ """Return the internal state of the subscriber."""
281
+ return self.messages_to_send
282
+
283
+ return BlankSubscriber
@@ -0,0 +1,99 @@
1
+ """Defines solvers meant to be utilized with Problems with discrete representations."""
2
+
3
+ import polars as pl
4
+
5
+ from desdeo.problem import (
6
+ ObjectiveTypeEnum,
7
+ PolarsEvaluator,
8
+ PolarsEvaluatorModesEnum,
9
+ Problem,
10
+ )
11
+ from desdeo.tools.generics import BaseSolver, SolverError, SolverResults
12
+
13
+
14
+ class ProximalSolver(BaseSolver):
15
+ """Creates a solver that finds the closest solution given a fully discrete problem.
16
+
17
+ Note:
18
+ This solver is extremely naive. It will optimize the problem and the result will
19
+ be a point defined for a discrete problem that is closest (Euclidean
20
+ distance) to the optimum. The result may be wildly inaccurate depending on how
21
+ representative the discrete points are of the original problem.
22
+ """
23
+
24
+ def __init__(self, problem: Problem, kwargs: dict | None = None):
25
+ """Creates a solver that assumes the problem being a fully discrete one.
26
+
27
+ Assumes that problem has only data-based objectives and a discrete definition
28
+ that fully defines all the objectives.
29
+
30
+ Args:
31
+ problem (Problem): the problem being solved.
32
+ kwargs (Optional[dict]): optional keyword arguments. Not used right now, but kept
33
+ here for compatibility reasons. Defaults to None.
34
+
35
+ Returns:
36
+ Callable[[str], SolverResults]: a solver that can be called with a target to be optimized.
37
+ This function then returns the results of the optimization as in a `SolverResults` dataclass.
38
+ """
39
+ for obj in problem.objectives:
40
+ if obj.objective_type is not ObjectiveTypeEnum.data_based:
41
+ raise SolverError(f"All objectives must be data-based {obj.symbol}.")
42
+ if problem.discrete_representation is None:
43
+ raise SolverError("Problem must have a discrete representation defined.")
44
+ self.problem = problem
45
+ self.evaluator = PolarsEvaluator(problem, evaluator_mode=PolarsEvaluatorModesEnum.discrete)
46
+
47
+ def solve(self, target: str) -> SolverResults:
48
+ """Solve the problem for the given target.
49
+
50
+ Args:
51
+ target (str): the symbol of the objective function to be optimized.
52
+
53
+ Returns:
54
+ SolverResults: the results fo the optimization.
55
+ """
56
+ results_df = self.evaluator.evaluate()
57
+
58
+ # check constraint values if problem has constraints
59
+ if self.problem.constraints is not None:
60
+ cons_condition = pl.lit(True)
61
+ for constraint in self.problem.constraints:
62
+ cons_condition = cons_condition & (results_df[constraint.symbol] <= 0)
63
+
64
+ results_df = results_df.filter(cons_condition)
65
+
66
+ # find the row with the minimum value in the 'target' column
67
+ closest = results_df.sort(target).head(1)
68
+
69
+ # extract relevant results, extract them as dict for easier jsonification
70
+ variable_values = {variable.symbol: closest[variable.symbol][0] for variable in self.problem.variables}
71
+ objective_values = {objective.symbol: closest[objective.symbol][0] for objective in self.problem.objectives}
72
+ constraint_values = (
73
+ {constraint.symbol: closest[constraint.symbol][0] for constraint in self.problem.constraints}
74
+ if self.problem.constraints is not None
75
+ else None
76
+ )
77
+ extra_func_values = (
78
+ {extra.symbol: closest[extra.symbol][0] for extra in self.problem.extra_funcs}
79
+ if self.problem.extra_funcs is not None
80
+ else None
81
+ )
82
+ scalarization_values = (
83
+ {scal.symbol: closest[scal.symbol][0] for scal in self.problem.scalarization_funcs}
84
+ if self.problem.scalarization_funcs is not None
85
+ else None
86
+ )
87
+ message = f"Optimal value found from tabular data minimizing the column '{target}'."
88
+ success = True
89
+
90
+ # wrap results and return them
91
+ return SolverResults(
92
+ optimal_variables=variable_values,
93
+ optimal_objectives=objective_values,
94
+ constraint_values=constraint_values,
95
+ extra_func_values=extra_func_values,
96
+ scalarization_values=scalarization_values,
97
+ success=success,
98
+ message=message,
99
+ )