[74] | 1 | from decimal import Decimal
|
---|
| 2 | import logging
|
---|
| 3 | import math
|
---|
| 4 | from random import randint
|
---|
| 5 | import time
|
---|
| 6 | from typing import Callable, cast
|
---|
| 7 | from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
|
---|
| 8 |
|
---|
| 9 | from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
|
---|
| 10 | from tudelft.utilities.immutablelist.ImmutableList import ImmutableList
|
---|
| 11 | from ...time_dependent_agent.extended_util_space import ExtendedUtilSpace
|
---|
| 12 |
|
---|
| 13 | from geniusweb.actions.Accept import Accept
|
---|
| 14 | from geniusweb.actions.Action import Action
|
---|
| 15 | from geniusweb.actions.Offer import Offer
|
---|
| 16 | from geniusweb.bidspace.AllBidsList import AllBidsList
|
---|
| 17 | from geniusweb.inform.ActionDone import ActionDone
|
---|
| 18 | from geniusweb.inform.Finished import Finished
|
---|
| 19 | from geniusweb.inform.Inform import Inform
|
---|
| 20 | from geniusweb.inform.Settings import Settings
|
---|
| 21 | from geniusweb.inform.YourTurn import YourTurn
|
---|
| 22 | from geniusweb.issuevalue.Bid import Bid
|
---|
| 23 | from geniusweb.party.Capabilities import Capabilities
|
---|
| 24 | from geniusweb.party.DefaultParty import DefaultParty
|
---|
| 25 | from geniusweb.profile.Profile import Profile
|
---|
| 26 | from geniusweb.profileconnection.ProfileConnectionFactory import (
|
---|
| 27 | ProfileConnectionFactory,
|
---|
| 28 | )
|
---|
| 29 | from geniusweb.profileconnection.ProfileInterface import ProfileInterface
|
---|
| 30 | from .group2_frequency_analyzer import FrequencyAnalyzer
|
---|
| 31 | from .group2_plot_trace import plot_characteristics
|
---|
| 32 | from geniusweb.progress.ProgressRounds import ProgressRounds
|
---|
| 33 | from tudelft_utilities_logging.Reporter import Reporter
|
---|
| 34 |
|
---|
| 35 |
|
---|
| 36 | class Agent2(DefaultParty):
|
---|
| 37 | """
|
---|
| 38 | Template agent that offers random bids until a bid with sufficient utility is offered.
|
---|
| 39 | """
|
---|
| 40 |
|
---|
| 41 | def __init__(self, reporter: Reporter = None):
|
---|
| 42 | super().__init__(reporter)
|
---|
| 43 | self.getReporter().log(logging.INFO, "party is initialized")
|
---|
| 44 | self._profileint: ProfileInterface = None # type:ignore
|
---|
| 45 | self._last_received_bid: Bid = None # type:ignore
|
---|
| 46 | self._utilspace: UtilitySpace = None # type:ignore
|
---|
| 47 | self._extendedspace: ExtendedUtilSpace = None # type:ignore
|
---|
| 48 |
|
---|
| 49 | self.highest_social_welfare_bid: list[Bid] = []
|
---|
| 50 |
|
---|
| 51 | # General settings
|
---|
| 52 | self.opponent_model = FrequencyAnalyzer()
|
---|
| 53 | self.reservation_utility: float = .0 # not sure if this is a good value to have, since any agreement is better than no agreement...
|
---|
| 54 | self.concession_speed: float = 11.0 # higher will concede slower (1/e is approximately linear) [0.0, ...]
|
---|
| 55 | self.attempts: int = 500 # the number of iterations it will go through to look for an 'optimal' bid
|
---|
| 56 | self.hard_to_get: float = .2 # the moment from which we'll consider playing nice [0.0, 1.0]
|
---|
| 57 | self.niceness: Decimal = Decimal(.05) # utility we're considering to give up for the sake of being nice [0.0, 1.0]
|
---|
| 58 |
|
---|
| 59 | # Agent characteristics:
|
---|
| 60 | # Can be included in plotting, make sure the dimensionality of all of them match up
|
---|
| 61 | self.lower_utility_bound: list[float] = []
|
---|
| 62 | self.our_social_welfare: list[float] = []
|
---|
| 63 | self.their_social_welfare: list[float] = []
|
---|
| 64 | self.their_social_welfare: list[float] = []
|
---|
| 65 | self.esitmated_opponent_utility: list[float] = []
|
---|
| 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 send 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._profileint = ProfileConnectionFactory.create(
|
---|
| 85 | info.getProfile().getURI(), self.getReporter()
|
---|
| 86 | )
|
---|
| 87 | self.opponent_model.set_domain(self._profileint.getProfile().getDomain())
|
---|
| 88 |
|
---|
| 89 | reservation_bid = self._profileint.getProfile().getReservationBid()
|
---|
| 90 |
|
---|
| 91 | if reservation_bid is not None:
|
---|
| 92 | profile, _ = self._get_profile_and_progress()
|
---|
| 93 | self.reservation_utility = profile.getUtility(reservation_bid)
|
---|
| 94 |
|
---|
| 95 | # ActionDone is an action send by an opponent (an offer or an accept)
|
---|
| 96 | elif isinstance(info, ActionDone):
|
---|
| 97 | action: Action = cast(ActionDone, info).getAction()
|
---|
| 98 |
|
---|
| 99 | # if it is an offer, set the last received bid
|
---|
| 100 | if isinstance(action, Offer):
|
---|
| 101 | self._last_received_bid = cast(Offer, action).getBid()
|
---|
| 102 | # YourTurn notifies you that it is your turn to act
|
---|
| 103 | elif isinstance(info, YourTurn):
|
---|
| 104 | action = self._my_turn()
|
---|
| 105 | if isinstance(self._progress, ProgressRounds):
|
---|
| 106 | self._progress = self._progress.advance()
|
---|
| 107 | self.getConnection().send(action)
|
---|
| 108 |
|
---|
| 109 | # Finished will be send if the negotiation has ended (through agreement or deadline)
|
---|
| 110 | elif isinstance(info, Finished):
|
---|
| 111 | # terminate the agent MUST BE CALLED
|
---|
| 112 | self.terminate()
|
---|
| 113 | else:
|
---|
| 114 | self.getReporter().log(
|
---|
| 115 | logging.WARNING, "Ignoring unknown info " + str(info)
|
---|
| 116 | )
|
---|
| 117 |
|
---|
| 118 | # lets the geniusweb system know what settings this agent can handle
|
---|
| 119 | # leave it as it is for this competition
|
---|
| 120 | def getCapabilities(self) -> Capabilities:
|
---|
| 121 | return Capabilities(
|
---|
| 122 | set(["SAOP"]),
|
---|
| 123 | set(["geniusweb.profile.utilityspace.LinearAdditive"]),
|
---|
| 124 | )
|
---|
| 125 |
|
---|
| 126 | # terminates the agent and its connections
|
---|
| 127 | # leave it as it is for this competition
|
---|
| 128 | def terminate(self):
|
---|
| 129 | self.getReporter().log(logging.INFO, "party is terminating:")
|
---|
| 130 | # self._plot_characteristics()
|
---|
| 131 | super().terminate()
|
---|
| 132 | if self._profileint is not None:
|
---|
| 133 | self._profileint.close()
|
---|
| 134 |
|
---|
| 135 | # ===================
|
---|
| 136 | # === AGENT LOGIC ===
|
---|
| 137 | # ===================
|
---|
| 138 |
|
---|
| 139 | # give a description of your agent
|
---|
| 140 | def getDescription(self) -> str:
|
---|
| 141 | return "Shaken, not stirred"
|
---|
| 142 |
|
---|
| 143 | # execute a turn
|
---|
| 144 | def _my_turn(self):
|
---|
| 145 | self._update_utilspace()
|
---|
| 146 | self.opponent_model.add_bid(self._last_received_bid)
|
---|
| 147 |
|
---|
| 148 | next_bid = self._find_bid(self.attempts)
|
---|
| 149 |
|
---|
| 150 | if self._is_acceptable(self._last_received_bid, next_bid):
|
---|
| 151 | action = Accept(self._me, self._last_received_bid)
|
---|
| 152 | else:
|
---|
| 153 | action = Offer(self._me, next_bid)
|
---|
| 154 |
|
---|
| 155 | # send the action
|
---|
| 156 | return action
|
---|
| 157 |
|
---|
| 158 | # =================
|
---|
| 159 | # === ACCEPTING ===
|
---|
| 160 | # =================
|
---|
| 161 |
|
---|
| 162 | def _is_acceptable(self, bid: Bid, our_next_bid: Bid) -> bool:
|
---|
| 163 | if bid is None:
|
---|
| 164 | return False
|
---|
| 165 |
|
---|
| 166 | profile, _ = self._get_profile_and_progress()
|
---|
| 167 | bid_utility = profile.getUtility(bid)
|
---|
| 168 |
|
---|
| 169 | threshold = self._lower_util_bound()
|
---|
| 170 |
|
---|
| 171 | self.lower_utility_bound.append(threshold)
|
---|
| 172 | self.our_social_welfare.append(float(self._social_welfare(our_next_bid)))
|
---|
| 173 | self.their_social_welfare.append(float(self._social_welfare(bid)))
|
---|
| 174 | self.esitmated_opponent_utility.append(float(self.opponent_model.get_utility(our_next_bid)))
|
---|
| 175 |
|
---|
| 176 | # has to be higher than the reservation value and our threshold, but if the bid is better than we expect we'll always accept
|
---|
| 177 | return (bid_utility > self.reservation_utility and bid_utility > threshold) or bid_utility > profile.getUtility(our_next_bid)
|
---|
| 178 |
|
---|
| 179 | def _lower_util_bound(self) -> float:
|
---|
| 180 | _, progress = self._get_profile_and_progress()
|
---|
| 181 |
|
---|
| 182 | threshold = self._exponential_decrease(progress, self.concession_speed)
|
---|
| 183 |
|
---|
| 184 | return threshold
|
---|
| 185 |
|
---|
| 186 | """
|
---|
| 187 | A function which is 1.0 at x=0.0, and 0.0 at x=1.0.
|
---|
| 188 | k determines how quickly it falls to 0.0; higher k is slower decrease
|
---|
| 189 | - k > 1/e will first fall slowly, then fast
|
---|
| 190 | - k < 1/e will first fall fast, then slow
|
---|
| 191 | - k = 1/e ~ linear
|
---|
| 192 | Rounding errors make x=1.0 not actually intersect (worse with higher k),
|
---|
| 193 | intersection with zero can be forced by setting force_intersect
|
---|
| 194 | """
|
---|
| 195 | def _exponential_decrease(self, x, k, force_intersect=True):
|
---|
| 196 | if force_intersect and x > 0.99:
|
---|
| 197 | return 0.0
|
---|
| 198 |
|
---|
| 199 | return -math.exp(x**k) + 2 - x**k * (-math.e + 2)
|
---|
| 200 |
|
---|
| 201 | # ===============
|
---|
| 202 | # === BIDDING ===
|
---|
| 203 | # ===============
|
---|
| 204 |
|
---|
| 205 | def _find_bid(self, attempts) -> Bid:
|
---|
| 206 | # compose a list of all possible bids
|
---|
| 207 | _, progress = self._get_profile_and_progress()
|
---|
| 208 |
|
---|
| 209 | # it the beginning we'll play hard to get
|
---|
| 210 | # after that we'll consider playing nice
|
---|
| 211 | # => this makes us indicate our interests and gives us
|
---|
| 212 | # the opportunity to collect information about our opponent
|
---|
| 213 | if progress < self.hard_to_get:
|
---|
| 214 | return self._find_max_bid()
|
---|
| 215 | else:
|
---|
| 216 | return self._find_max_nice_bid(attempts)
|
---|
| 217 |
|
---|
| 218 | """
|
---|
| 219 | Gets a random bid from the given list of all_bids
|
---|
| 220 | """
|
---|
| 221 | def _get_random_bid(self, all_bids: ImmutableList[Bid]):
|
---|
| 222 | return all_bids.get(randint(0, all_bids.size() - 1))
|
---|
| 223 |
|
---|
| 224 | """
|
---|
| 225 | Finds the maximum bid according to a certain proposition
|
---|
| 226 | """
|
---|
| 227 | def _find_bid_with(self, proposition: Callable[[Bid, Bid], bool], attempts: int):
|
---|
| 228 | # compose a list of all possible bids
|
---|
| 229 | # TODO Make the selection more constrained, the frequency analyzer performs relatively well
|
---|
| 230 | # but the amount of time it takes to find a good/nice bid can be reduced significantly
|
---|
| 231 | all_bids = AllBidsList(self._profileint.getProfile().getDomain())
|
---|
| 232 |
|
---|
| 233 | # TODO Also consider doing this differently
|
---|
| 234 | maxBid = self._find_lower_bid()
|
---|
| 235 |
|
---|
| 236 | if maxBid is None:
|
---|
| 237 | if len(self.highest_social_welfare_bid) == 0:
|
---|
| 238 | maxBid = self._find_max_bid()
|
---|
| 239 | else:
|
---|
| 240 | maxBid = self.highest_social_welfare_bid[-1]
|
---|
| 241 |
|
---|
| 242 | for _ in range(attempts):
|
---|
| 243 | bid = self._get_random_bid(all_bids)
|
---|
| 244 | maxBid = bid if proposition(bid, maxBid) else maxBid
|
---|
| 245 |
|
---|
| 246 | self.highest_social_welfare_bid.append(maxBid)
|
---|
| 247 | return maxBid
|
---|
| 248 |
|
---|
| 249 | """
|
---|
| 250 | Find a bid according to the current lower bound
|
---|
| 251 | returns _find_max_bid if no bid in that range can be found
|
---|
| 252 | """
|
---|
| 253 | def _find_lower_bid(self):
|
---|
| 254 | lower_bound_bids = self._extendedspace.getBids(min(Decimal(self._lower_util_bound()), Decimal(1) - self.niceness))
|
---|
| 255 |
|
---|
| 256 | if lower_bound_bids.size() == 0:
|
---|
| 257 | return None
|
---|
| 258 |
|
---|
| 259 | return self._get_random_bid(lower_bound_bids)
|
---|
| 260 |
|
---|
| 261 | """
|
---|
| 262 | Find the maximum bids from the domain
|
---|
| 263 | """
|
---|
| 264 | def _find_max_bid(self) -> Bid:
|
---|
| 265 | max_bids = self._extendedspace.getBids(self._extendedspace.getMax())
|
---|
| 266 | return self._get_random_bid(max_bids)
|
---|
| 267 |
|
---|
| 268 | """
|
---|
| 269 | Finds the maximum bid while trying to also accomodate the opponents interests
|
---|
| 270 | according to _is_better_bid with be_nice set to True
|
---|
| 271 | """
|
---|
| 272 | def _find_max_nice_bid(self, attempts) -> Bid:
|
---|
| 273 | # some cheeky CPL currying
|
---|
| 274 | return self._find_bid_with((lambda a, b: self._is_better_bid(a, b, self.niceness, be_nice=True) and self._is_acceptable(a, b)), attempts)
|
---|
| 275 |
|
---|
| 276 | """
|
---|
| 277 | Checks if bid a is better than bid b.
|
---|
| 278 | If be_nice is True, will also consider the opponents utility according to opponent_model and
|
---|
| 279 | is willing to sacrifice a niceness amount of utility when comparing in order to create a win-win
|
---|
| 280 | """
|
---|
| 281 | def _is_better_bid(self, a: Bid, b: Bid, niceness: Decimal, be_nice=False) -> bool:
|
---|
| 282 | profile, _ = self._get_profile_and_progress()
|
---|
| 283 |
|
---|
| 284 | if not be_nice:
|
---|
| 285 | return profile.getUtility(a) >= profile.getUtility(b)
|
---|
| 286 | else:
|
---|
| 287 | # TODO look into niceness possibly accumulating over multiple self.attempts
|
---|
| 288 | # TODO Social welfare metric?
|
---|
| 289 | return profile.getUtility(a) >= profile.getUtility(b) - - niceness \
|
---|
| 290 | and self.opponent_model.get_utility(a) >= self.opponent_model.get_utility(b)
|
---|
| 291 |
|
---|
| 292 | def _social_welfare(self, bid: Bid) -> Decimal:
|
---|
| 293 | profile, _ = self._get_profile_and_progress()
|
---|
| 294 |
|
---|
| 295 | return (profile.getUtility(bid) + Decimal(self.opponent_model.get_utility(bid)))/Decimal(2.0)
|
---|
| 296 |
|
---|
| 297 | # ==============
|
---|
| 298 | # === UTILS ====
|
---|
| 299 | # ==============
|
---|
| 300 |
|
---|
| 301 | def _get_profile_and_progress(self) -> tuple[LinearAdditive, float]:
|
---|
| 302 | profile: Profile = self._profileint.getProfile()
|
---|
| 303 | progress: float = self._progress.get(time.time() * 1000)
|
---|
| 304 |
|
---|
| 305 | return cast(LinearAdditive, profile), progress
|
---|
| 306 |
|
---|
| 307 | def _update_utilspace(self) -> None: # throws IOException
|
---|
| 308 | newutilspace = self._profileint.getProfile()
|
---|
| 309 | if not newutilspace == self._utilspace:
|
---|
| 310 | self._utilspace = cast(LinearAdditive, newutilspace)
|
---|
| 311 | self._extendedspace = ExtendedUtilSpace(self._utilspace)
|
---|
| 312 |
|
---|
| 313 | # ===================
|
---|
| 314 | # === DEBUG TOOLS ===
|
---|
| 315 | # ===================
|
---|
| 316 |
|
---|
| 317 | def _print_utility(self, bid: Bid) -> None:
|
---|
| 318 | profile, _ = self._get_profile_and_progress()
|
---|
| 319 | print("Bid:", bid, "with utility:", profile.getUtility(bid))
|
---|
| 320 |
|
---|
| 321 | def _plot_characteristics(self) -> None:
|
---|
| 322 | characteristics = {
|
---|
| 323 | "lowest acceptable utility": self._plot_space(self.lower_utility_bound, "gray"),
|
---|
| 324 | "social welfare (ours, estimation)": self._plot_space(self.our_social_welfare, "green"),
|
---|
| 325 | "social welfare (theirs, estimation)": self._plot_space(self.their_social_welfare, "blue"),
|
---|
| 326 | "opponent utility (estimation)": self._plot_space(self.esitmated_opponent_utility, "red")
|
---|
| 327 | }
|
---|
| 328 | plot_characteristics(characteristics, len(self.lower_utility_bound))
|
---|
| 329 |
|
---|
| 330 | def _plot_space(self, arr: list, color: str) -> tuple[list, list, str]:
|
---|
| 331 | return (list(range(len(arr))), arr, color)
|
---|