[74] | 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
|
---|