source: ANL2022/smart_agent/smart_agent.py

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

#6 added ANAC2022 parties

File size: 26.9 KB
Line 
1import json
2import logging
3import math
4import os.path
5import random
6from decimal import Decimal
7from random import randint
8from time import time
9from typing import cast
10from typing import final
11
12import geniusweb.actions.LearningDone
13from geniusweb.actions.Accept import Accept
14from geniusweb.actions.Action import Action
15from geniusweb.actions.Offer import Offer
16from geniusweb.actions.PartyId import PartyId
17from geniusweb.bidspace.AllBidsList import AllBidsList
18from geniusweb.inform.ActionDone import ActionDone
19from geniusweb.inform.Finished import Finished
20from geniusweb.inform.Inform import Inform
21from geniusweb.inform.Settings import Settings
22from geniusweb.inform.YourTurn import YourTurn
23from geniusweb.issuevalue import DiscreteValue, NumberValue
24from geniusweb.issuevalue.Bid import Bid
25from geniusweb.issuevalue.Domain import Domain
26from geniusweb.party.Capabilities import Capabilities
27from geniusweb.party.DefaultParty import DefaultParty
28from geniusweb.profile.utilityspace import UtilitySpace
29from geniusweb.bidspace.AllBidsList import AllBidsList
30from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import (
31 LinearAdditiveUtilitySpace,
32)
33from geniusweb.profileconnection.ProfileConnectionFactory import (
34 ProfileConnectionFactory,
35)
36from geniusweb.profileconnection.ProfileInterface import (
37 ProfileInterface
38)
39from geniusweb.progress.ProgressRounds import ProgressRounds
40from geniusweb.progress.ProgressTime import ProgressTime
41from geniusweb.references.Parameters import Parameters
42from tudelft_utilities_logging.ReportToLogger import ReportToLogger
43
44from agents.template_agent.utils.opponent_model import OpponentModel
45
46
47class SmartAgent(DefaultParty):
48 def __init__(self):
49 super().__init__()
50
51 self.all_bid_list = None
52 self.logger: ReportToLogger = self.getReporter()
53
54 self.domain: Domain = None
55 self.parameters: Parameters = None
56 self.profile: LinearAdditiveUtilitySpace = None
57 self.profileInt: ProfileInterface = None
58 self.progress: ProgressTime = None
59 self.me: PartyId = None
60 self.random: final(random) = random.Random()
61 self.protocol = ""
62 self.opponent_name: str = None
63 self.settings: Settings = None
64 self.storage_dir: str = None
65
66 self.time_split = 40
67 self.time_phase = 0.2
68 self.new_weight = 0.3
69 self.smooth_width = 3
70 self.opponent_decrease = 0.65
71 self.default_alpha = 10.7
72 self.alpha = self.default_alpha
73
74 self.opponent_avg_utility = 0.0
75 self.opponent_negotiations = 0
76 self.opponent_avg_max_utility = {}
77 self.opponent_encounters = {}
78
79 self.std_utility = 0.0
80 self.negotiation_results = []
81 self.avg_opponent_utility = {}
82 self.opponent_alpha = {}
83 self.opponent_sum = [0.0] * 5000
84 self.opponent_counter = [0.0] * 5000
85
86 self.persistent_state = {"opponent_alpha": self.default_alpha, "avg_max_utility": 0.0}
87 self.negotiation_data = {"aggreement_util": 0.0, "max_received_util": 0.0, "opponent_name": "", "opponent_util": 0.0,
88 "opponent_util_by_time": [0.0] * self.time_split}
89 self.opponent_utility_by_time = self.negotiation_data["opponent_util_by_time"]
90 self.need_to_read_persistent_data = True
91 self.freqMap = {}
92 self.MAX_SEARCHABLE_BIDSPACE = 50000
93 self.utilitySpace: UtilitySpace = None
94 self.all_bid_list: AllBidsList
95 self.optimalBid: Bid = None
96 self.bestOfferedBid: Bid = None
97 self.utilThreshold = None
98 self.opThreshold = None
99 self.last_received_bid: Bid = None
100 self.opponent_model: OpponentModel = None
101 self.logger.log(logging.INFO, "party is initialized")
102
103 def notifyChange(self, data: Inform):
104 """MUST BE IMPLEMENTED
105 This is the entry point of all interaction with your agent after is has been initialised.
106 How to handle the received data is based on its class type.
107
108 Args:
109 info (Inform): Contains either a request for action or information.
110 """
111
112 # a Settings message is the first message that will be send to your
113 # agent containing all the information about the negotiation session.
114 try:
115 if isinstance(data, Settings):
116 # data is an object that is passed at the start of the negotiation
117 self.settings = cast(Settings, data)
118 # ID of my agent
119 self.me = self.settings.getID()
120
121 # progress towards the deadline has to be tracked manually through the use of the Progress object
122 self.progress = self.settings.getProgress()
123
124 self.protocol = self.settings.getProtocol().getURI().getPath()
125 self.parameters = self.settings.getParameters()
126 self.storage_dir = self.parameters.get("storage_dir")
127
128 # TODO: Add persistance
129 # the profile contains the preferences of the agent over the domain
130 profile_connection = ProfileConnectionFactory.create(
131 data.getProfile().getURI(), self.getReporter()
132 )
133 self.profile = profile_connection.getProfile()
134 self.domain = self.profile.getDomain()
135
136 if str(self.settings.getProtocol().getURI()) == "Learn":
137 self.learn()
138 self.getConnection().send(geniusweb.actions.LearningDone.LearningDone)
139 else:
140 # This is the negotiation step
141 try:
142 self.profileInt = ProfileConnectionFactory.create(self.settings.getProfile().getURI(),
143 self.getReporter())
144 domain = self.profileInt.getProfile().getDomain()
145
146 if self.freqMap != {}:
147 self.freqMap.clear()
148 issues = domain.getIssues()
149 for s in issues:
150 pair = ({}, {})
151 vlist = pair[1]
152 vs = domain.getValues(s)
153 if isinstance(vs.get(0), DiscreteValue.DiscreteValue.__class__):
154 pair.type = 0
155 elif isinstance(vs.get(0), NumberValue.NumberValue.__class__):
156 pair.type = 1
157 for v in vs:
158 vlist[str(v)] = 0
159 self.freqMap[s] = pair
160 self.utilitySpace: UtilitySpace.UtilitySpace = self.profileInt.getProfile()
161 self.all_bid_list = AllBidsList(domain)
162
163 bids_zise = self.all_bid_list.size()
164 if bids_zise < self.MAX_SEARCHABLE_BIDSPACE:
165 r = -1
166 elif bids_zise == self.MAX_SEARCHABLE_BIDSPACE:
167 r = 0
168 else:
169 r = 1
170 if r == 0 or r == -1:
171 mx_util = 0
172 bidspace_size = self.all_bid_list.size()
173 for i in range(0, bidspace_size, 1):
174 b: Bid = self.all_bid_list.get(i)
175 candidate = self.utilitySpace.getUtility(b)
176 r = candidate.compare(mx_util)
177 if r == 1:
178 mx_util = candidate
179 self.optimalBid = b
180 else:
181 # Searching for best bid in random subspace
182 mx_util = 0
183 for attempt in range(0,self.MAX_SEARCHABLE_BIDSPACE,1):
184 irandom = random.random(self.all_bid_list.size())
185 b = self.all_bid_list.get(irandom)
186 candidate = self.utilitySpace.getUtility(b)
187 r = candidate.compare(mx_util)
188 if r == 1:
189 mx_util = candidate
190 self.optimalBid = b
191 except:
192 raise Exception("Illegal state exception")
193 profile_connection.close()
194 # ActionDone informs you of an action (an offer or an accept)
195 # that is performed by one of the agents (including yourself).
196 elif isinstance(data, ActionDone):
197 action = cast(ActionDone, data).getAction()
198 actor = action.getActor()
199 # ignore action if it is our action
200 if actor != self.me:
201 # obtain the name of the opponent, cutting of the position ID.
202 self.opponent_name = str(actor).rsplit("_", 1)[0]
203 if self.need_to_read_persistent_data:
204 self.negotiation_data = self.read_persistent_negotiation_data()
205 self.need_to_read_persistent_data = False
206 self.negotiation_data["opponent_name"] = self.opponent_name
207 self.opThreshold = self.getSmoothThresholdOverTime(self.opponent_name)
208 if self.opThreshold is not None:
209 for i in range(1, self.time_split, 1):
210 if self.opThreshold[i] < 0:
211 self.opThreshold[i] = self.opThreshold[i - 1]
212 self.alpha = self.persistent_state["opponent_alpha"]
213 if self.alpha < 0.0:
214 self.alpha = self.default_alpha
215 self.update_negotiation_data()
216
217 # process action done by opponent
218 self.opponent_action(action)
219
220 # YourTurn notifies you that it is your turn to act
221 elif isinstance(data, YourTurn):
222 if isinstance(self.progress, ProgressRounds):
223 self.progress = cast(ProgressRounds, self.progress).advance()
224 self.my_turn()
225 # Finished will be send if the negotiation has ended (through agreement or deadline)
226 elif isinstance(data, Finished):
227 self.negotiation_data["aggreement_util"] = float(self.utilitySpace.getUtility(self.last_received_bid))
228 self.negotiation_data["opponent_util"] = self.calc_opponnets_value(self.last_received_bid)
229 self.update_opponents_offers(self.opponent_sum, self.opponent_counter)
230 self.save_data()
231 # terminate the agent MUST BE CALLED
232 self.logger.log(logging.INFO, "party is terminating:")
233 super().terminate()
234 else:
235 self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data))
236 except:
237 raise Exception("Illegal state exception")
238
239 def getCapabilities(self) -> Capabilities:
240 """MUST BE IMPLEMENTED
241 Method to indicate to the protocol what the capabilities of this agent are.
242 Leave it as is for the ANL 2022 competition
243
244 Returns:
245 Capabilities: Capabilities representation class
246 """
247 return Capabilities(
248 set(["SAOP", "Learn"]),
249 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
250 )
251
252 def send_action(self, action: Action):
253 """Sends an action to the opponent(s)
254
255 Args:
256 action (Action): action of this agent
257 """
258 self.getConnection().send(action)
259
260 # give a description of your agent
261 def getDescription(self) -> str:
262 """MUST BE IMPLEMENTED
263 Returns a description of your agent. 1 or 2 sentences.
264
265 Returns:
266 str: Agent description
267 """
268 return "Smart agent for the ANL 2022 competition"
269
270 def update_frequency_map(self, bid):
271 if bid is not None:
272 issues = bid.getIssues()
273 for s in issues:
274 p = self.freqMap.get(s)
275 v = bid.getValue(s)
276 vList = p[1]
277 vList[str(v)] += 1
278
279 def opponent_action(self, action):
280 """Process an action that was received from the opponent.
281
282 Args:
283 action (Action): action of opponent
284 """
285 # if it is an offer, set the last received bid
286 if isinstance(action, Offer):
287 # create opponent model if it was not yet initialised
288 if self.opponent_model is None:
289 self.opponent_model = OpponentModel(self.domain)
290
291 bid = cast(Offer, action).getBid()
292 # update opponent model with bid
293 self.opponent_model.update(bid)
294 self.update_negotiation_data()
295 # set bid as last received
296 self.last_received_bid = bid
297 self.update_frequency_map(self.last_received_bid)
298 utilVal = self.utilitySpace.getUtility(bid)
299 self.negotiation_data["max_received_util"] = float(utilVal)
300 if isinstance(action, Accept):
301 self.last_received_bid = self.optimalBid
302 def my_turn(self):
303 """This method is called when it is our turn. It should decide upon an action
304 to perform and send this action to the opponent.
305 """
306 if self.is_near_negotiation_end() > 0:
307 index = int((self.time_split - 1) / (1 - self.time_phase) * (self.progress.get(int(time() * 1000)) - self.time_phase))
308 if self.opponent_sum[index]:
309 self.opponent_sum[index] = self.calc_opponnets_value(self.last_received_bid)
310 else:
311 self.opponent_sum[index] += self.calc_opponnets_value(self.last_received_bid)
312 self.opponent_counter[index] += 1
313 # check if the last received offer is good enough
314 if self.accept_condition(self.last_received_bid):
315 # if so, accept the offer
316 action = Accept(self.me, self.last_received_bid)
317 else:
318 # if not, find a bid to propose as counter offer
319 bid: Bid = None
320
321 if self.bestOfferedBid is None:
322 self.bestOfferedBid = self.last_received_bid
323 elif self.utilitySpace.getUtility(self.last_received_bid) > self.utilitySpace.getUtility(
324 self.bestOfferedBid):
325 self.bestOfferedBid = self.last_received_bid
326 if self.is_near_negotiation_end() == 0:
327 for attempt in range(0, 1000, 1):
328 if not self.accept_condition(bid):
329 i = random.randint(0, self.all_bid_list.size())
330 bid = self.all_bid_list.get(i)
331 if self.accept_condition(bid):
332 bid = bid
333 else:
334 bid = self.optimalBid
335
336 else:
337 for attempt in range(0, 1000, 1):
338 if bid != self.optimalBid and not self.accept_condition(bid) and not self.is_opponents_proposal_is_good(bid):
339 i = random.randint(0, self.all_bid_list.size())
340 bid = self.all_bid_list.get(i)
341 if self.progress.get(int(time()) * 1000) > 0.99 and self.accept_condition(self.bestOfferedBid):
342 bid = self.bestOfferedBid
343 if not self.accept_condition(bid):
344 bid = self.optimalBid
345 action = Offer(self.me, bid)
346
347 # send the action
348 self.send_action(action)
349
350 def read_persistent_negotiation_data(self):
351 if os.path.exists(f"{self.storage_dir}/{self.opponent_name}"):
352 with open(f"{self.storage_dir}/{self.opponent_name}", "r") as f:
353 data = json.load(f)
354 return data
355 else:
356 return {"opponent_alpha": self.default_alpha, "aggreement_util": 0.0, "max_received_util": 0.0,
357 "opponent_name": self.opponent_name,
358 "opponent_util": 0.0,
359 "opponent_util_by_time": [0.0] * self.time_split}
360
361 def save_data(self):
362 """This method is called after the negotiation is finished. It can be used to store data
363 for learning capabilities. Note that no extensive calculations can be done within this method.
364 Taking too much time might result in your agent being killed, so use it for storage only.
365 """
366 with open(f"{self.storage_dir}/{self.opponent_name}", "w") as f:
367 f.write(json.dumps(self.negotiation_data))
368
369 def is_near_negotiation_end(self):
370 prog = self.progress.get(time() * 1000)
371 if prog < self.time_phase:
372 return 0
373 else:
374 return 1
375
376 def calc_opponnets_value(self, bid: Bid):
377 if not bid:
378 return 0
379 # # own_utility = self.profile.getProfile().getUtility(bid)
380 # opponent_utility = self.opponent_model.get_predicted_utility(bid) # .getUtility(bid)
381 # return opponent_utility
382 value = 0
383 issues = bid.getIssues()
384 valUtil = [0.0]*len(issues)
385 isWeght = [0.0]*len(issues)
386 k = 0
387 for s in issues:
388 p = self.freqMap.get(s)
389 v = bid.getValue(s)
390 sumOfValues = 0
391 maxValue = 1
392 for vString in p[1].keys():
393 sumOfValues += p[1].get(vString)
394 maxValue = max(maxValue, p[1].get(vString))
395 valUtil[k] = float(p[1].get(vString)/maxValue)
396 mean = float(sumOfValues/len(p[1]))
397 for vString in p[1].keys():
398 isWeght[k] += math.pow(p[1].get(vString) - mean, 2)
399 isWeght[k] = 1.0/(math.sqrt(isWeght[k] + 0.1)/len(p[1]))
400 k += 1
401 sumOfwght = 0
402 for k in range(0, len(issues)):
403 value += valUtil[k] * isWeght[k]
404 sumOfwght += isWeght[k]
405 return value/sumOfwght
406
407 def is_opponents_proposal_is_good(self, bid: Bid):
408 if bid == None:
409 return 0
410 value = self.calc_opponnets_value(bid)
411 index = int(((self.time_split - 1) / (1 - self.time_phase) * (self.progress.get(time() * 1000) - self.time_phase)))
412 if self.opThreshold != None:
413 self.opThreshold = max(1 - 2 * self.opThreshold[index], 0.2)
414 else:
415 self.opThreshold = 0.6
416 return value > self.opThreshold
417
418 ###########################################################################################
419 ################################## Example methods below ##################################
420 ###########################################################################################
421
422 def accept_condition(self, bid: Bid) -> bool:
423 if bid is None or self.opponent_name is None:
424 return False
425 avg_max_utility = self.avg_opponent_utility[self.opponent_name]
426 if self.optimalBid is not None:
427 maxValue = 0.95 * float(self.utilitySpace.getUtility(self.optimalBid))
428 else:
429 maxValue = 0.95
430 if self.isKnownOpponent(self.opponent_name):
431 avg_max_utility = self.avg_opponent_utility[self.opponent_name]
432 if self.alpha != 0:
433 self.utilThreshold = maxValue - (
434 maxValue - 0.6 * self.opponent_avg_utility - 0.4 * avg_max_utility + pow(self.std_utility, 2)) * (
435 math.exp(self.alpha * self.progress.get(time() * 1000) - 1) - 1) / (
436 math.exp(self.alpha) - 1)
437 return self.utilitySpace.getUtility(bid) >= self.utilThreshold
438
439 def find_bid(self) -> Bid:
440 # compose a list of all possible bids
441 domain = self.profile.getDomain()
442 all_bids = AllBidsList(domain)
443
444 best_bid_score = 0.0
445 best_bid = None
446
447 # take 500 attempts to find a bid according to a heuristic score
448 for _ in range(500):
449 bid = all_bids.get(randint(0, all_bids.size() - 1))
450 bid_score = self.score_bid(bid)
451 if bid_score > best_bid_score:
452 best_bid_score, best_bid = bid_score, bid
453
454 return best_bid
455
456 def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float:
457 """Calculate heuristic score for a bid
458
459 Args:
460 bid (Bid): Bid to score
461 alpha (float, optional): Trade-off factor between self interested and
462 altruistic behaviour. Defaults to 0.95.
463 eps (float, optional): Time pressure factor, balances between conceding
464 and Boulware behaviour over time. Defaults to 0.1.
465
466 Returns:
467 float: score
468 """
469 progress = self.progress.get(time() * 1000)
470
471 our_utility = float(self.profile.getUtility(bid))
472
473 time_pressure = 1.0 - progress ** (1 / eps)
474 score = alpha * time_pressure * our_utility
475
476 if self.opponent_model is not None:
477 opponent_utility = self.opponent_model.get_predicted_utility(bid)
478 opponent_score = (1.0 - alpha * time_pressure) * opponent_utility
479 score += opponent_score
480
481 return score
482
483 def learn(self):
484 # not called...
485 return "ok"
486
487 def isKnownOpponent(self, opponent_name):
488 return self.opponent_encounters.get(opponent_name, 0)
489
490 def getSmoothThresholdOverTime(self, opponent_name):
491 if not self.isKnownOpponent(opponent_name):
492 return None
493 opponentTimeUtil = self.negotiation_data["opponent_util_by_time"]
494 smoothedTimeUtil = [0.0] * self.time_split
495
496 for i in range(0, self.time_split, 1):
497 for j in range(max(i - self.smooth_width, 0), min(i + self.smooth_width + 1, self.time_split), 1):
498 smoothedTimeUtil[i] += opponentTimeUtil[j]
499 smoothedTimeUtil[i] /= (min(i + self.smooth_width + 1, self.time_split) - max(i - self.smooth_width, 0))
500 return smoothedTimeUtil
501
502 def calculate_alpha(self, opponent_name):
503 alphaArray = self.getSmoothThresholdOverTime(opponent_name)
504 if alphaArray == None:
505 return self.default_alpha
506 for maxIndex in range(0, self.time_split, 1):
507 if alphaArray[maxIndex] > 0.2:
508 break
509 maxValue = alphaArray[0]
510 minValue = alphaArray[max(maxIndex - self.smooth_width - 1, 0)]
511
512 if maxValue - minValue < 0.1:
513 return self.default_alpha
514 for t in range(0, maxIndex, 1):
515 if alphaArray[t] > (maxValue - self.opponent_decrease * (maxValue - minValue)):
516 break
517 calibratedPolynom = {572.83, -1186.7, 899.29, -284.68, 32.911}
518 alpha = calibratedPolynom[0]
519
520 # lowers utility at 85% of the time why 85% ???
521 tTime = self.time_phase + (1 - self.time_phase) * (
522 maxIndex * (float(t) / self.time_split) + (self.time_split - maxIndex) * 0.85) / self.time_split
523 for i in range(1, len(calibratedPolynom), 1):
524 alpha = alpha * tTime + calibratedPolynom[i]
525
526 return alpha
527
528 def update_opponents_offers(self, op_sum, op_counts):
529 for i in range(0, self.time_split):
530 if op_counts[i] > 0:
531 self.negotiation_data["opponent_util_by_time"][i] = op_sum[i]/op_counts[i]
532 else:
533 self.negotiation_data["opponent_util_by_time"][i] = 0
534
535 def update_negotiation_data(self):
536 if self.negotiation_data.get("aggreement_util") > 0:
537 newUtil = self.negotiation_data.get("aggreement_util")
538 else:
539 newUtil = self.opponent_avg_utility - 1.1 * math.pow(self.std_utility, 2)
540 self.opponent_avg_utility = (self.opponent_avg_utility * self.opponent_negotiations + newUtil) / (
541 self.opponent_negotiations + 1)
542 self.opponent_negotiations += 1
543 self.avg_opponent_utility[self.opponent_name] = self.opponent_avg_utility
544 self.negotiation_results.append(self.negotiation_data["aggreement_util"])
545 self.std_utility = 0.0
546 for util in self.negotiation_results:
547 self.std_utility += math.pow(util - self.opponent_avg_utility, 2)
548 self.std_utility = math.sqrt(self.std_utility / self.opponent_negotiations)
549
550 opponent_name = self.negotiation_data["opponent_name"]
551
552 if opponent_name != "":
553 if self.opponent_encounters.get(opponent_name):
554 encounters = self.opponent_encounters.get(opponent_name)
555 else:
556 encounters = 0
557 self.opponent_encounters[opponent_name] = encounters + 1
558
559 if self.opponent_avg_max_utility.get(opponent_name):
560 avgUtil = self.opponent_avg_max_utility[opponent_name]
561 else:
562 avgUtil = 0.0
563 calculated_opponent_avg_max_utility = (float(avgUtil * encounters) + float(
564 self.negotiation_data["max_received_util"])) / (
565 encounters + 1)
566 self.opponent_avg_max_utility[opponent_name] = calculated_opponent_avg_max_utility
567
568 if self.avg_opponent_utility[opponent_name]:
569 avgOpUtil = self.avg_opponent_utility[opponent_name]
570 else:
571 avgOpUtil = 0.0
572 calculated_opponent_avg_utility = (float(avgOpUtil * encounters) + float(
573 self.negotiation_data["opponent_util"])) / (
574 encounters + 1)
575 self.avg_opponent_utility[opponent_name] = calculated_opponent_avg_utility
576 if self.opponent_utility_by_time:
577 opponentTimeUtility = self.opponent_utility_by_time
578 else:
579 opponentTimeUtility = [0.0] * self.time_split
580
581 newUtilData = self.negotiation_data.get("opponent_util_by_time")
582 if opponentTimeUtility[0] > 0.0:
583 ratio = ((1 - self.new_weight) * opponentTimeUtility[0] + self.new_weight * newUtilData[0] /
584 opponentTimeUtility[0])
585 else:
586 ratio = 1
587 for i in range(0, self.time_split, 1):
588 if newUtilData[i] > 0:
589 valueUtilData = (
590 (1 - self.new_weight) * opponentTimeUtility[i] + self.new_weight * newUtilData[i])
591 opponentTimeUtility[i] = valueUtilData
592 else:
593 opponentTimeUtility[i] *= ratio
594 self.negotiation_data["opponent_util_by_time"] = opponentTimeUtility
595 self.opponent_alpha[opponent_name] = self.calculate_alpha(opponent_name)
596
Note: See TracBrowser for help on using the repository browser.