source: CSE3210/agent52/agent52.py

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

#6 Added CSE3210 parties

File size: 11.9 KB
Line 
1import logging
2import time
3from typing import cast
4from operator import itemgetter
5import numpy as np
6from decimal import Decimal
7
8from geniusweb.actions.Accept import Accept
9from geniusweb.actions.Action import Action
10from geniusweb.actions.Offer import Offer
11from geniusweb.bidspace.AllBidsList import AllBidsList
12from geniusweb.inform.ActionDone import ActionDone
13from geniusweb.inform.Finished import Finished
14from geniusweb.inform.Inform import Inform
15from geniusweb.inform.Settings import Settings
16from geniusweb.inform.YourTurn import YourTurn
17from geniusweb.issuevalue.Bid import Bid
18from geniusweb.party.Capabilities import Capabilities
19from geniusweb.party.DefaultParty import DefaultParty
20from geniusweb.profileconnection.ProfileConnectionFactory import (
21 ProfileConnectionFactory,
22)
23from geniusweb.progress.ProgressRounds import ProgressRounds
24from .FreqModelWeighted import FreqModelWeighted
25from tudelft_utilities_logging.Reporter import Reporter
26
27"""
28BeanBot agent
29"""
30class Agent52(DefaultParty):
31
32 def __init__(self, reporter: Reporter = None):
33 super().__init__(reporter)
34 self.getReporter().log(logging.INFO, "party is initialized")
35 self._profile = None
36 self._last_received_bid: Bid = None
37 self._last_received_action = None
38 self._opp_model = None
39 self._window_size = 10 # last 10 opponent bids are stored window below
40 self._opp_bids_window = []
41 self._opp_best_bid = None
42
43 def notifyChange(self, info: Inform):
44 """This is the entry point of all interaction with your agent after is has been initialised.
45
46 Args:
47 info (Inform): Contains either a request for action or information.
48 """
49
50 # a Settings message is the first message that will be send to your
51 # agent containing all the information about the negotiation session.
52 if isinstance(info, Settings):
53 self._settings: Settings = cast(Settings, info)
54 self._me = self._settings.getID()
55
56 # progress towards the deadline has to be tracked manually through the use of the Progress object
57 self._progress: ProgressRounds = self._settings.getProgress()
58
59 # the profile contains the preferences of the agent over the domain
60 self._profile = ProfileConnectionFactory.create(
61 info.getProfile().getURI(), self.getReporter()
62 )
63
64 # Create the weighted frequency model
65 self._opp_model = FreqModelWeighted.create().With(self._profile.getProfile().getDomain(), None)
66 self._opp_model.__class__ = FreqModelWeighted
67
68
69 # Generate sorted (decr.) list of all possible bids with their corresponding utility values
70 # Create reservation value after
71 profile = self._profile.getProfile()
72 allBids = AllBidsList(self._profile.getProfile().getDomain())
73 self._bid_utility_tuple = [(bid, profile.getUtility(bid)) for bid in allBids]
74 self._bid_utility_tuple.sort(key=itemgetter(1), reverse=True)
75
76 # set reservation value to maximum of (0.4, worst bid utility in domain)
77 alpha = 0.4
78 self._rsv_val = alpha if alpha > self._bid_utility_tuple[-1][1] else self._bid_utility_tuple[-1][1]
79
80 # ActionDone is an action send by an opponent (an offer or an accept)
81 elif isinstance(info, ActionDone):
82 action: Action = cast(ActionDone, info).getAction()
83
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._last_received_action = action
88 # YourTurn notifies you that it is your turn to act
89 elif isinstance(info, YourTurn):
90 action = self._myTurn()
91 if isinstance(self._progress, ProgressRounds):
92 self._progress = self._progress.advance()
93 self.getConnection().send(action)
94
95 # Finished will be send if the negotiation has ended (through agreement or deadline)
96 elif isinstance(info, Finished):
97 # terminate the agent MUST BE CALLED
98 self.terminate()
99 else:
100 self.getReporter().log(
101 logging.WARNING, "Ignoring unknown info " + str(info)
102 )
103
104 # lets the geniusweb system know what settings this agent can handle
105 # leave it as it is for this competition
106 def getCapabilities(self) -> Capabilities:
107 return Capabilities(
108 set(["SAOP"]),
109 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
110 )
111
112 # terminates the agent and its connections
113 # leave it as it is for this competition
114 def terminate(self):
115 self.getReporter().log(logging.INFO, "party is terminating:")
116 super().terminate()
117 if self._profile is not None:
118 self._profile.close()
119 self._profile = None
120
121
122
123 # give a description of your agent
124 def getDescription(self) -> str:
125 return "BeanBot implementation for Collaborative AI course"
126
127 # execute a turn
128 def _myTurn(self):
129 # Update the frequency model and the issue weights with the last received bid
130 self._opp_model = self._opp_model.WithAction(self._last_received_action, self._progress)
131 self._opp_model.__class__ = FreqModelWeighted
132 self._opp_model.updateIssueWeights()
133
134 # Update the best bid offered by the opponent if the last received bid is better for us
135 profile = self._profile.getProfile()
136 self._opp_best_bid = self._last_received_bid \
137 if self._opp_best_bid is None or profile.getUtility(self._last_received_bid) > profile.getUtility(self._opp_best_bid) \
138 else self._opp_best_bid
139
140 # Update the bids in the window of last received bids (window has size self._window_size)
141 if self._last_received_bid is not None:
142 self._opp_bids_window.append(profile.getUtility(self._last_received_bid))
143 if len(self._opp_bids_window) > self._window_size:
144 self._opp_bids_window.pop(0)
145
146 bid = self._findBid()
147 action = Offer(self._me, bid)
148 # AC_combi check whether we should accept current offer or not
149 if self._isGood(self._last_received_bid, bid):
150 action = Accept(self._me, self._last_received_bid)
151
152 # send the action
153 return action
154
155
156 """
157 AC_combi hybrid acceptance strategy.
158 Uses AC_next condition in the first [0, T) fraction of the negotiation.
159 Additionally accepts some form of best opponent offer in phase [T, 1] of the negotiation (end phase).
160 This can be either if the received utility is better than overall best received bid, best bid in window,
161 or average utility in window.
162 """
163 def _isGood(self, offeredBid: Bid, nextBid: Bid, T=0.9) -> bool:
164 if offeredBid is None:
165 return False
166
167 # progress represents fraction of negotiation that has passed
168 profile = self._profile.getProfile()
169 progress = self._progress.get(time.time() * 1000)
170
171 # AC_next or AC_combi if we are in phase [T, 1]
172 accept = profile.getUtility(offeredBid) > profile.getUtility(nextBid) \
173 or (progress > T and self._window_max())
174 return accept
175
176 """
177 The following three methods represent the three possible AC_combi methods described above.
178 """
179 def _window_max(self):
180 # check if better than maximum utility in past window
181 profile = self._profile.getProfile()
182 return profile.getUtility(self._last_received_bid) >= np.max(self._opp_bids_window)
183
184 def _window_avg(self):
185 # check if better than average utility in past window
186 profile = self._profile.getProfile()
187 return profile.getUtility(self._last_received_bid) >= np.mean(self._opp_bids_window)
188
189 def _overall_max(self):
190 # check if better than maximum utility received in entire negotiation
191 profile = self._profile.getProfile()
192 return profile.getUtility(self._last_received_bid) >= profile.getUtility(self._opp_best_bid)
193
194 """
195 Implements bidding strategy inspired by the AgreeableAgent2018 (ANAC2018).
196 Take all bids higher than target utility and pick random one based on opponent preferences to send to opponent.
197 """
198 def _findBid(self) -> Bid:
199 # e value determines concession rate by influencing the shape of the target utility curve
200 e = 0.3
201 max_util = self._bid_utility_tuple[0][1]
202 target_util = self._getUtilityGoal(self._progress.get(time.time() * 1000), e, Decimal(self._rsv_val), max_util)
203 # Allow for some additional (10% of target utility) randomness in the possible bids to send to opponent
204 # in the first half of the negotiation. Otherwise, will send mostly the same bid constantly at first.
205 if self._progress.get(time.time() * 1000) < 0.5:
206 target_util = target_util - Decimal(np.random.uniform(0, 0.1 * float(target_util)))
207
208 candidates = []
209 opp_utilities = []
210 # Find all bids above target utility and store along with the associated opponent utilities of the bids
211 for items in self._bid_utility_tuple:
212 if items[1] < target_util:
213 break
214 candidates.append(items[0])
215 opp_utilities.append(float(self._opp_model.getUtility(items[0])))
216
217 # apply roulette wheel selection to the bids to choose one using exponential fitness function
218 return self._roulette_selection(candidates, opp_utilities, self._fitness_exp)
219
220 """
221 Roulette wheel selection to select a random bid to send.
222 Scale opponent utilities to [0, 1], apply fitness function to it and use fitness values as probabilities
223 for choosing each bid.
224 """
225 def _roulette_selection(self, candidates, utilities, fitness_func, eps=0.0001):
226 normalised_utils = np.array(utilities)
227
228 # if same utilities, choose random bid, otherwise continue scaling to [0, 1]
229 if np.max(normalised_utils) - np.min(normalised_utils) < eps:
230 return np.random.choice(candidates)
231
232 # scale utilities to [0, 1]
233 normalised_utils = (normalised_utils - np.min(normalised_utils)) / (np.max(normalised_utils) - np.min(normalised_utils))
234 # apply fitness function and divide by total sum in order to obtain probability values for each bid
235 fitnesses = fitness_func(normalised_utils)
236 fitnesses /= np.sum(fitnesses)
237 # return random bid using fitnesses as weights, or completely random bid if something went wrong in fitnesses
238 return np.random.choice(candidates, p=fitnesses) if not np.isnan(fitnesses).any() else np.random.choice(candidates)
239
240 """
241 Next two functions allow for two different shapes for the fitness transformation of the utilities
242 """
243 def _fitness_linear(self, normalised_utils):
244 return 0.3 + 0.7 * normalised_utils
245
246 def _fitness_exp(self, normalised_utils, alpha=3):
247 return np.exp(-alpha*(1-normalised_utils))
248
249 """
250 Same function as used by time_dependent_agent to determine the curve of utility target line.
251 """
252 def _getUtilityGoal(
253 self, t: float, e: float, minUtil: Decimal, maxUtil: Decimal
254 ) -> Decimal:
255 """
256 @param t the time in [0,1] where 0 means start of nego and 1 the
257 end of nego (absolute time/round limit)
258 @param e the e value that determinses how fast the party makes
259 concessions with time. Typically around 1. 0 means no
260 concession, 1 linear concession, &gt;1 faster than linear
261 concession.
262 @param minUtil the minimum utility possible in our profile
263 @param maxUtil the maximum utility possible in our profile
264 @return the utility goal for this time and e value
265 """
266
267 ft1 = Decimal(1)
268 if e != 0:
269 ft1 = round(Decimal(1 - pow(t, 1 / e)), 6) # defaults ROUND_HALF_UP
270 return max(min((minUtil + (maxUtil - minUtil) * ft1), maxUtil), minUtil)
Note: See TracBrowser for help on using the repository browser.