source: ANL2022/compromising_agent/compromising_agent.py

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

#6 added ANAC2022 parties

File size: 23.7 KB
Line 
1import json
2import math
3import os
4from decimal import Decimal
5from os.path import exists
6
7from geniusweb.inform.Agreements import Agreements
8from geniusweb.issuevalue.ValueSet import ValueSet
9from geniusweb.issuevalue.Value import Value
10from geniusweb.issuevalue.DiscreteValue import DiscreteValue
11from geniusweb.issuevalue.NumberValue import NumberValue
12
13import logging
14from random import randint
15import time
16from typing import cast
17
18from geniusweb.actions.Accept import Accept
19from geniusweb.actions.Action import Action
20from geniusweb.actions.Offer import Offer
21from geniusweb.actions.PartyId import PartyId
22from geniusweb.bidspace.AllBidsList import AllBidsList
23from geniusweb.inform.ActionDone import ActionDone
24from geniusweb.inform.Finished import Finished
25from geniusweb.inform.Inform import Inform
26from geniusweb.inform.Settings import Settings
27from geniusweb.inform.YourTurn import YourTurn
28from geniusweb.issuevalue.Bid import Bid
29from geniusweb.issuevalue.Domain import Domain
30from geniusweb.party.Capabilities import Capabilities
31from geniusweb.party.DefaultParty import DefaultParty
32from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
33from geniusweb.profileconnection.ProfileConnectionFactory import (
34 ProfileConnectionFactory,
35)
36from geniusweb.progress.ProgressTime import ProgressTime
37from geniusweb.references.Parameters import Parameters
38from numpy import long
39from tudelft_utilities_logging.ReportToLogger import ReportToLogger
40
41from .LearnedData import LearnedData
42from .NegotiationData import NegotiationData
43from .Pair import Pair
44
45# static vars
46defualtAlpha: float = 10.7
47# estimate opponent time - variant threshold function
48tSplit: int = 40
49# agent has 2 - phases - learning of the opponent and offering bids while considering opponent utility, this constant define the threshold between those two phases
50tPhase: float = 0.2
51
52
53
54class CompromisingAgent(DefaultParty):
55 def __init__(self):
56 super().__init__()
57 self.logger: ReportToLogger = self.getReporter()
58 self.lastReceivedBid: Bid = None
59 self.me: PartyId = None
60 self.progress: ProgressTime = None
61 self.protocol: str = None
62 self.parameters: Parameters = None
63 self.utilitySpace: UtilitySpace = None
64 self.domain: Domain = None
65 self.learnedData: LearnedData = None
66 self.negotiationData: NegotiationData = None
67 self.learnedDataPath: str = None
68 self.negotiationDataPath: str = None
69 self.storage_dir: str = None
70
71 self.opponentName: str = None
72
73 # Expecting Lower Limit of Concession Function behavior
74 # The idea here that we will keep for a negotiation scenario the most frequent
75 # Issues - Values, afterwards, as a counter offer bid for each issue we will select the most frequent value.
76 self.freqMap: dict = None
77
78 # average and standard deviation of the competition for determine "good" utility threshold
79 self.avgUtil: float = 0.95
80 self.stdUtil: float = 0.15
81 self.utilThreshold: float = 0.95
82
83 self.alpha: float = defualtAlpha
84
85 self.opCounter: list = [0] * tSplit
86 self.opSum: list = [0.0] * tSplit
87 self.opThreshold: list = [0.0] * tSplit
88 self.opReject: list = [0.0] * tSplit
89
90 # Best bid for agent, exists if bid space is small enough to search in
91 self.MAX_SEARCHABLE_BIDSPACE: long = 50000
92 self.MIN_UTILITY: float = 0.6
93 self.optimalBid: Bid = None
94 self.bestOfferBid: Bid = None
95 self.allBidList: AllBidsList = None
96
97 self.lastOfferBid = None # our last offer to the opponent
98
99 def notifyChange(self, data: Inform):
100 """
101 Args:
102 data (Inform): Contains either a request for action or information.
103 """
104 try:
105 # a Settings message is the first message that will be send to your
106 # agent containing all the information about the negotiation session.
107 if isinstance(data, Settings):
108 self.settingsFunction(cast(Settings, data))
109
110 # ActionDone informs you of an action (an offer or an accept)
111 # that is performed by one of the agents (including yourself).
112 elif isinstance(data, ActionDone):
113 self.actionDoneFunction(cast(ActionDone, data))
114
115 # YourTurn notifies you that it is your turn to act
116 elif isinstance(data, YourTurn):
117 # execute a turn
118 self.myTurn()
119
120 # Finished will be send if the negotiation has ended (through agreement or deadline)
121 elif isinstance(data, Finished):
122 self.finishedFunction(cast(Finished, data))
123
124 else:
125 self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data))
126
127 except:
128 self.logger.log(logging.ERROR, "error notifyChange")
129
130 def getDescription(self) -> str:
131 """Returns a description of your agent.
132
133 Returns:
134 str: Agent description
135 """
136 return "This is party of ANL 2022. It can handle the Learn protocol and learns utility function and threshold of the opponent."
137
138 def getCapabilities(self) -> Capabilities:
139 """
140 Method to indicate to the protocol what the capabilities of this agent are.
141 Leave it as is for the ANL 2022 competition
142
143 Returns:
144 Capabilities: Capabilities representation class
145 """
146 return Capabilities(
147 set(["SAOP"]),
148 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
149 )
150
151 def finishedFunction(self, data: Finished):
152 # object also contains the final agreement( if any).
153 agreements: Agreements = data.getAgreements()
154 self.processAgreements(agreements)
155
156 # Write the negotiation data that we collected to the path provided.
157 if not (self.negotiationDataPath == None or self.negotiationData == None):
158 try:
159 with open(self.negotiationDataPath, "w") as f:
160 # w means overwritten
161 json.dump(self.negotiationData.__dict__, default=lambda o: o.__dict__, indent=5, fp=f)
162
163
164 except:
165 self.logger.log(logging.ERROR, "Failed to write negotiation data to disk")
166
167 # Write the learned data to the path provided.
168 if not (self.learnedDataPath == None or self.learnedData == None):
169 try:
170 with open(self.learnedDataPath, "w") as f:
171 # w means overwritten
172 json.dump(self.learnedData.__dict__, default=lambda o: o.__dict__, indent=9, fp=f)
173
174
175 except:
176 self.logger.log(logging.ERROR, "Failed to learned data to disk")
177
178 self.logger.log(logging.INFO, "party is terminating:")
179 super().terminate()
180
181 def actionDoneFunction(self, data: ActionDone):
182 # The info object is an action that is performed by an agent.
183 action: Action = data.getAction()
184 actor = action.getActor()
185
186 # Check if this is not our own action
187 if self.me is not None and not (self.me == actor):
188 # Check if we already know who we are playing against.
189 if self.opponentName == None:
190 # The part behind the last _ is always changing, so we must cut it off.
191 self.opponentName = str(actor).rsplit("_", 1)[0]
192
193 # path depend on opponent name
194 self.negotiationDataPath = self.getPath("negotiationData", self.opponentName)
195 self.learnedDataPath = self.getPath("learnedData", self.opponentName)
196
197 # update and load learnedData
198 self.updateAndLoadLearnedData()
199
200 # Add name of the opponent to the negotiation data
201 self.negotiationData.setOpponentName(self.opponentName)
202
203 # avg opponent offer utility
204 self.opThreshold = self.learnedData.getSmoothThresholdOverTime() \
205 if self.learnedData != None else None
206 if not (self.opThreshold == None):
207 for i in range(tSplit):
208 self.opThreshold[i] = self.opThreshold[i] if self.opThreshold[i] > 0 else self.opThreshold[
209 i - 1]
210
211 # max offer the opponent reject
212 self.opReject = self.learnedData.getSmoothRejectOverTime() \
213 if self.learnedData != None else None
214 if not (self.opReject == None):
215 for i in range(tSplit):
216 self.opReject[i] = self.opReject[i] if self.opReject[i] > 0 else self.opReject[
217 i - 1]
218
219 # decay rate of threshold function
220 self.alpha = self.learnedData.getOpponentAlpha() if self.learnedData != None else 0.0
221 self.alpha = self.alpha if self.alpha > 0.0 else defualtAlpha
222
223 # Process the action of the opponent.
224 self.processAction(action)
225
226 def settingsFunction(self, data: Settings):
227 # info is a Settings object that is passed at the start of a negotiation
228 settings: Settings = data
229
230 # ID of my agent
231 self.me = settings.getID()
232
233 # The progress object keeps track of the deadline
234 self.progress = settings.getProgress()
235
236 # Protocol that is initiate for the agent
237 self.protocol = str(settings.getProtocol().getURI().getPath())
238
239 # Parameters for the agent (can be passed through the GeniusWeb GUI, or a JSON-file)
240 self.parameters = settings.getParameters()
241
242 self.storage_dir = self.parameters.get("storage_dir")
243
244 # We are in the negotiation step.
245 # Create a new NegotiationData object to store information on this negotiation.
246 # See 'NegotiationData.py'.
247
248 self.negotiationData = NegotiationData()
249
250 # Obtain our utility space, i.e.the problem we are negotiating and our
251 # preferences over it.
252 try:
253 # the profile contains the preferences of the agent over the domain
254 profile_connection = ProfileConnectionFactory.create(data.getProfile().getURI(), self.getReporter())
255 self.domain = profile_connection.getProfile().getDomain()
256
257 # Create a Issues-Values frequency map
258 if self.freqMap == None:
259 # Map wasn't created before, create a new instance now
260 self.freqMap = {}
261 else:
262 # Map was created before, but this is a new negotiation scenario, clear the old map.
263 self.freqMap.clear()
264
265 # Obtain all of the issues in the current negotiation domain
266 issues: set = self.domain.getIssues()
267 for s in issues:
268 # create new list of all the values for
269 p: Pair = Pair()
270 p.vList = {}
271
272 # gather type of issue based on the first element
273 vs: ValueSet = self.domain.getValues(s)
274 if isinstance(vs.get(0), DiscreteValue):
275 p.type = 0
276 elif isinstance(vs.get(0), NumberValue):
277 p.type = 1
278
279 # Obtain all of the values for an issue "s"
280 for v in vs:
281 # Add a new entry in the frequency map for each(s, v, typeof(v))
282 vStr: str = self.valueToStr(v, p)
283 p.vList[vStr] = 0
284
285 self.freqMap[s] = p
286
287 except:
288 self.logger.log(logging.ERROR, "error settingsFunction")
289
290 # self.utilitySpace = cast(profile_connection.getProfile(), UtilitySpace)
291 self.utilitySpace = profile_connection.getProfile()
292 profile_connection.close()
293
294 self.allBidList = AllBidsList(self.domain)
295
296 # Attempt to find the optimal bid in a search-able bid space, if bid space size
297 # is small / equal to MAX_SEARCHABLE_BIDSPACE
298 if self.allBidList.size() <= self.MAX_SEARCHABLE_BIDSPACE:
299 mx_util: Decimal = Decimal(0)
300 for i in range(self.allBidList.size()):
301 b: Bid = self.allBidList.get(i)
302 canidate: Decimal = self.utilitySpace.getUtility(b)
303 if canidate > mx_util:
304 mx_util = canidate
305 self.optimalBid = b
306
307 else:
308 mx_util: Decimal = Decimal(0)
309 # Iterate randomly through list of bids until we find a good bid
310 for attempt in range(self.MAX_SEARCHABLE_BIDSPACE.intValue()):
311 i: long = randint(0, self.allBidList.size())
312 b: Bid = self.allBidList.get(i)
313 canidate: Decimal = self.utilitySpace.getUtility(b)
314 if canidate > mx_util:
315 mx_util = canidate
316 self.optimalBid = b
317
318 def isNearNegotiationEnd(self):
319 return 0 if self.progress.get(int(time.time() * 1000)) < tPhase else 1
320
321 def processAction(self, action: Action):
322 """Processes an Action performed by the opponent."""
323 if isinstance(action, Offer):
324 # If the action was an offer: Obtain the bid
325 self.lastReceivedBid = cast(Offer, action).getBid()
326 self.updateFreqMap(self.lastReceivedBid)
327
328 # add it's value to our negotiation data.
329 utilVal: float = float(self.utilitySpace.getUtility(self.lastReceivedBid))
330 self.negotiationData.addBidUtil(utilVal)
331
332 def processAgreements(self, agreements: Agreements):
333
334 """ This method is called when the negotiation has finished. It can process the"
335 final agreement.
336 """
337 # Check if we reached an agreement (walking away or passing the deadline
338 # results in no agreement)
339 if agreements.getMap() != None and not (agreements.getMap() == {}):
340 # Get the bid that is agreed upon and add it's value to our negotiation data
341 agreement: Bid = list(agreements.getMap().values())[0]
342 self.negotiationData.addAgreementUtil(float(self.utilitySpace.getUtility(agreement)))
343 self.negotiationData.setOpponentUtil(self.calcOpValue(agreement))
344
345 # negotiation failed
346 else:
347 if not (self.bestOfferBid == None):
348 self.negotiationData.addAgreementUtil(float(self.utilitySpace.getUtility(self.bestOfferBid)))
349
350 # update opponent reject list
351 if self.lastOfferBid != None:
352 self.negotiationData.addRejectUtil(tSplit - 1, self.calcOpValue(self.lastOfferBid))
353
354 # update the opponent offers map, regardless of achieving agreement or not
355 try:
356 self.negotiationData.updateOpponentOffers(self.opSum, self.opCounter);
357 except:
358 self.logger.log(logging.ERROR, "error processAgreements")
359
360 # send our next offer
361 def myTurn(self):
362 action: Action = None
363
364 # save average of the last avgSplit offers (only when frequency table is stabilized)
365 if self.isNearNegotiationEnd() > 0:
366 index: int = (int)((tSplit - 1) / (1 - tPhase) * (self.progress.get(int(time.time() * 1000)) - tPhase))
367
368 if self.lastReceivedBid != None:
369 self.opSum[index] += self.calcOpValue(self.lastReceivedBid)
370 self.opCounter[index] += 1
371
372 if self.lastOfferBid != None:
373 self.negotiationData.addRejectUtil(index, self.calcOpValue(self.lastOfferBid))
374
375 # evaluate the offer and accept or give counter-offer
376 if self.isGood(self.lastReceivedBid):
377 # If the last received bid is good: create Accept action
378 action = Accept(self.me, self.lastReceivedBid)
379 else:
380 # there are 3 phases in the negotiation process:
381 # 1. Send random bids that considered to be GOOD for our agent
382 # 2. Send random bids that considered to be GOOD for both of the agents
383 bid: Bid = None
384
385 if self.bestOfferBid == None:
386 self.bestOfferBid = self.lastReceivedBid
387 elif self.lastReceivedBid != None and self.utilitySpace.getUtility(self.lastReceivedBid) > self.utilitySpace \
388 .getUtility(self.bestOfferBid):
389 self.bestOfferBid = self.lastReceivedBid
390
391 isNearNegotiationEnd = self.isNearNegotiationEnd()
392 if isNearNegotiationEnd == 0:
393 attempt = 0
394 while attempt < 1000 and not self.isGood(bid):
395 attempt += 1
396 i: long = randint(0, self.allBidList.size())
397 bid = self.allBidList.get(i)
398
399 bid = bid if (self.isGood(
400 bid)) else self.optimalBid # if the last bid isn't good, offer (default) the optimal bid
401
402 elif isNearNegotiationEnd == 1:
403 if self.progress.get(int(time.time() * 1000)) > 0.95:
404 maxOpponentUtility: float = 0.0
405 maxBid: Bid = None
406 i = 0
407 while i < 10000 and self.progress.get(int(time.time() * 1000)) < 0.99:
408 i: long = randint(0, self.allBidList.size())
409 bid = self.allBidList.get(i)
410 if self.isGood(bid) and self.isOpGood(bid):
411 opValue = self.calcOpValue(bid)
412 if opValue > maxOpponentUtility:
413 maxOpponentUtility = opValue
414 maxBid = bid
415 i += 1
416 bid = maxBid
417 else:
418 # look for bid with max utility for opponent
419 maxOpponentUtility: float = 0.0
420 maxBid: Bid = None
421 for i in range(2000):
422 i: long = randint(0, self.allBidList.size())
423 bid = self.allBidList.get(i)
424 if self.isGood(bid) and self.isOpGood(bid):
425 opValue = self.calcOpValue(bid)
426 if opValue > maxOpponentUtility:
427 maxOpponentUtility = opValue
428 maxBid = bid
429 bid = maxBid
430
431 bid = bid if self.isGood(
432 bid) else self.optimalBid # if the last bid isn't good, offer (default) the optimal bid
433 bid = self.bestOfferBid if (self.progress.get(int(time.time() * 1000)) > 0.99) else bid
434
435
436 # Create offer action
437 action = Offer(self.me, bid)
438 self.lastOfferBid = bid
439
440 # Send action
441 self.getConnection().send(action)
442
443 def isGood(self, bid: Bid):
444 """ The method checks if a bid is good.
445 param bid the bid to check
446 return true iff bid is good for us.
447 """
448 if bid == None:
449 return False
450 maxVlue: float = 0.95 * float(
451 self.utilitySpace.getUtility(self.optimalBid)) if not self.optimalBid == None else 0.95
452 avgMaxUtility: float = self.learnedData.getAvgMaxUtility() \
453 if self.learnedData != None \
454 else self.avgUtil
455
456 self.utilThreshold = maxVlue \
457 - (maxVlue - 0.55 * self.avgUtil - 0.4 * avgMaxUtility + 0.5 * pow(self.stdUtil, 2)) \
458 * (math.exp(self.alpha * self.progress.get(int(time.time() * 1000))) - 1) \
459 / (math.exp(self.alpha) - 1)
460
461 if (self.utilThreshold < self.MIN_UTILITY):
462 self.utilThreshold = self.MIN_UTILITY
463
464 return float(self.utilitySpace.getUtility(bid)) >= self.utilThreshold
465
466 def calcOpValue(self, bid: Bid):
467 value: float = 0
468
469 issues = bid.getIssues()
470 valUtil: list = [0] * len(issues)
471 issWeght: list = [0] * len(issues)
472 k: int = 0 # index
473
474 for s in issues:
475 p: Pair = self.freqMap[s]
476 v: Value = bid.getValue(s)
477 vs: str = self.valueToStr(v, p)
478
479 # calculate utility of value (in the issue)
480 sumOfValues: int = 0
481 maxValue: int = 1
482 for vString in p.vList.keys():
483 sumOfValues += p.vList[vString]
484 maxValue = max(maxValue, p.vList[vString])
485
486 # calculate estimated utility of the issuevalue
487 valUtil[k] = p.vList.get(vs) / maxValue
488
489 # calculate the inverse std deviation of the array
490 mean: float = sumOfValues / len(p.vList)
491 for vString in p.vList.keys():
492 issWeght[k] += pow(p.vList.get(vString) - mean, 2)
493 issWeght[k] = 1.0 / math.sqrt((issWeght[k] + 0.1) / len(p.vList))
494
495 k += 1
496
497 sumOfWght: float = 0
498 for k in range(len(issues)):
499 value += valUtil[k] * issWeght[k]
500 sumOfWght += issWeght[k]
501
502 return value / sumOfWght
503
504 def isOpGood(self, bid: Bid):
505 if bid == None:
506 return False
507
508 value: float = self.calcOpValue(bid)
509 index: int = int(((tSplit - 1) / (1 - tPhase) * (self.progress.get(int(
510 time.time() * 1000)) - tPhase)))
511 # change
512 opThreshold: float = max(max(2 * self.opThreshold[index] - 1, self.opReject[index]),
513 0.2) if self.opThreshold != None and self.opReject != None else 0.6
514 return value > opThreshold
515
516 def updateFreqMap(self, bid: Bid):
517 if not (bid == None):
518 issues = bid.getIssues()
519
520 for s in issues:
521 p: Pair = self.freqMap.get(s)
522 v: Value = bid.getValue(s)
523
524 vs: str = self.valueToStr(v, p)
525 p.vList[vs] = (p.vList.get(vs) + 1)
526
527 def valueToStr(self, v: Value, p: Pair):
528 v_str: str = ""
529 if p.type == 0:
530 v_str = cast(DiscreteValue, v).getValue()
531 elif p.type == 1:
532 v_str = cast(NumberValue, v).getValue()
533
534 if v_str == "":
535 print("Warning: Value wasn't found")
536 return v_str
537
538 def getPath(self, dataType: str, opponentName: str):
539 return os.path.join(self.storage_dir, dataType + "_" + opponentName + ".json")
540
541 def updateAndLoadLearnedData(self):
542 # we didn't meet this opponent before
543 if exists(self.negotiationDataPath):
544 try:
545 # Load the negotiation data object of a previous negotiation
546 with open(self.negotiationDataPath, "r") as f:
547 negotiationData: NegotiationData = NegotiationData()
548 negotiationData.encode(list(json.load(f).values()))
549
550 except:
551 self.logger.log(logging.ERROR, "Negotiation data does not exist")
552
553 if exists(self.learnedDataPath):
554 try:
555 # Load the negotiation data object of a previous negotiation
556 with open(self.learnedDataPath, "r") as f:
557 self.learnedData = LearnedData()
558 self.learnedData.encode(list(json.load(f).values()))
559
560 except:
561 self.logger.log(logging.ERROR, "learned data does not exist")
562
563 else:
564 self.learnedData = LearnedData()
565
566 # Process the negotiation data in our learned Data
567 self.learnedData.update(negotiationData)
568 self.avgUtil = self.learnedData.getAvgUtility()
569 self.stdUtil = self.learnedData.getStdUtility()
Note: See TracBrowser for help on using the repository browser.