1 | import logging
|
---|
2 | import time
|
---|
3 | import numpy as np
|
---|
4 | import copy
|
---|
5 | from geniusweb.progress.Progress import Progress
|
---|
6 | from scipy.stats import chisquare
|
---|
7 | from random import randint
|
---|
8 | from typing import cast
|
---|
9 | from time import time as clock
|
---|
10 | from geniusweb.actions.Accept import Accept
|
---|
11 | from geniusweb.actions.Action import Action
|
---|
12 | from geniusweb.actions.Offer import Offer
|
---|
13 | from geniusweb.inform.ActionDone import ActionDone
|
---|
14 | from geniusweb.inform.Finished import Finished
|
---|
15 | from geniusweb.inform.Inform import Inform
|
---|
16 | from geniusweb.inform.Settings import Settings
|
---|
17 | from geniusweb.inform.YourTurn import YourTurn
|
---|
18 | from geniusweb.issuevalue.Bid import Bid
|
---|
19 | from geniusweb.issuevalue.Value import Value
|
---|
20 | from decimal import Decimal
|
---|
21 | from geniusweb.party.Capabilities import Capabilities
|
---|
22 | from geniusweb.party.DefaultParty import DefaultParty
|
---|
23 | from geniusweb.profile.utilityspace import LinearAdditive
|
---|
24 | from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
|
---|
25 | from geniusweb.profileconnection import ProfileInterface
|
---|
26 | from geniusweb.profileconnection.ProfileConnectionFactory import (
|
---|
27 | ProfileConnectionFactory,
|
---|
28 | )
|
---|
29 | from tudelft.utilities.immutablelist.ImmutableList import ImmutableList
|
---|
30 |
|
---|
31 | from .extended_util_space import ExtendedUtilSpace
|
---|
32 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
33 | from tudelft_utilities_logging.Reporter import Reporter
|
---|
34 |
|
---|
35 |
|
---|
36 | class Agent22(DefaultParty):
|
---|
37 |
|
---|
38 | def __init__(self, reporter: Reporter = None):
|
---|
39 | super().__init__(reporter)
|
---|
40 | self.getReporter().log(logging.INFO, "party is initialized")
|
---|
41 | self._profile: ProfileInterface = None
|
---|
42 | self._last_received_bid: Bid = None
|
---|
43 | self._progress: Progress = None # type:ignore
|
---|
44 | self._extendedspace: ExtendedUtilSpace = None
|
---|
45 | self.issue_names = []
|
---|
46 | self.bidList: list[Bid] = []
|
---|
47 | self.bidListOpp: list[Bid] = []
|
---|
48 | self.weightList: dict[str, Decimal] = {}
|
---|
49 | self.weightListOpp: dict[str, Decimal] = {}
|
---|
50 | self.issue_value_frequencies = {}
|
---|
51 | self.prev_issue_value_frequencies = {}
|
---|
52 | self.cc = 1 # concession constant
|
---|
53 |
|
---|
54 | def notifyChange(self, info: Inform):
|
---|
55 | """This is the entry point of all interaction with your agent after is has been initialised.
|
---|
56 |
|
---|
57 | Args:
|
---|
58 | info (Inform): Contains either a request for action or information.
|
---|
59 | """
|
---|
60 |
|
---|
61 | # a Settings message is the first message that will be send to your
|
---|
62 | # agent containing all the information about the negotiation session.
|
---|
63 | if isinstance(info, Settings):
|
---|
64 | self._settings: Settings = cast(Settings, info)
|
---|
65 | self._me = self._settings.getID()
|
---|
66 |
|
---|
67 | # progress towards the deadline has to be tracked manually through the use of the Progress object
|
---|
68 | self._progress: Progress = self._settings.getProgress()
|
---|
69 |
|
---|
70 | # the profile contains the preferences of the agent over the domain
|
---|
71 | self._profile = ProfileConnectionFactory.create(
|
---|
72 | info.getProfile().getURI(), self.getReporter()
|
---|
73 | )
|
---|
74 |
|
---|
75 | profile: LinearAdditive = self._profile.getProfile()
|
---|
76 | self.weightList = profile.getWeights()
|
---|
77 | self.issue_names = list(self.weightList.keys())
|
---|
78 | n = len(self.issue_names)
|
---|
79 | self.weightListOpp = dict(zip(self.issue_names, np.full(n, Decimal(round(1 / n, 6)))))
|
---|
80 | self.issue_value_frequencies = dict(zip(self.issue_names, {}))
|
---|
81 | # ActionDone is an action send by an opponent (an offer or an accept)
|
---|
82 | elif isinstance(info, ActionDone):
|
---|
83 | action: Action = cast(ActionDone, info).getAction()
|
---|
84 | # if it is an offer, set the last received bid
|
---|
85 | if isinstance(action, Offer):
|
---|
86 | self._last_received_bid = cast(Offer, action).getBid()
|
---|
87 | self.bidListOpp.append(self._last_received_bid)
|
---|
88 | self._updateFrequencies(self._last_received_bid)
|
---|
89 | self.update_weight_every_window()
|
---|
90 |
|
---|
91 |
|
---|
92 |
|
---|
93 | # YourTurn notifies you that it is your turn to act
|
---|
94 | elif isinstance(info, YourTurn):
|
---|
95 | # execute a turn
|
---|
96 | action = self._myTurn()
|
---|
97 | if action is Offer:
|
---|
98 | self.bidList.append(action.getBid())
|
---|
99 | if isinstance(self._progress, ProgressRounds):
|
---|
100 | self._progress = self._progress.advance()
|
---|
101 | self.getConnection().send(action)
|
---|
102 |
|
---|
103 | # Finished will be send if the negotiation has ended (through agreement or deadline)
|
---|
104 | elif isinstance(info, Finished):
|
---|
105 | # terminate the agent MUST BE CALLED
|
---|
106 | self.terminate()
|
---|
107 | else:
|
---|
108 | self.getReporter().log(
|
---|
109 | logging.WARNING, "Ignoring unknown info " + str(info)
|
---|
110 | )
|
---|
111 |
|
---|
112 | # lets the geniusweb system know what settings this agent can handle
|
---|
113 | # leave it as it is for this competition
|
---|
114 | def getCapabilities(self) -> Capabilities:
|
---|
115 | return Capabilities(
|
---|
116 | set(["SAOP"]),
|
---|
117 | set(["geniusweb.profile.utilityspace.LinearAdditive"]),
|
---|
118 | )
|
---|
119 |
|
---|
120 | # terminates the agent and its connections
|
---|
121 | # leave it as it is for this competition
|
---|
122 | def terminate(self):
|
---|
123 | self.getReporter().log(logging.INFO, "party is terminating:")
|
---|
124 | super().terminate()
|
---|
125 | if self._profile is not None:
|
---|
126 | self._profile.close()
|
---|
127 | self._profile = None
|
---|
128 |
|
---|
129 |
|
---|
130 |
|
---|
131 | # give a description of your agent
|
---|
132 | # Overrride
|
---|
133 | def getDescription(self) -> str:
|
---|
134 | return "Agent22"
|
---|
135 |
|
---|
136 | # execute a turn
|
---|
137 | # Override
|
---|
138 | def _myTurn(self):
|
---|
139 | self._updateExtUtilSpace()
|
---|
140 | # check if the last received offer if the opponent is good enough
|
---|
141 | ourBid = self._findBid()
|
---|
142 | if self._isGoodNew(self._last_received_bid, ourBid):
|
---|
143 | # if so, accept the offer
|
---|
144 | action = Accept(self._me, self._last_received_bid)
|
---|
145 | else:
|
---|
146 | # if not, find a bid to propose as counter offer
|
---|
147 | bid = ourBid
|
---|
148 | action = Offer(self._me, bid)
|
---|
149 |
|
---|
150 | # send the action
|
---|
151 | return action
|
---|
152 | return action
|
---|
153 |
|
---|
154 | def _updateExtUtilSpace(self): # throws IOException
|
---|
155 | new_utilspace: LinearAdditive = self._profile.getProfile()
|
---|
156 | self._extendedspace = ExtendedUtilSpace(new_utilspace)
|
---|
157 |
|
---|
158 | def _findBid(self) -> Bid:
|
---|
159 | beta = self._checkStrategyOpp()
|
---|
160 | return self.time_dependent_bidding(beta)
|
---|
161 |
|
---|
162 | def _getTheirUtility(self, bid: Bid):
|
---|
163 | value_estimation = self.val_estimation()
|
---|
164 | utility = 0
|
---|
165 | for issue in bid.getIssues():
|
---|
166 | value = bid.getValue(issue)
|
---|
167 | if issue in value_estimation and value in value_estimation[issue]:
|
---|
168 | utility += float(self.weightListOpp[issue]) * value_estimation[issue][value]
|
---|
169 |
|
---|
170 | return Decimal(utility)
|
---|
171 |
|
---|
172 | def _updateFrequencies(self, bid: Bid):
|
---|
173 | issue_values = bid.getIssueValues()
|
---|
174 | for issue in issue_values.keys():
|
---|
175 | value = issue_values[issue]
|
---|
176 | if not (issue in self.issue_value_frequencies):
|
---|
177 | self.issue_value_frequencies[issue] = {}
|
---|
178 | if not (value in self.issue_value_frequencies[issue]):
|
---|
179 | self.issue_value_frequencies[issue][value] = 0
|
---|
180 |
|
---|
181 | self.issue_value_frequencies[issue][value] += 1
|
---|
182 |
|
---|
183 | def _evaluate_bid(self, bid: Bid):
|
---|
184 | profile = self._profile.getProfile()
|
---|
185 | progress = self._progress.get(time.time() * 1000)
|
---|
186 |
|
---|
187 | U_mine = profile.getUtility(bid)
|
---|
188 | U_theirs = self._getTheirUtility(bid)
|
---|
189 | a = Decimal(1 - progress)
|
---|
190 |
|
---|
191 | if a < 1.0 / 2: return U_mine
|
---|
192 | if a >= 1.0 / 2: return (a * U_mine + (1 - a) * U_theirs) / 2
|
---|
193 |
|
---|
194 |
|
---|
195 | def _checkStrategyOpp(self) -> float:
|
---|
196 | opp_bids_length = len(self.bidListOpp)
|
---|
197 | if opp_bids_length > 0:
|
---|
198 | unique_opp_bids_length = len(set(self.bidListOpp))
|
---|
199 | t1 = unique_opp_bids_length / opp_bids_length
|
---|
200 | # print(t1)
|
---|
201 | if t1 > 0.35:
|
---|
202 | return 0.2
|
---|
203 | else:
|
---|
204 | return 1.8
|
---|
205 | else:
|
---|
206 | return 0.2
|
---|
207 |
|
---|
208 | # Acceptance condition
|
---|
209 | def _isGoodNew(self, bid: Bid, plannedBid: Bid) -> bool:
|
---|
210 | # the offer is acceptable if it is better than
|
---|
211 | # all offers received in the previous time window W
|
---|
212 | # or the offer is better than our next planned offer
|
---|
213 | # W = [T - (1 - T), T]
|
---|
214 | if bid is None:
|
---|
215 | return False
|
---|
216 | profile = self._profile.getProfile()
|
---|
217 |
|
---|
218 | progress = self._progress.get(time.time() * 1000)
|
---|
219 | bidsFromW = []
|
---|
220 | maxBidFromW = 0
|
---|
221 | W = 0.02
|
---|
222 | T = 0.98
|
---|
223 | if isinstance(profile, UtilitySpace):
|
---|
224 | reservation_bid = profile.getReservationBid()
|
---|
225 | if reservation_bid is None and progress >= T:
|
---|
226 | return True
|
---|
227 | reservation_value = 0.3
|
---|
228 | if reservation_bid is not None:
|
---|
229 | reservation_value = profile.getUtility(reservation_bid)
|
---|
230 |
|
---|
231 | receivedBid = self._evaluate_bid(bid)
|
---|
232 | # If the opponent's bid is better than our next planned bid, accept
|
---|
233 | if (receivedBid > self._evaluate_bid(plannedBid)):
|
---|
234 | return True
|
---|
235 |
|
---|
236 | # Save bids from window W and save the best one
|
---|
237 | if (progress >= T - W and progress < T):
|
---|
238 | bidsFromW.append(receivedBid)
|
---|
239 | if (receivedBid > maxBidFromW):
|
---|
240 | maxBidFromW = receivedBid
|
---|
241 |
|
---|
242 | utility_target = reservation_value * 3 / 2
|
---|
243 | # After time T, accept the bid if it is better from the best bid recieved
|
---|
244 | # in the previous time window W
|
---|
245 | if (progress >= T and receivedBid < utility_target and receivedBid >= maxBidFromW):
|
---|
246 | return True
|
---|
247 |
|
---|
248 | return receivedBid >= utility_target
|
---|
249 |
|
---|
250 | def time_dependent_bidding(self, beta: float) -> Bid:
|
---|
251 | progress: float = self._progress.get(time.time() * 1000)
|
---|
252 | profile = self._profile.getProfile()
|
---|
253 |
|
---|
254 | reservation_bid: Bid = profile.getReservationBid()
|
---|
255 | min_util = Decimal(0.6) # reservation value
|
---|
256 | if reservation_bid is not None:
|
---|
257 | min_util = Decimal(profile.getUtility(reservation_bid))
|
---|
258 |
|
---|
259 | max_util: Decimal = Decimal(1)
|
---|
260 |
|
---|
261 | ft1 = Decimal(1)
|
---|
262 | if beta != 0:
|
---|
263 | ft1 = round(Decimal(1 - pow(progress, 1 / beta)), 6) # defaults ROUND_HALF_UP
|
---|
264 | utilityGoal: Decimal = min_util + (max_util - min_util) * ft1
|
---|
265 |
|
---|
266 | options: ImmutableList[Bid] = self._extendedspace.getBids(utilityGoal)
|
---|
267 | if options.size() == 0:
|
---|
268 | # if we can't find good bid, get max util bid....
|
---|
269 | options = self._extendedspace.getBids(self._extendedspace.getMax())
|
---|
270 |
|
---|
271 | for bid in options:
|
---|
272 | if self._isGoodNew(self._last_received_bid, bid):
|
---|
273 | return bid
|
---|
274 |
|
---|
275 | # else pick a random one.
|
---|
276 | return options.get(randint(0, options.size() - 1))
|
---|
277 |
|
---|
278 | def update_weight_every_window(self):
|
---|
279 | k = 10
|
---|
280 | if len(self.bidListOpp) % k == 0:
|
---|
281 | self.weightListOpp = self.oppWeights()
|
---|
282 | self.prev_issue_value_frequencies = copy.deepcopy(self.issue_value_frequencies)
|
---|
283 |
|
---|
284 | def val_estimation(self) -> dict[str, dict[Value, float]]:
|
---|
285 | gamma = 0.5
|
---|
286 | freqs = copy.deepcopy(self.issue_value_frequencies)
|
---|
287 | value_func = copy.deepcopy(self.issue_value_frequencies)
|
---|
288 | for issue in freqs.keys():
|
---|
289 | max_value = max(freqs[issue], key=freqs[issue].get)
|
---|
290 | for value in freqs[issue].keys():
|
---|
291 | value_func[issue][value] = ((1 + freqs[issue][value]) ** gamma) / (
|
---|
292 | (1 + freqs[issue][max_value]) ** gamma)
|
---|
293 |
|
---|
294 | return value_func
|
---|
295 |
|
---|
296 | def oppWeights(self) -> dict[str, Decimal]:
|
---|
297 | alpha = 10 # alpha denotes how much importance is added to weights
|
---|
298 | beta = 5 # beta denotes how much this importance matters over time
|
---|
299 | e = [] # list of issues that did not change significantly in frequency
|
---|
300 | concession = False
|
---|
301 | new_weights: dict[str, Decimal] = copy.deepcopy(self.weightListOpp)
|
---|
302 | issue_list = self.prev_issue_value_frequencies.keys()
|
---|
303 | value_func = self.val_estimation()
|
---|
304 | progress = self._progress.get(round(clock() * 1000))
|
---|
305 | n = len(issue_list)
|
---|
306 | for issue in issue_list:
|
---|
307 | # Calculate the frequencies from the currently found values
|
---|
308 | frequencies = copy.deepcopy(self.issue_value_frequencies[issue])
|
---|
309 | N = sum(frequencies.values())
|
---|
310 | for value in frequencies.keys():
|
---|
311 | frequencies[value] /= float(N)
|
---|
312 |
|
---|
313 | prev_frequencies = copy.deepcopy(self.prev_issue_value_frequencies[issue])
|
---|
314 | # Add the newly found values to the previous dictionary
|
---|
315 | for value in frequencies.keys():
|
---|
316 | if value not in prev_frequencies:
|
---|
317 | prev_frequencies[value] = 0
|
---|
318 | # Calculate the frequencies from the previous found values
|
---|
319 | N = sum(prev_frequencies.values())
|
---|
320 | for value in prev_frequencies.keys():
|
---|
321 | prev_frequencies[value] /= float(N)
|
---|
322 |
|
---|
323 | # Do a chi squared distribution test on the frequencies to check if they have changed significantly
|
---|
324 | obs = list(frequencies.values())
|
---|
325 | exp = list(prev_frequencies.values())
|
---|
326 | _, p_val = chisquare(f_obs=obs, f_exp=exp)
|
---|
327 | # If our frequencies did not change significantely add this issue to e
|
---|
328 | if p_val > 0.05:
|
---|
329 | e.append(issue)
|
---|
330 | else:
|
---|
331 | # Calculate the expected value for the utility for each issue value and compare with the previous found one
|
---|
332 | prev_expected = {k: prev_frequencies[k] * value_func[issue][k] for k in prev_frequencies}
|
---|
333 | expected = {k: frequencies[k] * value_func[issue][k] for k in frequencies}
|
---|
334 | if sum(expected.values()) < sum(prev_expected.values()):
|
---|
335 | concession = True
|
---|
336 |
|
---|
337 | if len(e) != len(issue_list) and concession:
|
---|
338 | for issue in e:
|
---|
339 | delta_t = Decimal(alpha * (1 - progress ** beta))
|
---|
340 | new_weights[issue] += delta_t
|
---|
341 |
|
---|
342 | # Normalize weights
|
---|
343 | summed = sum(new_weights.values())
|
---|
344 | for key in new_weights:
|
---|
345 | new_weights[key] = Decimal(round(new_weights[key] / summed, 6))
|
---|
346 |
|
---|
347 | # print(new_weights)
|
---|
348 | return new_weights
|
---|