1 | from decimal import Decimal
|
---|
2 | import logging
|
---|
3 | import math
|
---|
4 | from random import randint
|
---|
5 | import time
|
---|
6 | from typing import Callable, cast
|
---|
7 | from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
|
---|
8 |
|
---|
9 | from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
|
---|
10 | from tudelft.utilities.immutablelist.ImmutableList import ImmutableList
|
---|
11 | from ...time_dependent_agent.extended_util_space import ExtendedUtilSpace
|
---|
12 |
|
---|
13 | from geniusweb.actions.Accept import Accept
|
---|
14 | from geniusweb.actions.Action import Action
|
---|
15 | from geniusweb.actions.Offer import Offer
|
---|
16 | from geniusweb.bidspace.AllBidsList import AllBidsList
|
---|
17 | from geniusweb.inform.ActionDone import ActionDone
|
---|
18 | from geniusweb.inform.Finished import Finished
|
---|
19 | from geniusweb.inform.Inform import Inform
|
---|
20 | from geniusweb.inform.Settings import Settings
|
---|
21 | from geniusweb.inform.YourTurn import YourTurn
|
---|
22 | from geniusweb.issuevalue.Bid import Bid
|
---|
23 | from geniusweb.party.Capabilities import Capabilities
|
---|
24 | from geniusweb.party.DefaultParty import DefaultParty
|
---|
25 | from geniusweb.profile.Profile import Profile
|
---|
26 | from geniusweb.profileconnection.ProfileConnectionFactory import (
|
---|
27 | ProfileConnectionFactory,
|
---|
28 | )
|
---|
29 | from geniusweb.profileconnection.ProfileInterface import ProfileInterface
|
---|
30 | from .group2_frequency_analyzer import FrequencyAnalyzer
|
---|
31 | from .group2_plot_trace import plot_characteristics
|
---|
32 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
33 | from tudelft_utilities_logging.Reporter import Reporter
|
---|
34 |
|
---|
35 |
|
---|
36 | class Agent2(DefaultParty):
|
---|
37 | """
|
---|
38 | Template agent that offers random bids until a bid with sufficient utility is offered.
|
---|
39 | """
|
---|
40 |
|
---|
41 | def __init__(self, reporter: Reporter = None):
|
---|
42 | super().__init__(reporter)
|
---|
43 | self.getReporter().log(logging.INFO, "party is initialized")
|
---|
44 | self._profileint: ProfileInterface = None # type:ignore
|
---|
45 | self._last_received_bid: Bid = None # type:ignore
|
---|
46 | self._utilspace: UtilitySpace = None # type:ignore
|
---|
47 | self._extendedspace: ExtendedUtilSpace = None # type:ignore
|
---|
48 |
|
---|
49 | self.highest_social_welfare_bid: list[Bid] = []
|
---|
50 |
|
---|
51 | # General settings
|
---|
52 | self.opponent_model = FrequencyAnalyzer()
|
---|
53 | self.reservation_utility: float = .0 # not sure if this is a good value to have, since any agreement is better than no agreement...
|
---|
54 | self.concession_speed: float = 11.0 # higher will concede slower (1/e is approximately linear) [0.0, ...]
|
---|
55 | self.attempts: int = 500 # the number of iterations it will go through to look for an 'optimal' bid
|
---|
56 | self.hard_to_get: float = .2 # the moment from which we'll consider playing nice [0.0, 1.0]
|
---|
57 | self.niceness: Decimal = Decimal(.05) # utility we're considering to give up for the sake of being nice [0.0, 1.0]
|
---|
58 |
|
---|
59 | # Agent characteristics:
|
---|
60 | # Can be included in plotting, make sure the dimensionality of all of them match up
|
---|
61 | self.lower_utility_bound: list[float] = []
|
---|
62 | self.our_social_welfare: list[float] = []
|
---|
63 | self.their_social_welfare: list[float] = []
|
---|
64 | self.their_social_welfare: list[float] = []
|
---|
65 | self.esitmated_opponent_utility: list[float] = []
|
---|
66 |
|
---|
67 | def notifyChange(self, info: Inform):
|
---|
68 | """This is the entry point of all interaction with your agent after is has been initialised.
|
---|
69 |
|
---|
70 | Args:
|
---|
71 | info (Inform): Contains either a request for action or information.
|
---|
72 | """
|
---|
73 |
|
---|
74 | # a Settings message is the first message that will be send to your
|
---|
75 | # agent containing all the information about the negotiation session.
|
---|
76 | if isinstance(info, Settings):
|
---|
77 | self._settings: Settings = cast(Settings, info)
|
---|
78 | self._me = self._settings.getID()
|
---|
79 |
|
---|
80 | # progress towards the deadline has to be tracked manually through the use of the Progress object
|
---|
81 | self._progress = self._settings.getProgress()
|
---|
82 |
|
---|
83 | # the profile contains the preferences of the agent over the domain
|
---|
84 | self._profileint = ProfileConnectionFactory.create(
|
---|
85 | info.getProfile().getURI(), self.getReporter()
|
---|
86 | )
|
---|
87 | self.opponent_model.set_domain(self._profileint.getProfile().getDomain())
|
---|
88 |
|
---|
89 | reservation_bid = self._profileint.getProfile().getReservationBid()
|
---|
90 |
|
---|
91 | if reservation_bid is not None:
|
---|
92 | profile, _ = self._get_profile_and_progress()
|
---|
93 | self.reservation_utility = profile.getUtility(reservation_bid)
|
---|
94 |
|
---|
95 | # ActionDone is an action send by an opponent (an offer or an accept)
|
---|
96 | elif isinstance(info, ActionDone):
|
---|
97 | action: Action = cast(ActionDone, info).getAction()
|
---|
98 |
|
---|
99 | # if it is an offer, set the last received bid
|
---|
100 | if isinstance(action, Offer):
|
---|
101 | self._last_received_bid = cast(Offer, action).getBid()
|
---|
102 | # YourTurn notifies you that it is your turn to act
|
---|
103 | elif isinstance(info, YourTurn):
|
---|
104 | action = self._my_turn()
|
---|
105 | if isinstance(self._progress, ProgressRounds):
|
---|
106 | self._progress = self._progress.advance()
|
---|
107 | self.getConnection().send(action)
|
---|
108 |
|
---|
109 | # Finished will be send if the negotiation has ended (through agreement or deadline)
|
---|
110 | elif isinstance(info, Finished):
|
---|
111 | # terminate the agent MUST BE CALLED
|
---|
112 | self.terminate()
|
---|
113 | else:
|
---|
114 | self.getReporter().log(
|
---|
115 | logging.WARNING, "Ignoring unknown info " + str(info)
|
---|
116 | )
|
---|
117 |
|
---|
118 | # lets the geniusweb system know what settings this agent can handle
|
---|
119 | # leave it as it is for this competition
|
---|
120 | def getCapabilities(self) -> Capabilities:
|
---|
121 | return Capabilities(
|
---|
122 | set(["SAOP"]),
|
---|
123 | set(["geniusweb.profile.utilityspace.LinearAdditive"]),
|
---|
124 | )
|
---|
125 |
|
---|
126 | # terminates the agent and its connections
|
---|
127 | # leave it as it is for this competition
|
---|
128 | def terminate(self):
|
---|
129 | self.getReporter().log(logging.INFO, "party is terminating:")
|
---|
130 | # self._plot_characteristics()
|
---|
131 | super().terminate()
|
---|
132 | if self._profileint is not None:
|
---|
133 | self._profileint.close()
|
---|
134 |
|
---|
135 | # ===================
|
---|
136 | # === AGENT LOGIC ===
|
---|
137 | # ===================
|
---|
138 |
|
---|
139 | # give a description of your agent
|
---|
140 | def getDescription(self) -> str:
|
---|
141 | return "Shaken, not stirred"
|
---|
142 |
|
---|
143 | # execute a turn
|
---|
144 | def _my_turn(self):
|
---|
145 | self._update_utilspace()
|
---|
146 | self.opponent_model.add_bid(self._last_received_bid)
|
---|
147 |
|
---|
148 | next_bid = self._find_bid(self.attempts)
|
---|
149 |
|
---|
150 | if self._is_acceptable(self._last_received_bid, next_bid):
|
---|
151 | action = Accept(self._me, self._last_received_bid)
|
---|
152 | else:
|
---|
153 | action = Offer(self._me, next_bid)
|
---|
154 |
|
---|
155 | # send the action
|
---|
156 | return action
|
---|
157 |
|
---|
158 | # =================
|
---|
159 | # === ACCEPTING ===
|
---|
160 | # =================
|
---|
161 |
|
---|
162 | def _is_acceptable(self, bid: Bid, our_next_bid: Bid) -> bool:
|
---|
163 | if bid is None:
|
---|
164 | return False
|
---|
165 |
|
---|
166 | profile, _ = self._get_profile_and_progress()
|
---|
167 | bid_utility = profile.getUtility(bid)
|
---|
168 |
|
---|
169 | threshold = self._lower_util_bound()
|
---|
170 |
|
---|
171 | self.lower_utility_bound.append(threshold)
|
---|
172 | self.our_social_welfare.append(float(self._social_welfare(our_next_bid)))
|
---|
173 | self.their_social_welfare.append(float(self._social_welfare(bid)))
|
---|
174 | self.esitmated_opponent_utility.append(float(self.opponent_model.get_utility(our_next_bid)))
|
---|
175 |
|
---|
176 | # has to be higher than the reservation value and our threshold, but if the bid is better than we expect we'll always accept
|
---|
177 | return (bid_utility > self.reservation_utility and bid_utility > threshold) or bid_utility > profile.getUtility(our_next_bid)
|
---|
178 |
|
---|
179 | def _lower_util_bound(self) -> float:
|
---|
180 | _, progress = self._get_profile_and_progress()
|
---|
181 |
|
---|
182 | threshold = self._exponential_decrease(progress, self.concession_speed)
|
---|
183 |
|
---|
184 | return threshold
|
---|
185 |
|
---|
186 | """
|
---|
187 | A function which is 1.0 at x=0.0, and 0.0 at x=1.0.
|
---|
188 | k determines how quickly it falls to 0.0; higher k is slower decrease
|
---|
189 | - k > 1/e will first fall slowly, then fast
|
---|
190 | - k < 1/e will first fall fast, then slow
|
---|
191 | - k = 1/e ~ linear
|
---|
192 | Rounding errors make x=1.0 not actually intersect (worse with higher k),
|
---|
193 | intersection with zero can be forced by setting force_intersect
|
---|
194 | """
|
---|
195 | def _exponential_decrease(self, x, k, force_intersect=True):
|
---|
196 | if force_intersect and x > 0.99:
|
---|
197 | return 0.0
|
---|
198 |
|
---|
199 | return -math.exp(x**k) + 2 - x**k * (-math.e + 2)
|
---|
200 |
|
---|
201 | # ===============
|
---|
202 | # === BIDDING ===
|
---|
203 | # ===============
|
---|
204 |
|
---|
205 | def _find_bid(self, attempts) -> Bid:
|
---|
206 | # compose a list of all possible bids
|
---|
207 | _, progress = self._get_profile_and_progress()
|
---|
208 |
|
---|
209 | # it the beginning we'll play hard to get
|
---|
210 | # after that we'll consider playing nice
|
---|
211 | # => this makes us indicate our interests and gives us
|
---|
212 | # the opportunity to collect information about our opponent
|
---|
213 | if progress < self.hard_to_get:
|
---|
214 | return self._find_max_bid()
|
---|
215 | else:
|
---|
216 | return self._find_max_nice_bid(attempts)
|
---|
217 |
|
---|
218 | """
|
---|
219 | Gets a random bid from the given list of all_bids
|
---|
220 | """
|
---|
221 | def _get_random_bid(self, all_bids: ImmutableList[Bid]):
|
---|
222 | return all_bids.get(randint(0, all_bids.size() - 1))
|
---|
223 |
|
---|
224 | """
|
---|
225 | Finds the maximum bid according to a certain proposition
|
---|
226 | """
|
---|
227 | def _find_bid_with(self, proposition: Callable[[Bid, Bid], bool], attempts: int):
|
---|
228 | # compose a list of all possible bids
|
---|
229 | # TODO Make the selection more constrained, the frequency analyzer performs relatively well
|
---|
230 | # but the amount of time it takes to find a good/nice bid can be reduced significantly
|
---|
231 | all_bids = AllBidsList(self._profileint.getProfile().getDomain())
|
---|
232 |
|
---|
233 | # TODO Also consider doing this differently
|
---|
234 | maxBid = self._find_lower_bid()
|
---|
235 |
|
---|
236 | if maxBid is None:
|
---|
237 | if len(self.highest_social_welfare_bid) == 0:
|
---|
238 | maxBid = self._find_max_bid()
|
---|
239 | else:
|
---|
240 | maxBid = self.highest_social_welfare_bid[-1]
|
---|
241 |
|
---|
242 | for _ in range(attempts):
|
---|
243 | bid = self._get_random_bid(all_bids)
|
---|
244 | maxBid = bid if proposition(bid, maxBid) else maxBid
|
---|
245 |
|
---|
246 | self.highest_social_welfare_bid.append(maxBid)
|
---|
247 | return maxBid
|
---|
248 |
|
---|
249 | """
|
---|
250 | Find a bid according to the current lower bound
|
---|
251 | returns _find_max_bid if no bid in that range can be found
|
---|
252 | """
|
---|
253 | def _find_lower_bid(self):
|
---|
254 | lower_bound_bids = self._extendedspace.getBids(min(Decimal(self._lower_util_bound()), Decimal(1) - self.niceness))
|
---|
255 |
|
---|
256 | if lower_bound_bids.size() == 0:
|
---|
257 | return None
|
---|
258 |
|
---|
259 | return self._get_random_bid(lower_bound_bids)
|
---|
260 |
|
---|
261 | """
|
---|
262 | Find the maximum bids from the domain
|
---|
263 | """
|
---|
264 | def _find_max_bid(self) -> Bid:
|
---|
265 | max_bids = self._extendedspace.getBids(self._extendedspace.getMax())
|
---|
266 | return self._get_random_bid(max_bids)
|
---|
267 |
|
---|
268 | """
|
---|
269 | Finds the maximum bid while trying to also accomodate the opponents interests
|
---|
270 | according to _is_better_bid with be_nice set to True
|
---|
271 | """
|
---|
272 | def _find_max_nice_bid(self, attempts) -> Bid:
|
---|
273 | # some cheeky CPL currying
|
---|
274 | return self._find_bid_with((lambda a, b: self._is_better_bid(a, b, self.niceness, be_nice=True) and self._is_acceptable(a, b)), attempts)
|
---|
275 |
|
---|
276 | """
|
---|
277 | Checks if bid a is better than bid b.
|
---|
278 | If be_nice is True, will also consider the opponents utility according to opponent_model and
|
---|
279 | is willing to sacrifice a niceness amount of utility when comparing in order to create a win-win
|
---|
280 | """
|
---|
281 | def _is_better_bid(self, a: Bid, b: Bid, niceness: Decimal, be_nice=False) -> bool:
|
---|
282 | profile, _ = self._get_profile_and_progress()
|
---|
283 |
|
---|
284 | if not be_nice:
|
---|
285 | return profile.getUtility(a) >= profile.getUtility(b)
|
---|
286 | else:
|
---|
287 | # TODO look into niceness possibly accumulating over multiple self.attempts
|
---|
288 | # TODO Social welfare metric?
|
---|
289 | return profile.getUtility(a) >= profile.getUtility(b) - - niceness \
|
---|
290 | and self.opponent_model.get_utility(a) >= self.opponent_model.get_utility(b)
|
---|
291 |
|
---|
292 | def _social_welfare(self, bid: Bid) -> Decimal:
|
---|
293 | profile, _ = self._get_profile_and_progress()
|
---|
294 |
|
---|
295 | return (profile.getUtility(bid) + Decimal(self.opponent_model.get_utility(bid)))/Decimal(2.0)
|
---|
296 |
|
---|
297 | # ==============
|
---|
298 | # === UTILS ====
|
---|
299 | # ==============
|
---|
300 |
|
---|
301 | def _get_profile_and_progress(self) -> tuple[LinearAdditive, float]:
|
---|
302 | profile: Profile = self._profileint.getProfile()
|
---|
303 | progress: float = self._progress.get(time.time() * 1000)
|
---|
304 |
|
---|
305 | return cast(LinearAdditive, profile), progress
|
---|
306 |
|
---|
307 | def _update_utilspace(self) -> None: # throws IOException
|
---|
308 | newutilspace = self._profileint.getProfile()
|
---|
309 | if not newutilspace == self._utilspace:
|
---|
310 | self._utilspace = cast(LinearAdditive, newutilspace)
|
---|
311 | self._extendedspace = ExtendedUtilSpace(self._utilspace)
|
---|
312 |
|
---|
313 | # ===================
|
---|
314 | # === DEBUG TOOLS ===
|
---|
315 | # ===================
|
---|
316 |
|
---|
317 | def _print_utility(self, bid: Bid) -> None:
|
---|
318 | profile, _ = self._get_profile_and_progress()
|
---|
319 | print("Bid:", bid, "with utility:", profile.getUtility(bid))
|
---|
320 |
|
---|
321 | def _plot_characteristics(self) -> None:
|
---|
322 | characteristics = {
|
---|
323 | "lowest acceptable utility": self._plot_space(self.lower_utility_bound, "gray"),
|
---|
324 | "social welfare (ours, estimation)": self._plot_space(self.our_social_welfare, "green"),
|
---|
325 | "social welfare (theirs, estimation)": self._plot_space(self.their_social_welfare, "blue"),
|
---|
326 | "opponent utility (estimation)": self._plot_space(self.esitmated_opponent_utility, "red")
|
---|
327 | }
|
---|
328 | plot_characteristics(characteristics, len(self.lower_utility_bound))
|
---|
329 |
|
---|
330 | def _plot_space(self, arr: list, color: str) -> tuple[list, list, str]:
|
---|
331 | return (list(range(len(arr))), arr, color)
|
---|