desdeo 1.2__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.2.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
- desdeo-1.2.dist-info/METADATA +0 -16
- desdeo-1.2.dist-info/RECORD +0 -4
desdeo/tools/patterns.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
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 = 1,
|
|
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. 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.
|
|
74
|
+
"""
|
|
75
|
+
if not isinstance(verbosity, int):
|
|
76
|
+
raise TypeError("Verbosity must be an integer.")
|
|
77
|
+
self.publisher = publisher
|
|
78
|
+
self.verbosity: int = verbosity
|
|
79
|
+
|
|
80
|
+
def notify(self) -> None:
|
|
81
|
+
"""Notify the publisher of changes in the subject.
|
|
82
|
+
|
|
83
|
+
The contents of the message (a dictionary) are defined in the `state` method. The `state` method can return
|
|
84
|
+
different messages depending on the verbosity level.
|
|
85
|
+
"""
|
|
86
|
+
if self.verbosity not in AllowedMessagesAtVerbosity:
|
|
87
|
+
raise ValueError(f"Verbosity level {self.verbosity} is not allowed.")
|
|
88
|
+
if self.verbosity == 0:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
state = self.state()
|
|
92
|
+
if all(isinstance(x, AllowedMessagesAtVerbosity[self.verbosity]) for x in state):
|
|
93
|
+
self.publisher.notify(messages=state)
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def update(self, message: Message) -> None:
|
|
97
|
+
"""Update self as a result of messages from the publisher.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
message (Message): the message from the publisher. Note that each message is a pydantic model with a topic,
|
|
101
|
+
value, and a source.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def state(self) -> Sequence[Message]:
|
|
106
|
+
"""Return the state of the subject. This is the list of messages to send to the publisher."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Publisher:
|
|
110
|
+
"""Class for a publisher that sends messages to subscribers.
|
|
111
|
+
|
|
112
|
+
The publisher is unconnected from the evolutionary algorithms and only serves as a message router. The subscribers
|
|
113
|
+
can subscribe to different message keys and receive messages when the publisher receives a message with the
|
|
114
|
+
corresponding key.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self) -> None:
|
|
118
|
+
"""Initialize a blank publisher."""
|
|
119
|
+
self.subscribers = {}
|
|
120
|
+
self.global_subscribers = []
|
|
121
|
+
self.registered_topics: dict[MessageTopics, list[str]] = {}
|
|
122
|
+
|
|
123
|
+
def subscribe(self, subscriber: Subscriber, topic: MessageTopics) -> None:
|
|
124
|
+
"""Store a subscriber for a given message key.
|
|
125
|
+
|
|
126
|
+
Whenever the publisher receives a message with the given key, it will notify the subscriber. This method can
|
|
127
|
+
be used to subscribe to multiple topics by calling it multiple times. Moreover, the user can force the
|
|
128
|
+
subscriber to receive all messages by setting the topic to "ALL".
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
subscriber (Subscriber): the subscriber to notify.
|
|
132
|
+
topic (str): the message topic (key in message dictionary) to subscribe to.
|
|
133
|
+
If "ALL", the subscriber is notified of all messages.
|
|
134
|
+
"""
|
|
135
|
+
if topic == "ALL":
|
|
136
|
+
self.global_subscribers.append(subscriber)
|
|
137
|
+
return
|
|
138
|
+
if topic not in self.subscribers:
|
|
139
|
+
self.subscribers[topic] = []
|
|
140
|
+
self.subscribers[topic].append(subscriber)
|
|
141
|
+
|
|
142
|
+
def auto_subscribe(self, subscriber: Subscriber) -> None:
|
|
143
|
+
"""Store a subscriber for multiple message keys. The subscriber must have the topics attribute.
|
|
144
|
+
|
|
145
|
+
Whenever the publisher receives a message with the given key, it will notify the subscriber.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
subscriber (Subscriber): the subscriber to notify.
|
|
149
|
+
"""
|
|
150
|
+
for topic in subscriber.interested_topics:
|
|
151
|
+
self.subscribe(subscriber, topic)
|
|
152
|
+
|
|
153
|
+
def unsubscribe(self, subscriber: Subscriber, topic: str) -> None:
|
|
154
|
+
"""Remove a subscriber from a given message key.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
subscriber (Subscriber): the subscriber to remove.
|
|
158
|
+
topic (str): the key of the message to unsubscribe from.
|
|
159
|
+
"""
|
|
160
|
+
if topic == "ALL":
|
|
161
|
+
self.global_subscribers.remove(subscriber)
|
|
162
|
+
return
|
|
163
|
+
if topic in self.subscribers:
|
|
164
|
+
self.subscribers[topic].remove(subscriber)
|
|
165
|
+
|
|
166
|
+
def unsubscribe_multiple(self, subscriber: Subscriber, topics: list[str]) -> None:
|
|
167
|
+
"""Remove a subscriber from multiple message keys.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
subscriber (Subscriber): the subscriber to remove.
|
|
171
|
+
topics (list[str]): the keys of the messages to unsubscribe from.
|
|
172
|
+
"""
|
|
173
|
+
for topic in topics:
|
|
174
|
+
self.unsubscribe(subscriber, topic)
|
|
175
|
+
|
|
176
|
+
def force_unsubscribe(self, subscriber: Subscriber) -> None:
|
|
177
|
+
"""Remove a subscriber from all message keys.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
subscriber (Subscriber): the subscriber to remove.
|
|
181
|
+
"""
|
|
182
|
+
for topic in self.subscribers:
|
|
183
|
+
if subscriber in self.subscribers[topic]:
|
|
184
|
+
self.subscribers[topic].remove(subscriber)
|
|
185
|
+
|
|
186
|
+
def register_topics(self, topics: list[MessageTopics], source: str) -> None:
|
|
187
|
+
"""Register topics provided to the publisher.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
topics (list[MessageTopics]): the topics to register.
|
|
191
|
+
source (str): the source of the topics.
|
|
192
|
+
"""
|
|
193
|
+
for topic in topics:
|
|
194
|
+
if topic not in self.registered_topics:
|
|
195
|
+
self.registered_topics[topic] = [source]
|
|
196
|
+
else:
|
|
197
|
+
self.registered_topics[topic].append(source)
|
|
198
|
+
|
|
199
|
+
def check_consistency(self) -> bool | tuple[bool, dict[MessageTopics, list[str]]]:
|
|
200
|
+
"""Check if all subscribed topics have also been registered by a source.
|
|
201
|
+
|
|
202
|
+
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.
|
|
205
|
+
"""
|
|
206
|
+
unregistered_topics = {}
|
|
207
|
+
for topic in self.subscribers:
|
|
208
|
+
if topic not in self.registered_topics:
|
|
209
|
+
unregistered_topics[topic] = [x.__class__.__name__ for x in self.subscribers[topic]]
|
|
210
|
+
if unregistered_topics:
|
|
211
|
+
return False, unregistered_topics
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
def relationship_map(self):
|
|
215
|
+
"""Make a diagram connecting sources to subscribers based on topics."""
|
|
216
|
+
relationships = {}
|
|
217
|
+
for topic in self.subscribers:
|
|
218
|
+
for subscriber in self.subscribers[topic]:
|
|
219
|
+
if topic.value not in relationships:
|
|
220
|
+
relationships[topic.value] = [(subscriber.__class__.__name__, self.registered_topics[topic])]
|
|
221
|
+
else:
|
|
222
|
+
relationships[topic.value].append((subscriber.__class__.__name__, self.registered_topics[topic]))
|
|
223
|
+
return relationships
|
|
224
|
+
|
|
225
|
+
def notify(self, messages: Sequence[Message] | None) -> None:
|
|
226
|
+
"""Notify subscribers of the received message/messages.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
messages (Sequence[BaseMessage]): the messages to send to the subscribers. Each message is a pydantic model
|
|
230
|
+
with a topic, value, and a source.
|
|
231
|
+
"""
|
|
232
|
+
if messages is None:
|
|
233
|
+
return
|
|
234
|
+
for message in messages:
|
|
235
|
+
# Notify global subscribers
|
|
236
|
+
for subscriber in self.global_subscribers:
|
|
237
|
+
subscriber.update(message)
|
|
238
|
+
# Notify subscribers of the given key
|
|
239
|
+
if message.topic in self.subscribers:
|
|
240
|
+
for subscriber in self.subscribers[message.topic]:
|
|
241
|
+
subscriber.update(message)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def createblanksubs(interested_topics):
|
|
245
|
+
"""Create a blank subscriber for testing purposes.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
interested_topics (list[MessageTopics]): the topics the subscriber is interested in.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
class: the blank subscriber class.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
class BlankSubscriber(Subscriber):
|
|
255
|
+
"""A simple subscriber for testing purposes."""
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def interested_topics(self) -> Sequence[MessageTopics]:
|
|
259
|
+
"""Return the topics the subscriber is interested in."""
|
|
260
|
+
return interested_topics
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def provided_topics(self) -> dict[int, Sequence[MessageTopics]]:
|
|
264
|
+
"""Return the topics the subscriber provides to the publisher, grouped by verbosity level."""
|
|
265
|
+
return {0: []}
|
|
266
|
+
|
|
267
|
+
def __init__(self, publisher: "Publisher", verbosity: int = 0) -> None:
|
|
268
|
+
"""Initialize a subscriber."""
|
|
269
|
+
super().__init__(publisher, verbosity)
|
|
270
|
+
self.messages_to_send: list[Message] = []
|
|
271
|
+
self.messages_received: list[Message] = []
|
|
272
|
+
|
|
273
|
+
def update(self, message: Message) -> None:
|
|
274
|
+
"""Update the internal state of the subscriber."""
|
|
275
|
+
self.messages_received.append(message)
|
|
276
|
+
|
|
277
|
+
def state(self) -> list[Message]:
|
|
278
|
+
"""Return the internal state of the subscriber."""
|
|
279
|
+
return self.messages_to_send
|
|
280
|
+
|
|
281
|
+
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
|
+
)
|