source: CSE3210/agent29/agent29.py

Last change on this file was 74, checked in by wouter, 21 months ago

#6 Added CSE3210 parties

File size: 20.3 KB
Line 
1import logging
2import time
3from decimal import Decimal
4
5import numpy as np
6from random import randint
7from typing import cast
8
9from geniusweb.actions.Accept import Accept
10from geniusweb.actions.Action import Action
11from geniusweb.actions.Offer import Offer
12from geniusweb.bidspace.AllBidsList import AllBidsList
13from geniusweb.inform.ActionDone import ActionDone
14from geniusweb.inform.Finished import Finished
15from geniusweb.inform.Inform import Inform
16from geniusweb.inform.Settings import Settings
17from geniusweb.inform.YourTurn import YourTurn
18from geniusweb.issuevalue.Bid import Bid
19from geniusweb.party.Capabilities import Capabilities
20from geniusweb.party.DefaultParty import DefaultParty
21from geniusweb.profileconnection.ProfileConnectionFactory import (
22 ProfileConnectionFactory,
23)
24from geniusweb.progress.ProgressRounds import ProgressRounds
25from tudelft_utilities_logging.Reporter import Reporter
26
27
28class Agent29(DefaultParty):
29 """
30 Agent Hope
31
32 Linear concession is used for target utilities.
33
34 Each time a bid is to be chosen, a whole range of bids close to the target utility is considered from
35 which, the ones with values closest to the estimated opponent's preferred values are prioritised for
36 the final offer.
37
38 Close to the deadline, the agent offers the bid with the highest utility from the set of all the bids
39 received.
40
41 Bids are accepted only if the are sufficiently better than the average received bid.
42 This approach only accepts very good bids and only becomes more lenient towards the very end of
43 negotiation.
44
45 It is ensured that no bid with utility lower than the agent's reservation value
46 is ever offered or accepted.
47 """
48
49 def __init__(self, reporter: Reporter = None):
50 super().__init__(reporter)
51 self.getReporter().log(logging.INFO, "party is initialized")
52 self._profile = None
53 self._last_received_bid = None
54 self._reservation_value = 0.0
55 self._all_opponent_bids: list[Bid] = []
56 self._all_offered_bids: list[Bid] = []
57 self._log_times = [np.log(i / 200) for i in range(1, 201)]
58 self._log_times.insert(0, 0)
59 self._e = 1.0
60 self._last_ten_bids_counts = {}
61 self._all_possible_bids: AllBidsList
62 self._all_possible_bids_utils = []
63 self._all_possible_bids_ord: list[Bid] = []
64 self._all_possible_bids_ord_utils = []
65 self._num_possible_bids = 0
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 sent 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._profile = ProfileConnectionFactory.create(
85 info.getProfile().getURI(), self.getReporter()
86 )
87
88 # initialises the histogram opponent modelling
89 self.initialise_bid_counts()
90 self.initialise_all_possible_bids()
91 self.initialise_reservation_value()
92 # ActionDone is an action send by an opponent (an offer or an accept)
93 elif isinstance(info, ActionDone):
94 action: Action = cast(ActionDone, info).getAction()
95
96 # if it is an offer, set the last received bid
97 if isinstance(action, Offer):
98 self._last_received_bid = cast(Offer, action).getBid()
99 # YourTurn notifies you that it is your turn to act
100 elif isinstance(info, YourTurn):
101 action = self._myTurn()
102 if isinstance(self._progress, ProgressRounds):
103 self._progress = self._progress.advance()
104 self.getConnection().send(action)
105
106 # Finished will be sent if the negotiation has ended (through agreement or deadline)
107 elif isinstance(info, Finished):
108 # terminate the agent MUST BE CALLED
109 self.terminate()
110 else:
111 self.getReporter().log(
112 logging.WARNING, "Ignoring unknown info " + str(info)
113 )
114
115 # lets the geniusweb system know what settings this agent can handle
116 # leave it as it is for this competition
117 def getCapabilities(self) -> Capabilities:
118 return Capabilities(
119 set(["SAOP"]),
120 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
121 )
122
123 # terminates the agent and its connections
124 # leave it as it is for this competition
125 def terminate(self):
126 self.getReporter().log(logging.INFO, "party is terminating:")
127 super().terminate()
128 if self._profile is not None:
129 self._profile.close()
130 self._profile = None
131
132
133
134 # give a description of your agent
135 def getDescription(self) -> str:
136 return "Agent Hope: Linear concession is used for target utilities. \nEach time a bid is to be chosen, " \
137 "a whole range of bids close to the target utility is considered from which, the ones with values " \
138 "closest to the estimated opponent's preferred values are prioritised for the final offer. \nClose to " \
139 "the deadline, the agent offers the bid with the highest utility from the set of all the bids " \
140 "received. \nBids are accepted only if the are sufficiently better than the average received bid. " \
141 "This approach only accepts very good bids and only becomes more lenient towards the very end of " \
142 "negotiation. \nIt is ensured that no bid with utility lower than the agent's reservation value " \
143 "is ever offered or accepted."
144
145 """
146 Execute a turn
147 """
148
149 def _myTurn(self):
150 if self._last_received_bid is not None:
151 self._all_opponent_bids.append(self._last_received_bid)
152 if len(self._all_opponent_bids) != 0:
153 if len(self._all_opponent_bids) > 10:
154 self._uncount_oldest_bid()
155 self._count_last_bid()
156 # check if the last received offer of the opponent is good enough
157 if self._isGood(self._last_received_bid):
158 # if so, accept the offer
159 action = Accept(self._me, self._last_received_bid)
160 # checks if the negotiation is nearing the end. If so, the best received offer is sent
161 elif self._progress.get(time.time() * 1000) >= 0.95:
162 opp_bids_utilities = [self._profile.getProfile().getUtility(bid) for bid in self._all_opponent_bids]
163 best_opponent_bid = self._all_opponent_bids[np.argmax(opp_bids_utilities)]
164 if self._profile.getProfile().getUtility(best_opponent_bid) >= self._reservation_value:
165 action = Offer(self._me, best_opponent_bid)
166 else:
167 action = Offer(self._me, self._findBid())
168 else:
169 # if there is still time and the received offer was not good enough, the agent looks for a better one
170 bid = self._findBid()
171 action = Offer(self._me, bid)
172 self._all_offered_bids.append(bid)
173
174 # send the action
175 return action
176
177 """
178 The method that finds a bid in multiple possible ways based on the current situation.
179 If the opponent model is initialised, it uses it, otherwise a random bid is taken.
180 """
181
182 def _findBid(self) -> Bid:
183 # find bids with utilities closest to the target utility
184 target_utility = Decimal(1.0 - 0.3 * self._progress.get(time.time() * 1000))
185 bids_to_consider = self.bids_close_to_target_util(target_utility)
186 # only keep bids with utility above reservation value
187 acceptable_bids = self.remove_bids_below_reservation(bids_to_consider)
188
189 if len(acceptable_bids) == 0: # if no bids are acceptable, offer one with util >= reservation
190 best_bid = self.find_first_acceptable_bid()
191 elif len(self._all_opponent_bids) >= 10: # if the histogram is initialised, use it
192 best_bid = self.best_domain_bid(acceptable_bids)
193 else: # if the histogram is not initialised, offer random bids
194 # initialize the bid to something above reservation value
195 best_bid = self.find_first_acceptable_bid()
196 best_bid_util = self._profile.getProfile().getUtility(best_bid)
197
198 # take attempts at finding a random bid that is acceptable to us
199 best_bid = self.find_random_acceptable_bid(best_bid, best_bid_util)
200
201 return best_bid
202
203 """
204 This method receives bids and checks whether they should be accepted. It is responsible for checking
205 the quality of bids the agent offers using a three stage approach depending on the progress (number of rounds
206 finished).
207
208 In the first stage, it refuses any bids, which gives the agent enough time to learn about the opponent
209 (establish the average and start domain modeling).
210
211 The second stage covers majority of the rounds and accepts offers only when the bid offered is significantly
212 better than average.
213
214 In the last stage if an agreement hasn't been reached yet, any bid is accepted as long as it is better than the
215 reservation_value.
216 """
217
218 def _isGood(self, bid: Bid) -> bool:
219 if bid is None:
220 return False
221
222 # first stage - establish average of opponent
223 if self._progress.get(time.time() * 1000) < 0.2:
224 return False
225
226 # second stage - check if the received bid improved by at least 50% above the average
227 if self._progress.get(time.time() * 1000) < 0.97:
228 return self._significantImprovement(bid, 0.5)
229
230 # last part - this only gets executed if opponent doesn't accept an offer they sent previously.
231 return self._profile.getProfile().getUtility(bid) > self._reservation_value
232
233 """
234 Check whether the offered bid has a utility greater than 0.8 (as well as greater than our reservation value)
235 Not elaborate, only used for the first 10 offered bids (opponent acceptance at this stage is not really expected)
236 """
237
238 def _isGoodDomainAgent(self, bid: Bid) -> bool:
239 if bid is None:
240 return False
241 bid_util = self._profile.getProfile().getUtility(bid)
242 return bid_util > 0.8 and bid_util > self._reservation_value
243
244 """
245 Following method checks whether a given bid is better than an average bid by at least the value specified
246 (significance).
247
248 It also checks whether the bid is better than the reservationBid (if specified).
249 """
250
251 def _significantImprovement(self, bid: Bid, significance: float) -> bool:
252 if len(self._all_opponent_bids) == 0:
253 return False
254
255 # numpy average computation
256 get_util = lambda x: float(self._profile.getProfile().getUtility(x))
257 vgu = np.vectorize(get_util)
258 average = np.average(vgu(self._all_opponent_bids))
259
260 return float(self._profile.getProfile().getUtility(bid)) > average + significance and \
261 float(self._profile.getProfile().getUtility(bid)) > self._reservation_value
262
263 """
264 Initializes an empty histogram for use in the domain modeling.
265 """
266
267 def initialise_bid_counts(self):
268 domain = self._profile.getProfile().getDomain()
269 domain_issues = domain.getIssues()
270
271 self._num_possible_bids = 1
272 for issue in domain_issues:
273 self._last_ten_bids_counts[issue] = {}
274 issue_values = domain.getValues(issue)
275 self._num_possible_bids *= issue_values.size()
276 for issue_value in issue_values:
277 self._last_ten_bids_counts[issue][issue_value] = 0
278
279 """
280 Initializes a list of bids in the agent's bid space, and sorts them as well.
281 """
282
283 def initialise_all_possible_bids(self):
284 domain = self._profile.getProfile().getDomain()
285 self._all_possible_bids = AllBidsList(domain)
286 for i in range(self._all_possible_bids.size()):
287 current_bid = self._all_possible_bids.get(i)
288 self._all_possible_bids_utils.append(self._profile.getProfile().getUtility(current_bid))
289 self._all_possible_bids_utils = np.array(self._all_possible_bids_utils)
290 sort_indices = np.argsort(self._all_possible_bids_utils)
291 for i in range(self._all_possible_bids.size()):
292 self._all_possible_bids_ord.append(self._all_possible_bids.get(sort_indices[i]))
293
294 self._all_possible_bids_ord_utils = self._all_possible_bids_utils[sort_indices]
295 self._all_possible_bids_ord_utils = self._all_possible_bids_ord_utils = \
296 self._all_possible_bids_ord_utils.astype('float')
297
298 """
299 Initializes a reservation value, if a Reservation Bid is defined in the profile.
300 """
301
302 def initialise_reservation_value(self):
303 reservation_bid = self._profile.getProfile().getReservationBid()
304 if reservation_bid is not None:
305 self._reservation_value = self._profile.getProfile().getUtility(reservation_bid)
306
307 """
308 If the last received bid is not empty, add it to the histogram
309 """
310
311 def _count_last_bid(self):
312 domain = self._profile.getProfile().getDomain()
313 domain_issues = domain.getIssues()
314
315 for issue in domain_issues:
316 opponent_bid = self._last_received_bid
317 opp_bid_value = opponent_bid.getValue(issue)
318 if opp_bid_value is not None: # measure against the stupid agent
319 self._last_ten_bids_counts[issue][opp_bid_value] += 1
320
321 """
322 Remove the 11th most recent (i.e. the no longer relevant) bid from the histogram
323 """
324
325 def _uncount_oldest_bid(self):
326 domain = self._profile.getProfile().getDomain()
327 domain_issues = domain.getIssues()
328
329 for issue in domain_issues:
330 oldest_relevant_opp_bid = self._all_opponent_bids[-10]
331 opp_bid_value = oldest_relevant_opp_bid.getValue(issue)
332 self._last_ten_bids_counts[issue][opp_bid_value] -= 1
333
334 """
335 Return a number between 0 and 1 indicating how close the given bid is to the current opponent preference model.
336 """
337
338 def domain_similarity(self, bid: Bid):
339 domain = self._profile.getProfile().getDomain()
340 domain_issues = domain.getIssues()
341 num_issues = len(domain_issues)
342
343 similarity = 0.
344
345 for issue in domain_issues:
346 opp_bid_value = bid.getValue(issue)
347 similarity += (self._last_ten_bids_counts[issue][opp_bid_value] / 10.0) / num_issues
348
349 return similarity
350
351 """
352 Sort the given bids by how close they are to our opponent's preference model (histograms).
353 """
354
355 def sort_bids_by_similarity(self, bids_to_consider) -> list[Bid]:
356 bid_similarities = []
357 for i in bids_to_consider:
358 bid_similarities.append(self.domain_similarity(i))
359
360 bid_similarities_sort_index = np.argsort(bid_similarities)[::-1]
361 sorted_bids = np.array(bids_to_consider)[bid_similarities_sort_index]
362
363 return sorted_bids
364
365 """
366 Iterates over the array of bids sorted by similarity and tries to pick the first that hasn't been offered yet.
367 If all bids from the list were already offered, the first bid is returned.
368 """
369
370 def choose_bid_high_similarity(self, sorted_bids):
371 i = 0
372 chosen_bid = sorted_bids[i]
373 while chosen_bid in self._all_offered_bids and i < len(sorted_bids):
374 chosen_bid = sorted_bids[i]
375 i += 1
376 if i == len(self._all_offered_bids):
377 chosen_bid = sorted_bids[0]
378 return chosen_bid
379
380 """
381 Choose a bid randomly with priority given to those with highest similarity.
382 The choice happens through roulette wheel selection with exponential probabilities (1/2, 1/4, 1/8, ...)
383 """
384
385 def choose_bid_weighted_random(self, sorted_bids):
386 probabilities = [1 / 2 ** (i + 1) for i in range(len(sorted_bids))]
387 probabilities[-1] = probabilities[-2]
388
389 cum_prob = np.cumsum(probabilities)
390 rnd_n = np.random.uniform()
391
392 chosen_bid = None
393 for i in range(len(cum_prob)):
394 if rnd_n < cum_prob[i]:
395 chosen_bid = sorted_bids[i]
396 break
397 return chosen_bid
398
399 """
400 From the given list, choose a bid with priority given to bids with high similarity to the opponent model.
401 Roughly 80% of bids will be chosen deterministically with choose_bid_high_similarity, the remaining 20% are chosen
402 randomly with roulette wheel selection.
403 """
404
405 def best_domain_bid(self, bids_to_consider) -> Bid:
406 sorted_bids = self.sort_bids_by_similarity(bids_to_consider)
407
408 choice_n = np.random.uniform()
409 exploration_constant = 0.8
410 if len(sorted_bids) == 1: # when only one bid is considered, return it
411 chosen_bid = sorted_bids[0]
412 elif choice_n < exploration_constant: # choose the bids with the highest similarity
413 chosen_bid = self.choose_bid_high_similarity(sorted_bids)
414 else: # choose a bid with weighted randomness
415 chosen_bid = self.choose_bid_weighted_random(sorted_bids)
416
417 return chosen_bid
418
419 """
420 From all possible bids, extract those that are close to the target utility.
421 2 * fraction * 100% bids are expected to be extracted, but it can be less when the target utility is very high
422 (not enough bids with higher utility) or very low (not enough bids with lower utility)
423 """
424
425 def bids_close_to_target_util(self, target_utility, fraction=0.025):
426 util_distances = np.abs(np.subtract(self._all_possible_bids_ord_utils, float(target_utility)))
427 closest_bid_index = np.argmin(util_distances)
428 radius = int(fraction * self._num_possible_bids) # number of bids to consider
429 bids_to_consider = self._all_possible_bids_ord[max(0, closest_bid_index - radius):
430 min(len(self._all_possible_bids_ord) - 1,
431 closest_bid_index + radius)]
432 return bids_to_consider
433
434 """
435 From the given list of bids, remove all those that cannot be offered because of utility below reservation value.
436 """
437
438 def remove_bids_below_reservation(self, bids_to_consider):
439 acceptable_bids = []
440 for i in range(len(bids_to_consider)):
441 if self._profile.getProfile().getUtility(bids_to_consider[i]) >= self._reservation_value:
442 acceptable_bids.append(bids_to_consider[i])
443 return acceptable_bids
444
445 """
446 From all possible bids, choose the one with lowest utility that is higher than the reservation value.
447 """
448
449 def find_first_acceptable_bid(self):
450 best_bid = None
451 for i in range(len(self._all_possible_bids_ord)):
452 if self._all_possible_bids_ord_utils[i] >= self._reservation_value:
453 best_bid = self._all_possible_bids_ord[i]
454 break
455 return best_bid
456
457 """
458 Make a fixed number of attempts at finding a random bid that would be acceptable.
459 """
460
461 def find_random_acceptable_bid(self, best_bid, best_bid_util, attempts=100):
462 for _ in range(attempts):
463 bid = self._all_possible_bids.get(randint(0, self._all_possible_bids.size() - 1))
464 if self._isGoodDomainAgent(bid): # if the bid is good, offer it
465 best_bid = bid
466 break
467 # if the bid is not good but better than the best so far, update it
468 if self._profile.getProfile().getUtility(bid) > best_bid_util:
469 best_bid = bid
470 best_bid_util = self._profile.getProfile().getUtility(bid)
471 return best_bid
Note: See TracBrowser for help on using the repository browser.