diff --git a/.gitignore b/.gitignore index e697725e..33614408 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,11 @@ logs results *.sbatch -*.log \ No newline at end of file +*.log +*.csv +/baseline_results +/agents_test +/agents_test +agents_test/storage_dir/DreamTeam109Agent/agents_test_linear_agent_linear_agent_LinearAgent.json +/agents_test/storage_dir +bids_plot.png diff --git a/agents/ANL2022/BIU_agent/BIU_agent.py b/agents/ANL2022/BIU_agent/BIU_agent.py index 3fae15b6..0f3fc0bf 100644 --- a/agents/ANL2022/BIU_agent/BIU_agent.py +++ b/agents/ANL2022/BIU_agent/BIU_agent.py @@ -33,7 +33,7 @@ from geniusweb.references.Parameters import Parameters from tudelft_utilities_logging.ReportToLogger import ReportToLogger -from agents.template_agent.utils.opponent_model import OpponentModel +from agents.agent68.utils.opponent_model import OpponentModel class BIU_agent(DefaultParty): diff --git a/agents/ANL2022/gea_agent/gea_agent.py b/agents/ANL2022/gea_agent/gea_agent.py index 404b4d2c..602943eb 100644 --- a/agents/ANL2022/gea_agent/gea_agent.py +++ b/agents/ANL2022/gea_agent/gea_agent.py @@ -28,7 +28,7 @@ from geniusweb.references.Parameters import Parameters from tudelft_utilities_logging.ReportToLogger import ReportToLogger -from agents.template_agent.utils.opponent_model import OpponentModel +from agents.agent68.utils.opponent_model import OpponentModel # our imports import numpy as np diff --git a/agents/ANL2022/rg_agent/rg_agent.py b/agents/ANL2022/rg_agent/rg_agent.py index 3fa55c63..f2bb9692 100644 --- a/agents/ANL2022/rg_agent/rg_agent.py +++ b/agents/ANL2022/rg_agent/rg_agent.py @@ -29,7 +29,7 @@ from geniusweb.references.Parameters import Parameters from tudelft_utilities_logging.ReportToLogger import ReportToLogger -from agents.template_agent.utils.opponent_model import OpponentModel +from agents.agent68.utils.opponent_model import OpponentModel class RGAgent(DefaultParty): diff --git a/agents/template_agent/__init__.py b/agents/agent68/__init__.py similarity index 100% rename from agents/template_agent/__init__.py rename to agents/agent68/__init__.py diff --git a/agents/agent68/agent68.py b/agents/agent68/agent68.py new file mode 100644 index 00000000..435df79b --- /dev/null +++ b/agents/agent68/agent68.py @@ -0,0 +1,307 @@ +import logging +from random import randint +from time import time +from typing import cast + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import ( + LinearAdditiveUtilitySpace, +) +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressTime import ProgressTime +from geniusweb.references.Parameters import Parameters +from tudelft_utilities_logging.ReportToLogger import ReportToLogger + +from .utils.opponent_model import OpponentModel + + +class Agent68(DefaultParty): + """ + Template of a Python geniusweb agent. + """ + + def __init__(self): + super().__init__() + self.pareto_bids = [] + self.logger: ReportToLogger = self.getReporter() + + self.domain: Domain = None + self.parameters: Parameters = None + self.profile: LinearAdditiveUtilitySpace = None + self.progress: ProgressTime = None + self.me: PartyId = None + self.other: str = None + self.settings: Settings = None + self.storage_dir: str = None + self.current_bid: Bid = None + + self.last_received_bid: Bid = None + self.opponent_model: OpponentModel = None + self.logger.log(logging.INFO, "party is initialized") + + def notifyChange(self, data: Inform): + """MUST BE IMPLEMENTED + This is the entry point of all interaction with your agent after is has been initialised. + How to handle the received data is based on its class type. + + Args: + info (Inform): Contains either a request for action or information. + """ + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(data, Settings): + self.settings = cast(Settings, data) + self.me = self.settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self.progress = self.settings.getProgress() + + self.parameters = self.settings.getParameters() + self.storage_dir = self.parameters.get("storage_dir") + + # the profile contains the preferences of the agent over the domain + profile_connection = ProfileConnectionFactory.create( + data.getProfile().getURI(), self.getReporter() + ) + self.profile = profile_connection.getProfile() + self.domain = self.profile.getDomain() + profile_connection.close() + self.determine_good_utility() + + # ActionDone informs you of an action (an offer or an accept) + # that is performed by one of the agents (including yourself). + elif isinstance(data, ActionDone): + action = cast(ActionDone, data).getAction() + actor = action.getActor() + + # ignore action if it is our action + if actor != self.me: + # obtain the name of the opponent, cutting of the position ID. + self.other = str(actor).rsplit("_", 1)[0] + + # process action done by opponent + self.opponent_action(action) + # YourTurn notifies you that it is your turn to act + elif isinstance(data, YourTurn): + # execute a turn + self.my_turn() + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(data, Finished): + self.save_data() + # terminate the agent MUST BE CALLED + self.logger.log(logging.INFO, "party is terminating:") + super().terminate() + else: + self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data)) + + def getCapabilities(self) -> Capabilities: + """MUST BE IMPLEMENTED + Method to indicate to the protocol what the capabilities of this agent are. + Leave it as is for the ANL 2022 competition + + Returns: + Capabilities: Capabilities representation class + """ + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + def send_action(self, action: Action): + """Sends an action to the opponent(s) + + Args: + action (Action): action of this agent + """ + self.getConnection().send(action) + + # give a description of your agent + def getDescription(self) -> str: + """MUST BE IMPLEMENTED + Returns a description of your agent. 1 or 2 sentences. + + Returns: + str: Agent description + """ + return "Template agent for the ANL 2022 competition" + + def opponent_action(self, action): + """Process an action that was received from the opponent. + + Args: + action (Action): action of opponent + """ + # if it is an offer, set the last received bid + if isinstance(action, Offer): + # create opponent model if it was not yet initialised + if self.opponent_model is None: + self.opponent_model = OpponentModel(self.domain) + + bid = cast(Offer, action).getBid() + + # update opponent model with bid + self.opponent_model.update(bid) + # set bid as last received + self.last_received_bid = bid + + def my_turn(self): + """This method is called when it is our turn. It should decide upon an action + to perform and send this action to the opponent. + """ + self.current_bid = self.find_bid() + # check if the last received offer is good enough + if self.accept_condition(self.last_received_bid): + # if so, accept the offer + action = Accept(self.me, self.last_received_bid) + else: + # if not, find a bid to propose as counter offer + action = Offer(self.me, self.current_bid) + + # send the action + self.send_action(action) + + def save_data(self): + """This method is called after the negotiation is finished. It can be used to store data + for learning capabilities. Note that no extensive calculations can be done within this method. + Taking too much time might result in your agent being killed, so use it for storage only. + """ + data = "Data for learning (see README.md)" + with open(f"{self.storage_dir}/data.md", "w") as f: + f.write(data) + + ########################################################################################### + ################################## Example methods below ################################## + ########################################################################################### + + def determine_good_utility(self): + """Determines the quantile utility by selecting the utility of the 75th highest bid out of 500 random bids.""" + + domain = self.profile.getDomain() + all_bids = AllBidsList(domain) + + utilities = [] + # Take random 10% of all bids + all_bids_size = int(all_bids.size() * 0.1) + for _ in range(all_bids_size): + bid = all_bids.get(randint(0, all_bids.size() - 1)) + utilities.append(float(self.profile.getUtility(bid))) + + utilities.sort(reverse=True) + + top_5 = int(all_bids_size*0.02) + top_10_percent = utilities[:top_5] + self.good_utility_threshold = top_10_percent[-1] + + + def accept_condition(self, bid: Bid) -> bool: + if bid is None: + return False + + ## get progress + progress = self.progress.get(time() * 1000) + + ## get reservation value, if none, then 0.4 + reservation_bid = self.profile.getReservationBid() + reservation_value = float(self.profile.getUtility(reservation_bid)) if reservation_bid is not None else 0.4 + + ## get utilities + bid_utility = float(self.profile.getUtility(bid)) + current_bid_utility = float(self.profile.getUtility(self.current_bid)) + + conditions = [ + ## first period: accept next or accept if better than the calculated 90th percentile + progress < 0.5 and current_bid_utility < bid_utility, + progress < 0.5 and bid_utility > self.good_utility_threshold, + ## second period: accept next, accept if social welfare is more than 0.8 * 90th percentile * 2, + ## or accept if self utility is more than 0.8 * 90th percentile + progress < 0.995 and current_bid_utility < bid_utility, + progress < 0.995 and self.score_bid(bid) > self.good_utility_threshold * 0.8 * 2, + progress < 0.995 and bid_utility > self.good_utility_threshold * 0.8, + ## third period: accept if bid utility is more than the reservation value (last second deal) + progress > 0.995 and bid_utility > reservation_value, + ] + return any(conditions) + + + def find_bid(self) -> Bid: + domain = self.profile.getDomain() + all_bids = AllBidsList(domain) + + # Initialize lists to store candidate bids and new Pareto-efficient bids + candidate_bids = [] + pareto_bids = [] + # Generate 10% of the total bids random bids and evaluate their utility for both the agent and the opponent + for _ in range(int(all_bids.size() * 0.1)): + bid = all_bids.get(randint(0, all_bids.size() - 1)) + our_utility = self.profile.getUtility(bid) + opponent_utility = self.opponent_model.get_predicted_utility(bid) if self.opponent_model is not None else 0 + candidate_bids.append((bid, our_utility, opponent_utility)) + + # Filter out non-Pareto-efficient bids + for bid, our_utility, opponent_utility in candidate_bids: + # A bid is Pareto-efficient if there is no other bid that dominates it in both utilities + if not any( + (other_our_utility >= our_utility and other_opponent_utility > opponent_utility) or + (other_our_utility > our_utility and other_opponent_utility >= opponent_utility) + for _, other_our_utility, other_opponent_utility in candidate_bids + ): + # If the bid is Pareto-efficient, add it to the new Pareto frontier + pareto_bids.append((bid, our_utility, opponent_utility)) + + # If there are Pareto-efficient bids, select the one with the highest score + if pareto_bids: + return max(pareto_bids, key=lambda x: self.score_bid(x[0]))[0] + + # If no Pareto-efficient bids are found, return the first candidate bid + return candidate_bids[0][0] + + + def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float: + """Calculate heuristic score for a bid + + Args: + bid (Bid): Bid to score + alpha (float, optional): Trade-off factor between self interested and + altruistic behaviour. Defaults to 0.95. + eps (float, optional): Time pressure factor, balances between conceding + and Boulware behaviour over time. Defaults to 0.1. + + Returns: + float: score + """ + ## get progress + progress = self.progress.get(time() * 1000) + + ## get utility + our_utility = float(self.profile.getUtility(bid)) + + ## calculate the time pressure + time_pressure = 1.0 - progress ** (1 / eps) + + ## calculate the self bid score depending on the time pressure + score = alpha * time_pressure * our_utility + + ## if there is opponent modelling, calculate the score including the time + ## pressure and add it to the score + if self.opponent_model is not None: + opponent_utility = self.opponent_model.get_predicted_utility(bid) + opponent_score = (1.0 - alpha * time_pressure) * opponent_utility + score += opponent_score + return score diff --git a/agents/template_agent/utils/__init__.py b/agents/agent68/utils/__init__.py similarity index 100% rename from agents/template_agent/utils/__init__.py rename to agents/agent68/utils/__init__.py diff --git a/agents/agent68/utils/opponent_model.py b/agents/agent68/utils/opponent_model.py new file mode 100644 index 00000000..36acf1e3 --- /dev/null +++ b/agents/agent68/utils/opponent_model.py @@ -0,0 +1,98 @@ +import math +from collections import defaultdict + +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value + + +class OpponentModel: + def __init__(self, domain: Domain): + self.offers = [] + self.domain = domain + + self.issue_estimators = { + i: IssueEstimator(v) for i, v in domain.getIssuesValues().items() + } + + def update(self, bid: Bid): + # to keep track of all bids received + self.offers.append(bid) + + # calculating the weight that will be assigned to the current bid + # based on the number of bids received so far + decay_lambda = 0.2 + time_weight = math.exp(-decay_lambda * (len(self.offers) - 1)) + + # go over all the issue estimators and update them with the value from the new bid + for issue_id, issue_estimator in self.issue_estimators.items(): + issue_estimator.update(bid.getValue(issue_id), time_weight) + + def get_predicted_utility(self, bid: Bid): + # if there are no bids yet, the utility is 0 + if len(self.offers) == 0 or bid is None: + return 0 + + # calculate the total weight of all issues + total_weight = sum(est.weight for est in self.issue_estimators.values()) + if total_weight == 0: total_weight = len(self.issue_estimators) + + predicted_utility = 0 + + # go over each issue to calculate the overall weighted utility + for issue_id, issue_estimator in self.issue_estimators.items(): + value = bid.getValue(issue_id) + issue_weight = issue_estimator.weight / total_weight + value_utility = issue_estimator.get_value_utility(value) + predicted_utility += issue_weight * value_utility + + return predicted_utility + + +class IssueEstimator: + def __init__(self, value_set: DiscreteValueSet): + if not isinstance(value_set, DiscreteValueSet): + raise TypeError("This issue estimator only supports discrete value sets") + + self.bids_received = 0 # the number of times this issue has been updated + self.num_values = value_set.size() # the total number of values for current issue + self.value_trackers = defaultdict(ValueEstimator) # we have one estimator per value + self.weight = 0 # how important this issue seems to be + + def update(self, value: Value, time_weight: float): + self.bids_received += 1 + + # update the weighted count of the given issue value + self.value_trackers[value].update(time_weight) + + # get the total weight of all values for this issue + total_weighted = sum(vt.weighted_count for vt in self.value_trackers.values()) + + # recalculate the utilities of all values with the new total weight + for value_tracker in self.value_trackers.values(): + value_tracker.recalculate_utility(total_weighted) + + # this issue is more important if one value persists (the opponent does not concede easily) + if total_weighted > 0: + max_count = max(vt.weighted_count for vt in self.value_trackers.values()) + self.weight = max_count / total_weighted + else: + self.weight = 0 + + def get_value_utility(self, value: Value): + if value in self.value_trackers: return self.value_trackers[value].utility + else: return 0 + + +class ValueEstimator: + def __init__(self): + self.weighted_count = 0 + self.utility = 0 + + def update(self, time_weight: float): + self.weighted_count += time_weight + + def recalculate_utility(self, total_weighted_bids: float): + if total_weighted_bids > 0: self.utility = self.weighted_count / total_weighted_bids + else: self.utility = 0 diff --git a/agents_test/agent007/__init__.py b/agents_test/agent007/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents_test/agent007/agent007.py b/agents_test/agent007/agent007.py new file mode 100644 index 00000000..0e4bdc35 --- /dev/null +++ b/agents_test/agent007/agent007.py @@ -0,0 +1,217 @@ +import logging +import numpy as np +from time import time +from typing import cast +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import LinearAdditiveUtilitySpace +from geniusweb.profileconnection.ProfileConnectionFactory import ProfileConnectionFactory +from geniusweb.progress.ProgressTime import ProgressTime +from geniusweb.references.Parameters import Parameters +from tudelft_utilities_logging.ReportToLogger import ReportToLogger + +class agentBidHistory: + def __init__(self): + self.bidHistory = [] + + def addBid(self, bid, label): + self.bidHistory.append((bid, label)) + +class Agent007(DefaultParty): + """Agent007""" + def __init__(self): + super().__init__() + self._profileint: LinearAdditiveUtilitySpace = None + self.lastOfferedBid = None + self.logger: ReportToLogger = self.getReporter() + self.logger.log(logging.INFO, "party is initialized") + self.me: PartyId = None + self.progress: ProgressTime = None + self.settings: Settings = None + self.domain: Domain = None + self.parameters: Parameters = None + self.other: str = None + self.storage_dir: str = None + self.bidHistory = None + + def notifyChange(self, data: Inform): + """ Arg: info (Inform): Contains either a request for action or information. """ + if isinstance(data, Settings): + self.settings = cast(Settings, data) + self.me = self.settings.getID() + self.progress = self.settings.getProgress() + self._profileint = ProfileConnectionFactory.create( # the profile contains the preferences of the agent over the domain + data.getProfile().getURI(), self.getReporter() + ) + self.parameters = self.settings.getParameters() + self.storage_dir = self.parameters.get("storage_dir") + self.domain = self._profileint.getProfile().getDomain() + self._profileint.close() + self.rejected_bids = [] + self.bidHistory = agentBidHistory() + self.issues = [issue for issue in sorted(self.domain.getIssues())] + self.num_values_in_issue = [self.domain.getValues(issue).size() for issue in self.issues] + self.bid_dict = self.bid_decode() + + elif isinstance(data, ActionDone): # if opponent answered (reject or accept) + action: Action = data.getAction() + if isinstance(action, Offer): # [1] if opponent respond by reject our offer + proposed his offer + if self.lastOfferedBid: # if we have already proposed an offer before + self.rejected_bids.append(self.lastOfferedBid) + self.bidHistory.addBid(self.bid_encode(self.lastOfferedBid), 0) # opponent rejected our offer (negative label) + actor = action.getActor() + self.other = str(actor).rsplit("_", 1)[0] # obtain the name of the opponent, cutting of the position ID. + self.lastOfferedBid = cast(Offer, action).getBid() + self.bidHistory.addBid(self.bid_encode(self.lastOfferedBid), 1) # opponent offer (positive label) + else: # if [2] opponent accepted our offer + self.bidHistory.addBid(self.bid_encode(self.lastOfferedBid), 1) # opponent accepted our offer (positive label) + elif isinstance(data, YourTurn): # [3] YourTurn notifies you that it is your turn to act + action = self.chooseAction() + self.send_action(action) + elif isinstance(data, Finished): # [2] Finished will be send if the negotiation has ended (through agreement or deadline) + self.save_data() + self.logger.log(logging.INFO, "party is terminating:") + super().terminate() # terminate the agent MUST BE CALLED + else: + self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data)) + + def send_action(self, action: Action): + """Sends an action to the opponent(s) """ + self.getConnection().send(action) + + def getCapabilities(self) -> Capabilities: + return Capabilities(set(["SAOP"]),set(["geniusweb.profile.utilityspace.LinearAdditive"])) + + def getDescription(self) -> str: + return "Agent007 for the ANL 2022 competition" + + def save_data(self): + """This method is called after the negotiation is finished. It can be used to store data + for learning capabilities. Note that no extensive calculations can be done within this method. + Taking too much time might result in your agent being killed, so use it for storage only. + """ + data = "Data for learning (see README.md)" + with open(f"{self.storage_dir}/data.md", "w") as f: + f.write(data) + + def bid_decode(self): + ''' perform decoding on the bid''' + bid_dict = {} + for bid in AllBidsList(self.domain): + bid_vals = tuple(self.domain.getValues(issue).getValues().index(bid.getValue(issue)) for issue in self.issues) + bid_dict[bid_vals] = bid + return bid_dict + + def bid_encode(self, bid: Bid): + ''' perform One Hot Encoding on the bid''' + bid_vals = [self.domain.getValues(issue).getValues().index(bid.getValue(issue)) for issue in self.issues] + total_num_values = sum(self.num_values_in_issue) + ohe_vec = np.zeros(1+total_num_values) # added 1 for bias + ohe_vec[0] = 1.0 # the bias term + start = 1 + for i in range(len(self.num_values_in_issue)): + ohe_vec[start + bid_vals[i]] = 1.0 + start += self.num_values_in_issue[i] + return ohe_vec + + def chooseAction(self): + ''' Choose if to accept the last offer or make a new offer + @return The chosen action + ''' + progress = self.progress.get(time() * 1000) + if self.shouldAccept(): + action = Accept(self.me, self.lastOfferedBid) + elif progress > 0.7: # if we have enough data + nextBid = self.get_bid() + self.lastOfferedBid = nextBid + action = Offer(self.me, nextBid) + else: + nextBid = self.findNextBid() + self.lastOfferedBid = nextBid + action = Offer(self.me, nextBid) + return action + + def shouldAccept(self): + ''' + @return Whether to accept the last bid or offer the nextBid + ''' + progress = self.progress.get(time() * 1000) + if self.lastOfferedBid == None: + return False + if progress > 0.97: + return True + if progress > 0.9 and self._profileint.getProfile().getUtility(self.lastOfferedBid) > 0.5: + return True + if progress > 0.8 and self._profileint.getProfile().getUtility(self.lastOfferedBid) > 0.6: + return True + return False + + def get_bid(self): + issue_pos = [1]+[sum(self.num_values_in_issue[:i])+1 for i in range(1, len(self.num_values_in_issue))] + profile = self._profileint.getProfile() + issue_weight = [float(profile.getWeights()[issue]) for issue in profile.getWeights()] + utilities = [profile.getUtilities()[issue] for issue in profile.getUtilities()] + issues_values = [[float(v) for v in util.getUtilities().values()] for util in utilities] + + total_num_values = sum(self.num_values_in_issue) + offered = np.zeros(1+total_num_values) # added 1 for bias + for bid in self.bidHistory.bidHistory: + if bid[1] == 1: + offered = np.add(offered, bid[0]) + + issues_offered = [offered[v_pos: v_pos+v_len] for (v_pos, v_len) in zip(issue_pos, self.num_values_in_issue)] + vec = [] + for i in range(len(self.issues)): + avg = sum(issue_weight) / len(issue_weight) + weight_ = issue_weight[i]/avg + avg = sum(issues_offered[i]) / len(issues_offered[i]) + issues_offered_ = [issue_offered/avg for issue_offered in issues_offered[i]] + avg = (sum(issues_values[i])/len(issues_values[i])) + issues_values_ = [issue_value/avg for issue_value in issues_values[i]] + candidates = [(j,offer,val) for (j,offer,val) in zip(range(len(issues_offered_)), issues_offered_, issues_values_) if (offer >= 1 and val >= 1)] + if len(candidates) == 0: + if weight_ >= 1: + value_id = np.argmax(issues_values_) # select best for my agent + else: + value_id = np.argmax(issues_offered_) # select best for opponent + elif len(candidates) == 1: + value_id = candidates[0][0] # select best for both my agent and opponent + else: + values_ids, offers, values = zip(*candidates) + if weight_ >= 1: + id = np.argmax(values) # select best for my agent + else: + id = np.argmax(offers) # select best for opponent + value_id = values_ids[id] + vec.append(value_id) + bid = self.bid_dict[tuple(vec)] + return bid + + def findNextBid(self): + ''' + @return The next bid to offer + ''' + all_bids = AllBidsList(self.domain) + bestBidEvaluation = 0 + nextBid = None + for _ in range(500): + domain_size = all_bids.size() + id = np.random.randint(domain_size) + bid = all_bids.get(id) + bid_utility = float(self._profileint.getProfile().getUtility(bid)) + if bid_utility >= bestBidEvaluation: + nextBid = bid + bestBidEvaluation = bid_utility + return nextBid diff --git a/agents_test/agent007/utils/__init__.py b/agents_test/agent007/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents/template_agent/utils/opponent_model.py b/agents_test/agent007/utils/opponent_model.py similarity index 100% rename from agents/template_agent/utils/opponent_model.py rename to agents_test/agent007/utils/opponent_model.py diff --git a/agents_test/agent24/agent24.py b/agents_test/agent24/agent24.py new file mode 100644 index 00000000..63a3754f --- /dev/null +++ b/agents_test/agent24/agent24.py @@ -0,0 +1,205 @@ +import decimal +import logging +import time +from random import randint +from typing import cast +import numpy as np + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value +from geniusweb.issuevalue.ValueSet import ValueSet +from geniusweb.opponentmodel import FrequencyOpponentModel +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressRounds import ProgressRounds +from tudelft_utilities_logging.Reporter import Reporter + + + +class Agent24(DefaultParty): + """ + Tit-for-tat agent that offers bids according to the opponents bids. + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + self.getReporter().log(logging.INFO, "party is initialized") + self._profile = None + self._last_received_bid: Bid = None + self._opponent_model = FrequencyOpponentModel.FrequencyOpponentModel.create() + self._issue_weights = [] + self._value_changed = [] + self._bids_matrix = [] + self._frequency_matrix = [] + self._previous_bid_enemy = 1 + self._previous_bid_self = 1 + + def notifyChange(self, info: Inform): + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(info, Settings): + self._settings: Settings = cast(Settings, info) + self._me = self._settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self._progress: ProgressRounds = self._settings.getProgress() + + # the profile contains the preferences of the agent over the domain + self._profile = ProfileConnectionFactory.create( + info.getProfile().getURI(), self.getReporter() + ) + # ActionDone is an action send by an opponent (an offer or an accept) + elif isinstance(info, ActionDone): + action: Action = cast(ActionDone, info).getAction() + + # if it is an offer, set the last received bid + if isinstance(action, Offer): + self._last_received_bid = cast(Offer, action).getBid() + # YourTurn notifies you that it is your turn to act + elif isinstance(info, YourTurn): + action = self._myTurn() + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + self.getConnection().send(action) + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(info, Finished): + # terminate the agent MUST BE CALLED + self.terminate() + else: + self.getReporter().log( + logging.WARNING, "Ignoring unknown info " + str(info) + ) + + # lets the geniusweb system know what settings this agent can handle + # leave it as it is for this competition + def getCapabilities(self) -> Capabilities: + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + # terminates the agent and its connections + # leave it as it is for this competition + def terminate(self): + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self._profile is not None: + self._profile.close() + self._profile = None + + # give a description of your agent + def getDescription(self) -> str: + return "The ultimate Bastard tit-for-tat agent" + + # execute a turn + def _myTurn(self): + + if self._last_received_bid is None: + bid = self._findBid(1) + action = Offer(self._me, bid) + else: + len_weights = len(self._last_received_bid.getIssues()) + dict_values = list(self._last_received_bid.getIssueValues().values()) + self._bids_matrix.append(dict_values) + + if len(self._issue_weights) == 0: + for i in range(len_weights): + self._issue_weights.append(1 / len_weights) + self._value_changed.append(1) + self._frequency_matrix.append({dict_values[i]: 1}) + + else: + for i in range(len_weights): + last_bid = list(self._last_received_bid.getIssueValues().values())[i] + if self._bids_matrix[-1][i] != last_bid: + self._value_changed[i] *= 2 + self._issue_weights[i] = (sum(self._value_changed) - self._value_changed[i]) / \ + (sum(self._value_changed) * (len_weights - 1)) + if last_bid not in self._frequency_matrix[i]: + self._frequency_matrix[i][last_bid] = 1 + else: + self._frequency_matrix[i][last_bid] += 1 + + utility = 0 + for i in range(len_weights): + utility += self._issue_weights[i] * (self._frequency_matrix[i][list(self._last_received_bid.getIssueValues().values())[i]] / sum(self._frequency_matrix[i].values())) + + bid = self._findBid(utility) + if self._isOpponentBidGood(self._last_received_bid): + action = Accept(self._me, self._last_received_bid) + + else: + # if not, find a bid to propose as counter offer + action = Offer(self._me, bid) + + # send the action + self._previous_bid_enemy = utility + return action + + # determine if bid should be accepted + def _isOpponentBidGood(self, bid: Bid) -> bool: + if bid is None: + return False + profile = self._profile.getProfile() + progress = self._progress.get(time.time() * 1000) + + # set reservation value + if self._profile.getProfile().getReservationBid() is None: + reservation = 0.0 + else: + reservation = profile.getUtility(self._profile.getProfile().getReservationBid()) + + # ACconst + if profile.getUtility(bid) >= 0.99: + return True + + # boulware/conceder + beta = 0.000000001 # beta: small = boulware, large = conceder, 0.5 = linear + k = 0.9 + a = k + (1.0 - k) * pow(progress, (1.0 / beta)) + min1 = 0.8 + max1 = 1.0 + utility = min1 + (1.0 - a) * (max1 - min1) + if profile.getUtility(bid) >= utility: + return True + + return progress >= 0.99 and profile.getUtility(bid) > reservation + + def _findBid(self, utility) -> Bid: + # compose a list of all possible bids + changed_utility = self._previous_bid_enemy - utility + + domain = self._profile.getProfile().getDomain() + profile = self._profile.getProfile() + all_bids = AllBidsList(domain) + + found = False + for _ in range(5000): + bid = all_bids.get(randint(0, all_bids.size() - 1)) + if -0.2 < decimal.Decimal(self._previous_bid_self) - profile.getUtility(bid) - (decimal.Decimal(changed_utility) * decimal.Decimal(0.3)) < 0.05 and self._previous_bid_self - profile.getUtility(bid) < 0.1: + found = True + break + if not found: + for _ in range(5000): + bid = all_bids.get(randint(0, all_bids.size() - 1)) + if self._previous_bid_self - profile.getUtility(bid) < 0.1: + break + self._previous_bid_self = profile.getUtility(bid) + return bid diff --git a/agents_test/agent32/agent32.py b/agents_test/agent32/agent32.py new file mode 100644 index 00000000..371b83b7 --- /dev/null +++ b/agents_test/agent32/agent32.py @@ -0,0 +1,204 @@ +import logging +import time +from random import randint +from typing import cast +import numpy as np + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value +from geniusweb.issuevalue.ValueSet import ValueSet +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressRounds import ProgressRounds +from tudelft_utilities_logging.Reporter import Reporter + + +class Agent32(DefaultParty): + """ + RAT4TA: Random Tit 4 Tat agent by group 32 + """ + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + self.getReporter().log(logging.INFO, "party is initialized") + self._profile = None + self._last_received_bid: Bid = None + self.previousReceivedBids = [] + self.previousReceivedUtils = [] + self.hasGoodEnemy = True + + + def notifyChange(self, info: Inform): + """This is the entry point of all interaction with your agent after is has been initialised. + + Args: + info (Inform): Contains either a request for action or information. + """ + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(info, Settings): + self._settings: Settings = cast(Settings, info) + self._me = self._settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self._progress = self._settings.getProgress() + + # the profile contains the preferences of the agent over the domain + self._profile = ProfileConnectionFactory.create( + info.getProfile().getURI(), self.getReporter() + ) + # ActionDone is an action send by an opponent (an offer or an accept) + elif isinstance(info, ActionDone): + action: Action = cast(ActionDone, info).getAction() + + # if it is an offer, set the last received bid + if isinstance(action, Offer): + self._last_received_bid = cast(Offer, action).getBid() + # YourTurn notifies you that it is your turn to act + elif isinstance(info, YourTurn): + action = self._myTurn() + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + self.getConnection().send(action) + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(info, Finished): + # terminate the agent MUST BE CALLED + self.terminate() + else: + self.getReporter().log( + logging.WARNING, "Ignoring unknown info " + str(info) + ) + + # lets the geniusweb system know what settings this agent can handle + # leave it as it is for this competition + def getCapabilities(self) -> Capabilities: + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + # terminates the agent and its connections + # leave it as it is for this competition + def terminate(self): + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self._profile is not None: + self._profile.close() + self._profile = None + + + + # give a description of your agent + def getDescription(self) -> str: + return "RAT4TA: RAndom Tit-4-Tat Agent by group 32" + + # Detects wether an enemy is conceiding or hard lining. + # This is done by analyzing the latest bits of the opponent. + # It's not water tight but it functions good enough. + def enemyConceiding(self): + if len(self.previousReceivedUtils) < 10: + return False + value = np.std(self.previousReceivedUtils) + last10Values = self.previousReceivedUtils[-10:] + last5Better = (last10Values[0] + last10Values[1] + last10Values[2] + last10Values[3] + last10Values[4]) < (last10Values[-5] + last10Values[-1] + last10Values[-2] + last10Values[-3] + last10Values[-4]) + # print(value, np.std(last10Values)) + if value > 0.1: return True + if np.std(last10Values) > 0.1 and last5Better: return True + return False + + # execute a turn + def _myTurn(self): + profile = self._profile.getProfile() + # check if the last received offer if the opponent is good enough + if self._isGood(self._last_received_bid): + # if so, accept the offer + action = Accept(self._me, self._last_received_bid) + else: + # if not, find a bid to propose as counter offer + if self._last_received_bid is not None: + self.previousReceivedBids.append([profile.getUtility(self._last_received_bid), self._last_received_bid]) + self.previousReceivedUtils.append(profile.getUtility(self._last_received_bid)) + bid = self._findBid() + action = Offer(self._me, bid) + # send the action + return action + + # method that checks if we would agree with an offer + def _isGood(self, bid: Bid) -> bool: + if bid is None: + return False + profile = self._profile.getProfile() + progress = self._progress.get(time.time() * 1000) + + #To still get some points from a hardlining enemy accept their last bid (since there is no gurantee they will accept our last bid) + if progress >= 0.995: return True + # Checks if the enemy is also conceiding, otherwise only send bids of 0.95 utility + if not self.hasGoodEnemy: return profile.getUtility(bid) > 0.95 + # Send a bid as good as possible at the start + if progress == 0: return profile.getUtility(bid) > 0.98 + # Creates an linear conceiding line ending at 0.65 user utility at the end. + return profile.getUtility(bid) > max (0.99 - 0.35 * progress, 0.65) + + # Used to sort the list of bids + def takeUtility(elem, elem2): + return elem2[0] + + def _findBid(self) -> Bid: + # compose a list of all possible bids + domain = self._profile.getProfile().getDomain() + all_bids = AllBidsList(domain) + profile = self._profile.getProfile() + progress = self._progress.get(time.time() * 1000) + self.validBidOptions = [] + self.allBidOptions = [] + + + self.previousReceivedBids.sort(key= self.takeUtility, reverse=True) + + # After 45% of the bids happend it will check if the enemy is conceiding. + if progress > 0.45: + self.hasGoodEnemy = True if self.enemyConceiding() else False + # take 1000 attempts at finding a random bid that is acceptable to us + for _ in range(1000): + bid = all_bids.get(randint(0, all_bids.size() - 1)) + # Save all bid options generated in the format [[utility_1,bid_1], [utility_2,bid_2] , ... [utility_n,bid_n]] + # This format is used to later sort on these values + self.allBidOptions.append([profile.getUtility(bid), bid]) + if self._isGood(bid): + # Save all valid options in the before mentioned format. note that the bids are not sorted! + self.validBidOptions.append([profile.getUtility(bid), bid]) + try: + # Sort all bid options so that some checks on the best util can be performed + self.allBidOptions.sort(key= self.takeUtility, reverse=True) + except: + print("\n") + + nextBid = None + # Sends the best bid it received back to the other agent if it is the last bid + if(progress >= 0.99 and len(self.previousReceivedBids) > 0): + nextBid = self.previousReceivedBids[0] + # checks if a previous received bit is better than the current selected option. If so send back that bid + elif(len(self.previousReceivedBids) > 0 and len(self.validBidOptions) > 0 and self.previousReceivedBids[0][0] > self.validBidOptions[0][0]): + nextBid = self.previousReceivedBids[0] + else: + # Send back a random valid bid if there is one, otherwise send the best bid for our selves. + # (the first bid in the validBidOptions list is already random since it isnt sorted) + nextBid = self.validBidOptions[0] if len(self.validBidOptions) > 0 else self.allBidOptions[0] + # return the bid + return nextBid[1] \ No newline at end of file diff --git a/agents_test/agent55/Group55OpponentModel.py b/agents_test/agent55/Group55OpponentModel.py new file mode 100644 index 00000000..9fcf0143 --- /dev/null +++ b/agents_test/agent55/Group55OpponentModel.py @@ -0,0 +1,238 @@ +from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace +from geniusweb.opponentmodel.OpponentModel import OpponentModel +from decimal import Decimal +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Bid import Bid +from typing import Dict, Optional +from geniusweb.issuevalue.Value import Value +from geniusweb.actions.Action import Action +from geniusweb.progress.Progress import Progress +from geniusweb.actions.Offer import Offer +from geniusweb.references.Parameters import Parameters +from geniusweb.utils import val, HASH, toStr + + +class FrequencyOpponentModel(UtilitySpace, OpponentModel): + ''' + implements an {@link OpponentModel} by counting frequencies of bids placed by + the opponent. +

+ NOTE: {@link NumberValue}s are also treated as 'discrete', so the frequency + of one value does not influence the influence the frequency of nearby values + (as you might expect as {@link NumberValueSetUtilities} is only affected by + the endpoints). +

+ immutable. + ''' + + _DECIMALS = 4 # accuracy of our computations. + + def __init__(self, domain: Optional[Domain], + freqs: Dict[str, Dict[Value, int]], total: int, + resBid: Optional[Bid]): + ''' + internal constructor. DO NOT USE, see create. Assumes the freqs keyset is + equal to the available issues. + + @param domain the domain. Should not be None + @param freqs the observed frequencies for all issue values. This map is + assumed to be a fresh private-access only copy. + @param total the total number of bids contained in the freqs map. This + must be equal to the sum of the Integer values in the + {@link #bidFrequencies} for each issue (this is not + checked). + @param resBid the reservation bid. Can be null + ''' + self._domain = domain + self._bidFrequencies = freqs + self._totalBids = total + self._resBid = resBid + + """ + These variables are dictionaries with all issues of the domain as their keys. '_BidsChangedFrequency' and + '_previousIssueValue' are helper-structures to construct the final structure: '_issueWeights', which holds the + estimated weight of any issue. + """ + self._BidsChangedFrequency = { + key: 0 for key in self._bidFrequencies.keys()} + self._previousIssueValue = { + key: None for key in self._bidFrequencies.keys()} + self._issueWeights = {key: Decimal( + 1/len(self._bidFrequencies)) for key in self._bidFrequencies.keys()} + + @staticmethod + def create() -> "FrequencyOpponentModel": + return FrequencyOpponentModel(None, {}, 0, None) + + # Override + def With(self, newDomain: Domain, newResBid: Optional[Bid]) -> "FrequencyOpponentModel": + if newDomain == None: + raise ValueError("domain is not initialized") + # FIXME merge already available frequencies? + return FrequencyOpponentModel(newDomain, + {iss: {} + for iss in newDomain.getIssues()}, + 0, newResBid) + + """ + The original implementation provided by Geniusweb calculates the utility for a bid with equal weights for each issue: + '1 / (all issues present in the domain)' Instead of that we now use individual weights for each issue. + """ + # Override + + def getUtility(self, bid: Bid) -> Decimal: + if self._domain == None: + raise ValueError("domain is not initialized") + if self._totalBids == 0: + return Decimal(1) + sum = Decimal(0) + + for issue in val(self._domain).getIssues(): + if issue in bid.getIssues(): + sum += (self._issueWeights[issue] * + self._getFraction(issue, val(bid.getValue(issue)))) + return round(sum, FrequencyOpponentModel._DECIMALS) + + # Override + def getName(self) -> str: + if self._domain == None: + raise ValueError("domain is not initialized") + return "FreqOppModel" + str(hash(self)) + "For" + str(self._domain) + + # Override + def getDomain(self) -> Domain: + return val(self._domain) + + """ + Since this method updates the model with every offer, this is also where we update our + weights-estimation-variables. + """ + # Override + + def WithAction(self, action: Action, progress: Progress) -> "FrequencyOpponentModel": + if self._domain == None: + raise ValueError("domain is not initialized") + + if not isinstance(action, Offer): + return self + + bid: Bid = action.getBid() + newFreqs: Dict[str, Dict[Value, int] + ] = self.cloneMap(self._bidFrequencies) + for issue in self._domain.getIssues(): # type:ignore + freqs: Dict[Value, int] = newFreqs[issue] + value = bid.getValue(issue) + if value != None: + + """" + Added by group55: + If this is the first time the issue is mentioned, we do not do anything. + + if the issue is mentioned before, but the value is not changed, we do not do anything. + + if the issue is mentioned before and the value is changed, we update the 'bidsChangedFrequency' for + that issue. + + In any case, we do update the 'previousIssueValue' afterwards to now be the current value. + """ + if self._previousIssueValue[issue] is not None: + if self._previousIssueValue[issue] is not value: + self._BidsChangedFrequency[issue] += 1 + self._previousIssueValue[issue] = value + + """ + End of Group55 contribution. + """ + + oldfreq = 0 + if value in freqs: + oldfreq = freqs[value] + freqs[value] = oldfreq + 1 # type:ignore + + """ + Added Group55: + Now that all issues have been processed. We loop through them again to calculate their weights. + + First of all, if the total amount in changes is less then the total amount of issues, we keep the default + weights, which are all equal. This is because the calculation below is non-representative with little data. + Therefore, this way, at least all issues have had a change to be changed. + + After that point, all issues-weights are updated as follows: + they are 1 - (the frequency of their changes divided by the total amount of changes of all issues). This way + The more an issue has been changes, the lower the weight. At the end of the loop, all weights will sum up to 1. + """ + totalAmountOfChanges = sum(self._BidsChangedFrequency.values()) + if totalAmountOfChanges >= len(self._domain.getIssues()): + for issue in self._domain.getIssues(): + self._issueWeights[issue] = Decimal( + 1 - (self._BidsChangedFrequency / totalAmountOfChanges)) + + """ + End of Group55 contribution + """ + + return FrequencyOpponentModel(self._domain, newFreqs, + self._totalBids+1, self._resBid) + + def getCounts(self, issue: str) -> Dict[Value, int]: + ''' + @param issue the issue to get frequency info for + @return a map containing a map of values and the number of times that + value was used in previous bids. Values that are possible but not + in the map have frequency 0. + ''' + if self._domain == None: + raise ValueError("domain is not initialized") + if not issue in self._bidFrequencies: + return {} + return dict(self._bidFrequencies.get(issue)) # type:ignore + + # Override + def WithParameters(self, parameters: Parameters) -> OpponentModel: + return self # ignore parameters + + def _getFraction(self, issue: str, value: Value) -> Decimal: + ''' + @param issue the issue to check + @param value the value to check + @return the fraction of the total cases that bids contained given value + for the issue. + ''' + if self._totalBids == 0: + return Decimal(1) + if not (issue in self._bidFrequencies and value in self._bidFrequencies[issue]): + return Decimal(0) + freq: int = self._bidFrequencies[issue][value] + # type:ignore + return round(Decimal(freq) / self._totalBids, FrequencyOpponentModel._DECIMALS) + + @staticmethod + def cloneMap(freqs: Dict[str, Dict[Value, int]]) -> Dict[str, Dict[Value, int]]: + ''' + @param freqs + @return deep copy of freqs map. + ''' + map: Dict[str, Dict[Value, int]] = {} + for issue in freqs: + map[issue] = dict(freqs[issue]) + return map + + # Override + def getReservationBid(self) -> Optional[Bid]: + return self._resBid + + def __eq__(self, other): + return isinstance(other, self.__class__) and \ + self._domain == other._domain and \ + self._bidFrequencies == other._bidFrequencies and \ + self._totalBids == other._totalBids and \ + self._resBid == other._resBid + + def __hash__(self): + return HASH((self._domain, self._bidFrequencies, self._totalBids, self._resBid)) + # Override + + # Override + def __repr__(self) -> str: + return "FrequencyOpponentModel[" + str(self._totalBids) + "," + \ + toStr(self._bidFrequencies) + "]" diff --git a/agents_test/agent55/OpponentModel.py b/agents_test/agent55/OpponentModel.py new file mode 100644 index 00000000..2bee1804 --- /dev/null +++ b/agents_test/agent55/OpponentModel.py @@ -0,0 +1,58 @@ +from abc import ABC, abstractmethod +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Bid import Bid +from geniusweb.references.Parameters import Parameters +from geniusweb.actions.Action import Action +from geniusweb.progress.Progress import Progress + + +class OpponentModel(ABC): + ''' + An opponentmodel estimates a {@link UtilitySpace} from received opponent + actions. +

Requirement

A OpponentModel must have a constructor that takes the + Domain as argument. unfortunately this can not be enforced in a java + interface + +

+ MUST have an empty constructor as these are also used as part of the + BOA framework. + ''' + + @abstractmethod + def With(self, domain: Domain, resBid: Bid) -> "OpponentModel": + ''' + Initializes the model. This function must be called first after + constructing an instance. It can also be called again later, if there is + a change in the domain or resBid. +

+ This late-initialization is to support boa models that have late + initialization. + + @param domain the domain to work with. Must be not null. + @param resBid the reservation bid, or null if no reservationbid is + available. + @return OpponentModel that uses given domain and reservationbid. + ''' + + @abstractmethod + def WithParameters(self, parameters: Parameters) -> "OpponentModel": + ''' + @param parameters Opponent-model specific {@link Parameters} + @return an updated OpponentMode, with parameters used. Each + implementation of OpponentModel is free to use parameters as it + likes. For instance to set learning speed. + ''' + + @abstractmethod + def WithAction(self, action: Action, progress: Progress) -> "OpponentModel": + ''' + Update this with a new action that was done by the opponent that this + model is modeling. {@link #with(Domain, Bid)} must be called before + calling this. + + @param action the new incoming action. + @param progress the current progress of the negotiation. Calls to this + must be done with increasing progress. + @return the updated {@link OpponentModel} + ''' diff --git a/agents_test/agent55/agent55.py b/agents_test/agent55/agent55.py new file mode 100644 index 00000000..6f94435f --- /dev/null +++ b/agents_test/agent55/agent55.py @@ -0,0 +1,457 @@ +import logging +import time +from random import randint, uniform +from typing import cast +from math import log10, floor +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.bidspace.BidsWithUtility import BidsWithUtility +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from geniusweb.bidspace.Interval import Interval +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profileconnection.ProfileConnectionFactory import ProfileConnectionFactory +from geniusweb.progress.ProgressRounds import ProgressRounds +from tudelft_utilities_logging.Reporter import Reporter +import heapq +from decimal import * +from .Group55OpponentModel import FrequencyOpponentModel + + +class Agent55(DefaultParty): + """ + Template agent that offers random bids until a bid with sufficient utility is offered. + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + self._utilspace: LinearAdditive = None + self._bidutils = None + self.getReporter().log(logging.INFO, "party is initialized") + self._profile = None + self._lastReceivedBid: Bid = None + + """ + this will create the opponent model + """ + self.opponentModel = FrequencyOpponentModel.create() + + """ + baselineAcceptableUtility is a utility value for which we accept immediately + """ + self.baselineAcceptableUtility = 0.95 + + """ + hardballOpponentUtilityDelta is the opponent utility change value over which an opponnent is considered to be playing hardball + """ + self.hardballOpponentUtilityDelta = -0.005 + + """ + timePassedAccept is a fixed amount of time passed in the negotiation after which we accept + """ + self.timePassedAccept = 0.95 + + """ + timePassedConcede is a fixed amount of time passed in the negotiation when our agent starts conceding more + """ + self.timePassedConcede = 0.75 + + """ + These two variables will show the average utility-change of their and our agent, throughout their offerings. + This excludes the jump from no offer to the initial offer. Note that the first bid this will return None, so + there must be a check for this. + """ + self.theirAverageUtilityChangeByTheirBids = None + self.ourAverageUtilityChangeByTheirBids = None + + """ + These variables help with the calculation of 'theirAverageUtilityChangeByTheirBids' and + 'ourAverageUtilityChangeByTheirBids'. + """ + self.sumChangeOurUtilitiesByTheirBids = 0 + self.sumChangeTheirUtilitiesByTheirBids = 0 + self.ourUtilityLastTimeByTheirBids = 0 + self.theirUtilityLastTimeByTheirBids = 0 + + """ + Matas: These variables enable our bidding strategy + """ + self.ourBestBids = [] + self.opponentsBestBids = [] + self.roundsSinceBidRecalibration = 0 + self.reCalibrateEveryRounds = 10 + self.randomBidDiscoveryAttemptsPerTurn = 500 + self.acceptableUtilityNormalizationWidth = 0.1 + self.utilityThresholdAdjustmentStep = 0.1 + self.percentOfTimeWeUseOpponentsBestBidIfItIsBetter = 0.7 + self.paddingForUsingRandomBid = 0.1 + self.amountOfBestBidsToKeep = 50 + self.bidsToKeepBasedOnProgressScale = 0.3 + self.opponentNicenessConceedingContributionScale = 0.3 + + def notifyChange(self, info: Inform): + """This is the entry point of all interaction with your agent after is has been initialised. + + Args: + info (Inform): Contains either a request for action or information. + """ + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(info, Settings): + self._settings: Settings = cast(Settings, info) + self._me = self._settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self._progress: ProgressRounds = self._settings.getProgress() + + # the profile contains the preferences of the agent over the domain + self._profile = ProfileConnectionFactory.create( + info.getProfile().getURI(), self.getReporter() + ) + + # create and initialize opponent-model + profile = self._profile.getProfile() + self.opponentModel = self.opponentModel.With( + profile.getDomain(), profile.getReservationBid()) + + # ActionDone is an action send by an opponent (an offer or an accept) + elif isinstance(info, ActionDone): + action: Action = cast(ActionDone, info).getAction() + + # if it is an offer, set the last received bid + if isinstance(action, Offer): + self._lastReceivedBid = cast(Offer, action).getBid() + + """ + Important caveat: anytime we do an offer the program also passes this part and updates the + last_received bid with the offer we made. + + The reason that their variable is called 'lastReceivedBid' is that we access it during our turn and + during our turn this is always the last bid done by the opponent. + + For this reason, we first check if the Action does not contain our id before updating the + opponent model. + """ + if cast(Offer, action).getActor() is not self._me: + self.opponentModel = self.opponentModel.WithAction( + action, self._progress) + self._updateOpponentModel() + + # YourTurn notifies you that it is your turn to act + elif isinstance(info, YourTurn): + action = self._myTurn() + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + self.getConnection().send(action) + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(info, Finished): + # terminate the agent MUST BE CALLED + self.terminate() + else: + self.getReporter().log( + logging.WARNING, "Ignoring unknown info " + str(info) + ) + + # lets the geniusweb system know what settings this agent can handle + # leave it as it is for this competition + def getCapabilities(self) -> Capabilities: + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + # terminates the agent and its connections + # leave it as it is for this competition + def terminate(self): + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self._profile is not None: + self._profile.close() + self._profile = None + + + + # give a description of your agent + + def getDescription(self) -> str: + return "Agent55" + + # execute a turn + def _myTurn(self): + self._updateUtilSpace() + + # Generate a bid according to our current acceptable utility + (aGoodBid, nashProduct) = self._generateAGoodBid() + + # Update our best bid store and fetch the best bid + (currentBestOurBid, currentBestOurBidNashProduct) = self._updateBidsAndGetBestBid( + self.ourBestBids, aGoodBid, nashProduct, floor(self.amountOfBestBidsToKeep * (1 - self._progress.get(time.time() * 1000)) * self.bidsToKeepBasedOnProgressScale)) + + currentBestBid = currentBestOurBid + currentBestBidNashProduct = currentBestOurBidNashProduct + + # If we have a bid from the opponent, store it in the opponent's best bid store + (currentBestTheirBid, currentBestTheirBidNashProduct) = (None, 0) + if self._lastReceivedBid is not None: + (currentBestTheirBid, currentBestTheirBidNashProduct) = self._updateBidsAndGetBestBid( + self.opponentsBestBids, self._lastReceivedBid, self._getNashProduct(self._lastReceivedBid), 1) + + # print("Our best stored bid: {}, their best stored bid: {}".format( + # currentBestOurBidNashProduct, currentBestTheirBidNashProduct)) + + # Pick which best bid we are using as base. Slight random bias towards our best bid. Also the opponent best bid must be more favorable to us. + if currentBestOurBidNashProduct < currentBestTheirBidNashProduct \ + and self.percentOfTimeWeUseOpponentsBestBidIfItIsBetter > uniform(0, 1) \ + and self._profile.getProfile().getUtility(currentBestTheirBid) > self.opponentModel.getUtility(currentBestTheirBid): + + currentBestBid = currentBestTheirBid + currentBestBidNashProduct = currentBestTheirBidNashProduct + + # Use a newly generated bid instead of offering an optimal one with a random chance that is higher at the beginning and lower at the end. + # Moreover, use the freshly generated bids if we are conceding. + if currentBestBid is None or self._progress.get(time.time() * 1000) + self.paddingForUsingRandomBid < uniform(0, 1) or self._progress.get(time.time() * 1000) > self.timePassedConcede: + currentBestBid = aGoodBid + currentBestBidNashProduct = nashProduct + + if self._isAcceptable(self._lastReceivedBid, currentBestBid): + # if so, accept the offer + action = Accept(self._me, self._lastReceivedBid) + else: + # if not, propose a counter offer + + action = Offer(self._me, currentBestBid) + + # send the action + return action + + def _isOpponentPlayingHardball(self) -> bool: + if self.theirAverageUtilityChangeByTheirBids is None: + return False + + return self.theirAverageUtilityChangeByTheirBids > self.hardballOpponentUtilityDelta + + def _getHardballFactor(self) -> Decimal: + timeLeft = self._progress.get(time.time() * 1000) + + # high hardball factor before conceding time + if timeLeft <= self.timePassedConcede: + return 20 + + # opponent is not playing hardball so we can concede less + if not self._isOpponentPlayingHardball(): + return 14 + + return 8 + + def _getAcceptableUtility(self) -> Decimal: + timePassed = self._progress.get(time.time() * 1000) + timeLeft = 1 - timePassed + # the higher the factor the less we concede + hardballFactor = self._getHardballFactor() + + return Decimal(log10(timeLeft) / hardballFactor + self.baselineAcceptableUtility) + + # method that checks if we should accept an offer + def _isAcceptable(self, lastReceivedBid: Bid, proposedBid: Bid) -> bool: + if lastReceivedBid is None or proposedBid is None: + return False + + profile = self._profile.getProfile() + + if profile.getUtility(lastReceivedBid) >= profile.getUtility(proposedBid): + return True + + return self._isGood(lastReceivedBid) + + # method that checks if an offer is considered good + def _isGood(self, lastReceivedBid: Bid) -> bool: + if lastReceivedBid is None: + return False + + progress = self._progress.get(time.time() * 1000) + + if progress >= self.timePassedAccept: + return True + + profile = self._profile.getProfile() + utility = profile.getUtility(lastReceivedBid) + + if utility >= self.baselineAcceptableUtility: + return True + + return utility >= self._getAcceptableUtility() + + def _generateAGoodBid(self) -> tuple[Bid, Decimal]: + # Use the expexted opponent utility to set a range to find a bid that is acceptable to us + + # Starting points + acceptableUtility = self._getAcceptableUtility() + maxUtility = 1 + + # Decrease our max utility if the opponent is taking losses according to our model + if self._progress.get(time.time() * 1000) > self.timePassedConcede: + maxUtility -= (Decimal(self._progress.get(time.time() * 1000)) * + Decimal(self.opponentNicenessConceedingContributionScale) * (1 - self.theirUtilityLastTimeByTheirBids)) + + # Normalize in case we decrease maxUtil by too much. + if maxUtility <= acceptableUtility: + acceptableUtility = maxUtility - \ + Decimal(self.acceptableUtilityNormalizationWidth) + + # Attempt to generate a bid, and adjust our utility thresholds if necessary + while maxUtility <= 1 or acceptableUtility >= 0: + generatedBid, nash = self._generateAGoodBidGivenMinMaxUtil( + acceptableUtility, maxUtility) + if generatedBid is None: + + # Adjust thresholds. First expand the max utility, then reduce the min utility. + if maxUtility < 1: + maxUtility = min( + maxUtility + Decimal(self.utilityThresholdAdjustmentStep), 1) + else: + acceptableUtility = max( + acceptableUtility - Decimal(self.utilityThresholdAdjustmentStep), 0) + + else: + return generatedBid, nash + + # All atempts have failed. Generate a random bid. + return self._generateRandomBid() + + def _generateAGoodBidGivenMinMaxUtil(self, acceptableUtility, maxUtility) -> tuple[Bid, Decimal]: + currentAvailableBids = self._bidutils.getBids( + Interval(acceptableUtility, Decimal(maxUtility)) + ) + + # If no available bids, we can't generate a bid. + if currentAvailableBids.size() == 0: + return None, 0 + + goodBid = currentAvailableBids.get( + randint(0, currentAvailableBids.size() - 1)) + nash = self._getNashProduct(goodBid) + + return goodBid, nash + + def _updateBidsAndGetBestBid(self, bestBids, bestBidFromThisTurn, nashProduct, nBestBids) -> Bid: + self.roundsSinceBidRecalibration += 1 + + # Must at least pick one option + if nBestBids < 1: + nBestBids = 1 + + # After a certain amount of rounds has passed, we recallibrate our bid storage + if self.roundsSinceBidRecalibration >= self.reCalibrateEveryRounds: + self.roundsSinceBidRecalibration = 0 + + # Update and prune + updatedRaw = [self._popAndUpdate(bestBids) + for i in range(min(len(bestBids), self.amountOfBestBidsToKeep))] + + bestBids.clear() + [heapq.heappush(bestBids, x) + for x in updatedRaw] + + # Invert the nash product since heapq is a min queue + invertedNashProduct = 1 - nashProduct + heapq.heappush(bestBids, + (invertedNashProduct, MaxHeapObj(bestBidFromThisTurn))) + + # Pick a bid close to the Nash Equilibrium + toPickFrom = heapq.nsmallest(min(nBestBids, len(bestBids)), bestBids) + + (currentBestInvertedNashProduct, + currentBestBid) = toPickFrom[randint(0, len(toPickFrom) - 1)] + + # Invert nash product and return + return currentBestBid.val, 1 - currentBestInvertedNashProduct + + def _getNashProduct(self, bid) -> Decimal: + utility = self._profile.getProfile().getUtility(bid) + opponentUtility = self.opponentModel.getUtility(bid) + return utility * opponentUtility + + def _popAndUpdate(self, bestBids): + x = heapq.heappop(bestBids) + return (self._getNashProduct(x[1].val), x[1]) + + def _updateUtilSpace(self) -> LinearAdditive: + newutilspace = self._profile.getProfile() + if not newutilspace == self._utilspace: + self._utilspace = newutilspace + self._bidutils = BidsWithUtility.create(self._utilspace) + return self._utilspace + + def _generateRandomBid(self) -> tuple[Bid, Decimal]: + domain = self._profile.getProfile().getDomain() + all_bids = AllBidsList(domain) + bid = None + + # Try to generate a good random bid + for _ in range(self.randomBidDiscoveryAttemptsPerTurn): + candidate = all_bids.get(randint(0, all_bids.size() - 1)) + if self._isGood(candidate): + bid = candidate + break + + # If no good ones found within the allocated attempt count, pick at random + if bid is None: + bid = all_bids.get(randint(0, all_bids.size() - 1)) + + nash = self._getNashProduct(bid) + + return bid, nash + + """ + This method maintains all extensions of the opponent model. Everytime the opponent makes an offer, this gets + updated. Currently the method maintains the following extensions: + * theirAverageUtilityChangeByTheirBids + * ourAverageUtilityChangeByTheirBids + """ + + def _updateOpponentModel(self): + + ###This block calculates: ourAverageUtilityChangeByTheirBids and TheirAverageUtilityChangeByTheirBids ######## + + ourUtilityThisBid = self._profile.getProfile().getUtility(self._lastReceivedBid) + theirUtilityThisBid = self.opponentModel.getUtility( + self._lastReceivedBid) + bidCount = self.opponentModel._totalBids + + # if it's the first offer + if bidCount == 1: + self.ourUtilityLastTimeByTheirBids = ourUtilityThisBid + self.theirUtilityLastTimeByTheirBids = theirUtilityThisBid + else: + ourDifference = ourUtilityThisBid - self.ourUtilityLastTimeByTheirBids + theirDifference = theirUtilityThisBid - self.theirUtilityLastTimeByTheirBids + + self.sumChangeOurUtilitiesByTheirBids += ourDifference + self.sumChangeTheirUtilitiesByTheirBids += theirDifference + + self.theirAverageUtilityChangeByTheirBids = self.sumChangeTheirUtilitiesByTheirBids / \ + (bidCount - 1) + self.ourAverageUtilityChangeByTheirBids = self.sumChangeOurUtilitiesByTheirBids / \ + (bidCount - 1) + + self.ourUtilityLastTimeByTheirBids = ourUtilityThisBid + self.theirUtilityLastTimeByTheirBids = theirUtilityThisBid + ###End of calculation: ourAverageUtilityChangeByTheirBids and TheirAverageUtilityChangeByTheirBids ######## + +# helper for heap + + +class MaxHeapObj(object): + def __init__(self, val): self.val = val + def __lt__(self, other): return True + def __eq__(self, other): return True diff --git a/agents_test/agent61/agent61.py b/agents_test/agent61/agent61.py new file mode 100644 index 00000000..45ba9bef --- /dev/null +++ b/agents_test/agent61/agent61.py @@ -0,0 +1,241 @@ +import copy +import logging +import time +from random import randint, choice +from typing import cast, Dict + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue import Value +from geniusweb.issuevalue.Bid import Bid +from geniusweb.opponentmodel.FrequencyOpponentModel import FrequencyOpponentModel +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace import LinearAdditiveUtilitySpace +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressRounds import ProgressRounds +from tudelft_utilities_logging.Reporter import Reporter + + +class Agent61(DefaultParty): + """ + Template agent that offers random bids until a bid with sufficient utility is offered. + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + self.getReporter().log(logging.INFO, "party is initialized") + self._profile = None + self._received_bids = list() + self._sent_bids = list() + self._best_bid = None + self._last_received_bid: Bid = None + self._last_sent_bid: Bid = None + self._opponent_model: FrequencyOpponentModel = None + self._reservation_value = None + + def notifyChange(self, info: Inform): + """This is the entry point of all interaction with your agent after is has been initialised. + + Args: + info (Inform): Contains either a request for action or information. + """ + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(info, Settings): + self._settings: Settings = cast(Settings, info) + self._me = self._settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self._progress: ProgressRounds = self._settings.getProgress() + + # the profile contains the preferences of the agent over the domain + self._profile = ProfileConnectionFactory.create( + info.getProfile().getURI(), self.getReporter() + ) + # ActionDone is an action send by an opponent (an offer or an accept) + elif isinstance(info, ActionDone): + action: Action = cast(ActionDone, info).getAction() + + # if it is an offer, set the last received bid + if isinstance(action, Offer) and self._me.getName() != action.getActor().getName(): + self._last_received_bid = cast(Offer, action).getBid() + + # Add the action to the opponent model, create one if it doesn't exist + if self._opponent_model is None: + self._opponent_model = FrequencyOpponentModel.create() + self._opponent_model = self._opponent_model \ + .With(newDomain=(self._profile.getProfile()).getDomain(), newResBid=None) + self._opponent_model = FrequencyOpponentModel.WithAction(self._opponent_model, action, + self._progress) + else: + self._opponent_model = FrequencyOpponentModel.WithAction(self._opponent_model, action, + self._progress) + + # YourTurn notifies you that it is your turn to act + elif isinstance(info, YourTurn): + action = self._myTurn() + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + self.getConnection().send(action) + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(info, Finished): + # terminate the agent MUST BE CALLED + self.terminate() + else: + self.getReporter().log( + logging.WARNING, "Ignoring unknown info " + str(info) + ) + + # lets the geniusweb system know what settings this agent can handle + # leave it as it is for this competition + def getCapabilities(self) -> Capabilities: + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + # terminates the agent and its connections + # leave it as it is for this competition + def terminate(self): + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self._profile is not None: + self._profile.close() + self._profile = None + + + + # give a description of your agent + def getDescription(self) -> str: + return "Agent61" + + # Creates the ideal bid for the agent + def _createBestBid(self): + own_prof = self._profile.getProfile() + + bidVals: dict[str, Value] = dict() + prof_vals = own_prof.getDomain().getIssuesValues() + + for issue in prof_vals.keys(): + utilvals = own_prof.getUtilities()[issue] + bidVals[issue] = max(utilvals.getUtilities(), key=utilvals.getUtilities().get) + + self._best_bid = Bid(bidVals) + + # execute a turn + def _myTurn(self): + + if self._best_bid is None: + self._createBestBid() + + # If a reservation bid exists, its utility is the lower bound for accepting / sending offers + if self._reservation_value is None: + if self._profile.getProfile().getReservationBid() is None: + self._reservation_value = 0 + else: + self._reservation_value = self._profile.getProfile().getUtility(self._profile.getProfile().getReservationBid()) + + # check if the last received offer if the opponent is good enough + if self._isGood(self._last_received_bid): + # if so, accept the offer + action = Accept(self._me, self._last_received_bid) + else: + # if not, find a bid to propose as counter offer + # bid = self._findBid() + bid = self._findCounterBid() + action = Offer(self._me, bid) + + # send the action + return action + + # method that checks if we would agree with an offer + def _isGood(self, bid: Bid) -> bool: + if bid is None: + return False + profile = self._profile.getProfile() + progress = self._progress.get(time.time() * 1000) + + diff = (self._opponent_model.getUtility(bid) - profile.getUtility(bid)) + + # Ensure that bid is above reservation value. Additionally, in the early-mid game, + # the utility should be above a time-dependent threshold. After that, the difference + # in utilities between the agent and the opponent should be lower that 0.1 + return (profile.getUtility(bid) > self._reservation_value) and \ + (profile.getUtility(bid) > (0.9 - 0.1 * progress) or \ + (progress > 0.8 and diff < 0.1)) + + # Defines the bidding strategy of the agent, returing the ideal + # bid at the beginning, and otherwise generating intelligent counter-bids. + # The method also saves sent bids for future bidding. + def _findCounterBid(self) -> Bid: + + if self._progress.get(time.time() * 1000) < 0.1: + selected_bid = self._best_bid + else: + selected_bid = self._findCounterBidMutate() + + self._last_sent_bid = selected_bid + self._sent_bids.append(copy.deepcopy(selected_bid)) + return selected_bid + + # Creates a bid by mutating the agent's ideal bid to fit closer + # to what the opponent model believes is beneficial to the other + # party. The more time has passed, the more the ideal bid is mutated + def _mutateBid(self, bid: Bid) -> Bid: + + own_prof = self._profile.getProfile() + bw = own_prof.getWeights() + + sorted_weights = sorted(bw, key=bw.get) + issues_vals = copy.deepcopy(bid.getIssueValues()) + current_index = int((len(sorted_weights) - 1.0) * self._progress.get(time.time() * 1000)) + + while current_index >= 0 and own_prof.getUtility(Bid(issues_vals)) > self._reservation_value: + sel_issue_vals = own_prof.getDomain().getIssuesValues()[sorted_weights[current_index]] + issues_vals[sorted_weights[current_index]] = sel_issue_vals.get(randint(0, sel_issue_vals.size() - 1)) + current_index = current_index - 1 + + return Bid(issues_vals) + + # Finds an intelligent counter bid, relying on opponent modelling and the + # mutateBid function to find a bid that maximizes the Nash product, tries + # to equalize both parties' utility value and that is above reservation + def _findCounterBidMutate(self) -> Bid: + + own_prof = self._profile.getProfile() + + selected_bid = copy.deepcopy(self._last_sent_bid) + max_nash_prod = (own_prof.getUtility(selected_bid) * self._opponent_model.getUtility(selected_bid)) + + for _ in range(50): + newbid = self._mutateBid(copy.deepcopy(self._best_bid)) + new_nash_prod = (own_prof.getUtility(newbid) * self._opponent_model.getUtility(newbid)) + + diff = (self._opponent_model.getUtility(newbid) - own_prof.getUtility(newbid)) + + if new_nash_prod > max_nash_prod and diff < 0.1 and own_prof.getUtility(newbid) > self._reservation_value: + # print("OLD: " + str(max_nash_prod) + ", NEW: " + str(new_nash_prod)) + + max_nash_prod = new_nash_prod + selected_bid = copy.deepcopy(newbid) + + if self._progress.get(time.time() * 1000) > 0.95: + for bid in self._sent_bids: + if abs(self._opponent_model.getUtility(bid) - own_prof.getUtility(bid)) < 0.1 and \ + self._opponent_model.getUtility(bid) > self._opponent_model.getUtility(selected_bid) and \ + own_prof.getUtility(bid) > self._reservation_value: + selected_bid = bid + + return selected_bid diff --git a/agents_test/agent61/visualization.py b/agents_test/agent61/visualization.py new file mode 100644 index 00000000..1b7480df --- /dev/null +++ b/agents_test/agent61/visualization.py @@ -0,0 +1,39 @@ +import matplotlib.pyplot as plt +import json + +dictionary = json.load(open('././results/results_summaries.json', 'r')) + +i = 0 + +for result in dictionary: + print(result.items()) + agreement = (result["result"] == "agreement") + print(agreement) + adding = {'nash_product','social_welfare'} + + xAxis = [] + yAxis = [] + for (key, value) in result.items(): + if adding.__contains__(key): + xAxis.append(key) + yAxis.append(value) + elif 'utility' in key: + key_new ="agent_"+ key[8:] + + xAxis.append(result[key_new][:len(result[key_new]) - 5] + " utility") + yAxis.append(value) + + if agreement: + ## LINE GRAPH ## + color = 'blue' + + ## BAR GRAPH ## + fig = plt.figure() + plt.bar(xAxis, yAxis, alpha=1, color=color, zorder=5) + plt.grid(figure=fig, zorder=0) + plt.xlabel('variable') + plt.ylabel('value') + plt.show() + + fig.savefig("././results/plots/fig" + str(i) + ".png") + i = i+1 diff --git a/agents_test/agent67/agent67.py b/agents_test/agent67/agent67.py new file mode 100644 index 00000000..082310a9 --- /dev/null +++ b/agents_test/agent67/agent67.py @@ -0,0 +1,427 @@ +import logging +import time +import numpy as np +from typing import cast + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value +from geniusweb.issuevalue.ValueSet import ValueSet +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressRounds import ProgressRounds +from tudelft_utilities_logging.Reporter import Reporter + + + +class Agent67(DefaultParty): + """ + Template agent that offers random bids until a bid with sufficient utility is offered. + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + self.getReporter().log(logging.INFO, "party is initialized") + self._profile = None + self._last_received_bid: Bid = None + self.best_offer_opponent: Bid = None + self.best_bid: Bid = None + self.calculated_bid: bool = False + self.opponent_issues = {} + self.opp_history_bids = ({}) + self.sorted_bid = [] + + self.walk_down_counter = 0 + self.curr_walk_down_bid = None + self.whether_walk_down = True + + self.average_util = 0 + self.issues = [] + self.bid_history = {} + self.opp_profile = {} + + # Issues to numeric + self.issue_to_numeric = {} + self.idx_issue = 1 + + # Values to numeric + self.value_to_numeric = {} + self.idx_value = 1 + + def notifyChange(self, info: Inform): + """This is the entry point of all interaction with your agent after is has been initialised. + + Args: + info (Inform): Contains either a request for action or information. + """ + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(info, Settings): + self._settings: Settings = cast(Settings, info) + self._me = self._settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self._progress = self._settings.getProgress() + + # the profile contains the preferences of the agent over the domain + self._profile = ProfileConnectionFactory.create( + info.getProfile().getURI(), self.getReporter() + ) + # ActionDone is an action send by an opponent (an offer or an accept) + elif isinstance(info, ActionDone): + action: Action = cast(ActionDone, info).getAction() + + # if it is an offer, set the last received bid + if isinstance(action, Offer): + self._last_received_bid = cast(Offer, action).getBid() + # YourTurn notifies you that it is your turn to act + elif isinstance(info, YourTurn): + action = self._myTurn() + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + self.getConnection().send(action) + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(info, Finished): + # terminate the agent MUST BE CALLED + self.terminate() + else: + self.getReporter().log( + logging.WARNING, "Ignoring unknown info " + str(info) + ) + + # lets the geniusweb system know what settings this agent can handle + # leave it as it is for this competition + def getCapabilities(self) -> Capabilities: + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + # terminates the agent and its connections + # leave it as it is for this competition + def terminate(self): + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self._profile is not None: + self._profile.close() + self._profile = None + + + + """ + AgentBernie : Using frequency analysis model with walk-down strategy and boulware-style concession. + """ + + def getDescription(self) -> str: + return "AgentBernie : Using frequency analysis model with walk-down strategy and boulware-style concession." + + # execute a turn + def _myTurn(self): + + # Sort the highest bid in decreasing order + if len(self.sorted_bid) <= 0: + self.sort_high_bids() + bid = self.sorted_bid[0] + action = Offer(self._me, bid) + return action + + else: + # Update the opponent profile with the new bid + self.update_bid_history(self._last_received_bid) + self.analyse_opp_profile() + + # check if the last received offer if the opponent is good enough + if self._isGood(self._last_received_bid): + # if so, accept the offer + action = Accept(self._me, self._last_received_bid) + else: + # if not, find a bid to propose as counter offer + bid = self._findBid() + action = Offer(self._me, bid) + + # send the action + return action + + ##################################################################################### + ############################## ACCEPTANCE STRATEGY ################################## + ##################################################################################### + + def _isGood(self, bid: Bid) -> bool: + """ + Checking whether the opponent's offered bid is good or bad. + """ + if bid is None: + return False + profile = self._profile.getProfile() + progress = self._progress.get(time.time() * 1000) + + # If 90% of the rounds towards the deadline have passed + # and no progress made. Then switching to the + # concession strategy + if progress > 0.90 and profile.getUtility(bid) > 0.4: + return True + + # 75% of the rounds towards the deadline have passed + # Using acceptance strategy here called : AC_NEXT + return progress > 0.75 \ + and profile.getUtility(bid) > profile.getUtility(self._findBid()) \ + and self.batna(bid) + + ##################################################################################### + ############################## BIDDING STRATEGY ##################################### + ##################################################################################### + + def _findBid(self) -> Bid: + """ + Finds the best offer for us and the opponent to + reach desirable agreeement and results. + """ + profile = self._profile.getProfile() + + # Walk-down strategy, stop until offered utility value + # is below lambda parameter + if(self.whether_walk_down): + walk_down_bid = self.walk_down_strategy() + util_bid = profile.getUtility(walk_down_bid) + + lambda_value = 0.80 + if util_bid > lambda_value: + return walk_down_bid + else: + self.whether_walk_down = False + return self.find_best_offer() + + return self.find_best_offer() + + def walk_down_strategy(self): + """ + Walk-down strategy : Starting from the highest until + the utility is below lambda parameter. + """ + walk_down_bid = self.sorted_bid[self.walk_down_counter] + self.curr_walk_down_bid = walk_down_bid + self.walk_down_counter += + 1 + + return walk_down_bid + + def batna(self, bid): + """ + Checking whether the bid's utility is + bove batna utility value. + """ + profile = self._profile.getProfile() + reservation_bid = profile.getReservationBid() + + if reservation_bid is None: + return True + else: + return profile.getUtility(bid) > profile.getUtility(reservation_bid) + + def find_best_offer(self) -> Bid: + """ + Finding + """ + for bid in self.sorted_bid: + found = True + + for issue, value in bid.getIssueValues().items(): + num_issue = self.issue_to_numeric[issue] + if self.accept_range(issue, value) \ + and self.batna(bid) \ + and self.opp_profile[num_issue][1] != -1: + continue + else: + found = False + break + if(found): + return bid + curr_walk_down_bid = self.walk_down_strategy() + return curr_walk_down_bid + + ##################################################################################### + ############################## OPPONENT MODELLING ################################### + ##################################################################################### + + def update_bid_history(self, bid): + """ + Adding new bid/offer to the history, if issue is not recorded in the + "issue_to_numeric". Assign a numeric representation of the issue and save it in the + dict. + + Same for values, if there are missing numerical representation for them. Create one in + "value_to_numeric" dict. + """ + bid_dict = bid.getIssueValues().items() + for issue, value in bid_dict: + + # If issue doesn't exist in the categorical-numerical mapping + # in dictionary "map_issues_to_numeric_and_initialize" then + # initialize. Same for values + if issue not in self.issue_to_numeric: + self.map_issues_to_numeric_and_initialize(issue) + if value not in self.value_to_numeric: + self.map_value_to_numeric(value) + + idx_numeric_issue = self.issue_to_numeric[issue] + self.bid_history[idx_numeric_issue].append(value) + + def analyse_opp_profile(self): + """ + Calculate the mode and variance of values per issue to + know the opponent's profile + """ + for issue, values in self.bid_history.items(): + if issue not in self.opp_profile: + self.opp_profile[issue] = () + + # Mapping from categorival to numerical with values from the issue + numerical_values = [self.value_to_numeric[value] + for value in values] + + if(len(numerical_values) == 1): + self.opp_profile[issue] = ( + max(numerical_values, key=numerical_values.count), -1) + else: + self.opp_profile[issue] = ( + max(numerical_values, key=numerical_values.count), np.var(numerical_values, ddof=1)) + + def accept_range(self, issue, value): + """ + Accepts when given that the value of the partucular issue is in the + calculated acceptable range. + """ + issue_range = self.calculate_acceptable_range(issue) + if value not in self.value_to_numeric: + self.map_value_to_numeric(value) + + low = issue_range[0] + high = issue_range[1] + + return low < self.value_to_numeric[value] < high + + def calculate_acceptable_range(self, issue): + """ + Calculate ranges by taking a mode and variance into account. + """ + numerical_issue = self.issue_to_numeric[issue] + issue_mode = self.opp_profile[numerical_issue][0] + issue_var = self.opp_profile[numerical_issue][1] + + # Calculating the ranges + low = issue_mode - issue_var + high = issue_mode + issue_var + + return low, high + + ##################################################################################### + ############################## HELPER FUNCTIONS ##################################### + ##################################################################################### + + def update_opponent_issues(self): + """ + Keep track of frequencies of the values in bids received by + the opponents over period of time. + """ + + recentIssues = self._last_received_bid.getIssues() + recentIssuesValues = self._last_received_bid.getIssueValues() + + for issue in recentIssues: + if issue in self.opponent_issues: + if recentIssuesValues[issue] in self.opponent_issues[issue]: + self.opponent_issues[issue] = self.opponent_issues[issue][recentIssuesValues[issue]] + 1 + else: + self.opponent_issues[issue][recentIssuesValues[issue]] = 1 + else: + self.opponent_issues[issue][recentIssuesValues[issue]] = 1 + + def update_history_opp_issues(self): + """ + Updates history of opponent's issues + """ + recentIssuesValues = self._last_received_bid.getIssueValues() + self.opp_history_bids.append(recentIssuesValues) + + def always_best_bid_init(self) -> Bid: + """ + Returns the best bid + """ + if(not self.calculated_bid): + domain = self._profile.getProfile().getDomain() + all_bids = AllBidsList(domain) + profile = self._profile.getProfile() + + best_utility = 0.0 + + for x in all_bids: + curr_utility = profile.getUtility(x) + if(best_utility < curr_utility): + bid = x + best_utility = curr_utility + + self.calculated_bid = True + self.best_bid = bid + + return self.best_bid + else: + return self.best_bid + + def sort_high_bids(self): + """ + Sorting bids based on the utility values + """ + temp_tuple_bid = [] + if(not self.calculated_bid): + domain = self._profile.getProfile().getDomain() + all_bids = AllBidsList(domain) + profile = self._profile.getProfile() + + for x in all_bids: + temp_tuple_bid.append((profile.getUtility(x), x)) + + temp_tuple_bid = sorted( + temp_tuple_bid, key=lambda x: x[0], reverse=True) + + self.calculated_bid = True + self.sorted_bid = [bid[1] for bid in temp_tuple_bid] + + def map_issues_to_numeric_and_initialize(self, issue): + """ + Map issues which are represented in String to numeric values, + furthermore it initializes issue-history pair in "bid_history" dict. + """ + self.issue_to_numeric[issue] = self.idx_issue + self.bid_history[self.idx_issue] = [] + self.idx_issue = self.idx_issue + 1 + + def map_value_to_numeric(self, value): + """ + Map values which are represented in String to numeric values. + """ + self.value_to_numeric[value] = self.idx_value + self.idx_value = self.idx_value + 1 + + def calculate_avg_util(self): + profile = self._profile.getProfile() + util_avg = 0 + + for bid in self.sorted_bid: + util_avg += profile.getUtility(bid) + + return util_avg / len(self.sorted_bid) diff --git a/agents_test/agent68/agent68.py b/agents_test/agent68/agent68.py new file mode 100644 index 00000000..993da77d --- /dev/null +++ b/agents_test/agent68/agent68.py @@ -0,0 +1,189 @@ +import logging +import time + +from typing import cast + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressRounds import ProgressRounds +from geniusweb.opponentmodel.FrequencyOpponentModel import FrequencyOpponentModel +from tudelft_utilities_logging.Reporter import Reporter + +# from main.bidding.bidding import Bidding +# from Group68_NegotiationAssignment_Agent.bidding.bidding import Bidding +from .bidding.bidding import Bidding + +from time import time as clock + + +class Agent68(DefaultParty): + """ + Template agent that offers random bids until a bid with sufficient utility is offered. + """ + PHASE_ONE_ROUNDS = (0, 10) # Reconnaissance + PHASE_TWO_ROUNDS = (11, 70) # Main negotiation + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + self._progress = None + self.getReporter().log(logging.INFO, "party is initialized") + self._profile = None + self._last_received_bid: Bid = None + + # New attributes + self._opponent = FrequencyOpponentModel.create() + self._freqDict = {} + self._bidding: Bidding = Bidding() + self._selfCurrBid: Bid = None + + self._e1 = 0.3 + self._e2 = 0.3 + self._e3 = 0.1 + + def notifyChange(self, info: Inform): + """This is the entry point of all interaction with your agent after is has been initialised. + + Args: + info (Inform): Contains either a request for action or information. + """ + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(info, Settings): + self._settings: Settings = cast(Settings, info) + self._me = self._settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self._progress: ProgressRounds = self._settings.getProgress() + + # the profile contains the preferences of the agent over the domain + self._profile = ProfileConnectionFactory.create( + info.getProfile().getURI(), self.getReporter() + ) + + self._opponent = self._opponent.With(self._profile.getProfile().getDomain(), None) + + self._getParams() + self._bidding.initBidding(cast(Settings, info), self.getReporter()) + + # ActionDone is an action send by an opponent (an offer or an accept) + elif isinstance(info, ActionDone): + action: Action = cast(ActionDone, info).getAction() + + # if it is an offer, set the last received bid + if isinstance(action, Offer): + + offer: Offer = cast(Offer, action) + # print(self._profile.getProfile().getReservationBid()) + if offer.getActor() != self._settings.getID(): + opp_bid = cast(Offer, action).getBid() + self._last_received_bid = opp_bid + self._opponent = self._opponent.WithAction(action, self._progress) + self._bidding.updateOpponentUtility(self._opponent.getUtility) + opponent_utility = self._opponent.getUtility(opp_bid) + # print("Estimated opponent utility: " + str(opponent_utility)) + self._bidding.receivedBid(opp_bid) + + # YourTurn notifies you that it is your turn to act + elif isinstance(info, YourTurn): + action = self._myTurn() + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + self.getConnection().send(action) + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(info, Finished): + # terminate the agent MUST BE CALLED + self.terminate() + else: + self.getReporter().log( + logging.WARNING, "Ignoring unknown info " + str(info) + ) + self._updateRound(info) + + # lets the geniusweb system know what settings this agent can handle + # leave it as it is for this competition + def getCapabilities(self) -> Capabilities: + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + # terminates the agent and its connections + # leave it as it is for this competition + def terminate(self): + + super().terminate() + if self._profile is not None: + self._profile.close() + self._profile = None + + + + def _getParams(self): + params = self._settings.getParameters() + + self._e1 = params.getDouble("e1", 0.3, 0.0, 2.0) + self._e2 = params.getDouble("e2", 0.3, 0.0, 2.0) + self._e3 = params.getDouble("e3", 0.1, 0.0, 2.0) + + + + def _updateRound(self, info: Inform): + """ + Update {@link #progress}, depending on the protocol and last received + {@link Inform} + + @param info the received info. + """ + if self._settings == None: # not yet initialized + return + + if not isinstance(info, YourTurn): + return + + # if we get here, round must be increased. + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + self._bidding.updateProgress(self._progress) + + # give a description of your agent + def getDescription(self) -> str: + return "Agent68" + + # execute a turn + def _myTurn(self): + self._bidding._updateUtilSpace() + self._progress.get(round(clock() * 1000)) + # check if the last received offer if the opponent is good enough + #TODO try changing params and integrating with acceptance strat. Try filtered combinations + if self._progress.get(round(clock() * 1000)) < 0.4: + self._bidding.setE(self._e1) + elif self._progress.get(round(clock() * 1000)) < 0.8: + self._bidding.setE(self._e2) + else: + self._bidding.setE(self._e3) + if self._bidding._isGood(self._last_received_bid): + # if so, accept the offer + action = Accept(self._me, self._last_received_bid) + else: + # if not, find a bid to propose as counter offer + bid = self._bidding.makeBid(self._opponent) + action = Offer(self._me, bid) + opponent_utility = self._opponent.getUtility(bid) + + # send the actionp + return action + diff --git a/agents_test/agent68/bidding/bidding.py b/agents_test/agent68/bidding/bidding.py new file mode 100644 index 00000000..3b96e884 --- /dev/null +++ b/agents_test/agent68/bidding/bidding.py @@ -0,0 +1,296 @@ +from multiprocessing import Value + +from geniusweb.opponentmodel.FrequencyOpponentModel import FrequencyOpponentModel +from geniusweb.profile.Profile import Profile +from geniusweb.issuevalue.Bid import Bid +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from tudelft.utilities.immutablelist.ImmutableList import ImmutableList +from tudelft.utilities.immutablelist.JoinedList import JoinedList +from geniusweb.inform.Settings import Settings +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from time import sleep, time as clock +from decimal import Decimal + +from random import randint, random +from typing import cast, Dict, List, Set, Collection + +# from main.bidding.extended_util_space import ExtendedUtilSpace +# from Group68_NegotiationAssignment_Agent.Group68_NegotiationAssignment_Agent.bidding.extended_util_space import ExtendedUtilSpace +from .extended_util_space import ExtendedUtilSpace +from geniusweb.progress.Progress import Progress +import numpy as np + + +class Bidding(): + + def __init__(self) -> None: + self._profileint: ProfileInterface = None # type:ignore + self._utilspace: LinearAdditive = None # type:ignore + self._me: PartyId = None # type:ignore + self._progress: Progress = None # type:ignore + self._extendedspace: ExtendedUtilSpace = None # type:ignore + self._e: float = 0.002 + self._settings: Settings = None # type:ignore + + self._best_backup_bid: Bid = None + self._opp_utility = None + self._bids_to_make_stack: [Bid] = [] + self._resBidValue = Decimal('0.0') + self._recentBidScore = [Decimal('0.0') for _ in range(5)] + self._utilW = 0.75 + self._leniencyW = 0.25 + self._leniencyBase = 0.35 + + def initBidding(self, info: Settings, reporter): + self._settings = info + self._me = self._settings.getID() + self._progress = self._settings.getProgress() + self._profileint = ProfileConnectionFactory.create( + self._settings.getProfile().getURI(), reporter + ) + resBid = self._profileint.getProfile().getReservationBid() + params = self._settings.getParameters() + + self._utilW = params.getDouble("utilWeight", 0.8, 0.0, 1.0) + self._leniencyW = params.getDouble("leniencyWeight", 0.2, 0.0, 1.0) + self._leniencyBase = params.getDouble("leniencyBase", 0.4, 0.0, 1.0) + + if (resBid): + self._resBidValue = self._profileint.getProfile().getUtility(resBid) + else: + self._resBidValue = 0.0 + + def updateProgress(self, progress): + self._progress = progress + + def getE(self) -> float: + """ + @return the E value that controls the party's behaviour. Depending on the + value of e, extreme sets show clearly different patterns of + behaviour [1]: + + 1. Boulware: For this strategy e < 1 and the initial offer is + maintained till time is almost exhausted, when the agent concedes + up to its reservation value. + + 2. Conceder: For this strategy e > 1 and the agent goes to its + reservation value very quickly. + + 3. When e = 1, the price is increased linearly. + + 4. When e = 0, the agent plays hardball. + """ + return self._e + + def receivedBid(self, bid: Bid): + self._recentBidScore.sort() + + profile = cast(LinearAdditive, self._profileint.getProfile()) + + bidUtility = profile.getUtility(bid) + + if (self._recentBidScore[0] < bidUtility): + self._recentBidScore[0] = bidUtility + + self.updateBestBackupBid(bid, bidUtility) + + def leniencyThresh(self): + """ + Modify acceptance threshold based on the recent offers from the opponent. + Good offers -> Lower threshold + Bad offers -> Higher threshold + + Returns: + _type_: _description_ + """ + avgRecentBids = float(sum(self._recentBidScore) / len(self._recentBidScore)) + + return np.clip((1 - avgRecentBids) + self._leniencyBase, 0, 1) + + def updateBestBackupBid(self, bid: Bid, bidScore): + """Updates best bid proposed by the opponent so far. + In order to have a backup bid in the final few rounds. + + Args: + bid (Bid): _description_ + """ + if self._best_backup_bid is None: + self._best_backup_bid = bid + return + + profile = cast(LinearAdditive, self._profileint.getProfile()) + if profile.getUtility(self._best_backup_bid) < bidScore: + self._best_backup_bid = bid + + def updateOpponentUtility(self, oppUtility): + """Passes the estimated opponent utility function to use during sorting the list of bids gotten + + Args: opp_utility_func: estimated opponent utility function from frequency analysis + """ + self._opp_utility = oppUtility + + def setProfile(self, profile: Profile): + self._profileint = profile + + def setE(self, E: float): + self._e = E + + def _updateUtilSpace(self) -> LinearAdditive: # throws IOException + newutilspace = self._profileint.getProfile() + if not newutilspace == self._utilspace: + self._utilspace = cast(LinearAdditive, newutilspace) + self._extendedspace = ExtendedUtilSpace(self._utilspace) + return self._utilspace + + """Method to select bids to make. Works with stateful stack- _bids_to_make_stack. + Makes 2 bids for every utility goal when the stack is empty and pops the stack if it is not. + + Args: opponent: estimated frequency model of the opponent used to sort and select bids to make + """ + + def makeBid(self, opponent: FrequencyOpponentModel): + freqs: Dict[str, Dict[Value, int]] = opponent._bidFrequencies + + time = self._progress.get(round(clock() * 1000)) + + # if this is the first round of negotiation where the max and second max frequency issue-value are uninitialized. + # return the max bid from self._extendedspace + if len(freqs) == 0 or time <= 0.02: + options = self._extendedspace.getBids(self._extendedspace.getMax()) + outBid = options.get(randint(0, options.size() - 1)) + + return outBid + + profile = cast(LinearAdditive, self._profileint.getProfile()) + # Following find largest and second-largest frequency issue-value pairs in one iteration through table + max_freq = -1 + max_issue: str = None + max_value: Value = None + second_max: str = -1 + second_max_issue: Value = None + second_max_value = None + for issue in freqs: + for value in freqs[issue]: + frequency = freqs[issue][value] + if frequency >= max_freq: + max_issue = issue + max_value = value + max_freq = frequency + elif frequency >= second_max: + second_max = frequency + second_max_value = value + second_max_issue = issue + + # Get our bids + # If we don't have any bids pre-calculated, calculate them + if len(self._bids_to_make_stack) == 0: + time = self._progress.get(round(clock() * 1000)) + leniencyValue = self.leniencyThresh() + utilityGoal = float(self._getUtilityGoal( + time, + self.getE(), + self._extendedspace.getMin(), + self._extendedspace.getMax(), + )) + + utilityGoal = Decimal(self._utilW * utilityGoal + self._leniencyW * leniencyValue) + + options: ImmutableList[Bid] = self._extendedspace.getBids(utilityGoal) + + if options.size() == 0: + # No good bid found - return bid with max utility + options = self._extendedspace.getBids(self._extendedspace.getMax()) + outBid = options.get(randint(0, options.size() - 1)) + return outBid + + # filter based on the frequencies found above + filtered = list(filter(lambda bid: bid._issuevalues[max_issue] == max_value, options)) + top = [] + if len(filtered) == 0 or self._progress.get(round(clock() * 1000)) < 0.25: + util_predictor = lambda bid: opponent.getUtility(bid) + + top = sorted(self._joinedSubList(options, 0, min(10, options.size())), key=util_predictor, reverse=True) + # if top[0] is smaller than best bid so far + if profile.getUtility(self._best_backup_bid) >= profile.getUtility(top[0]): + self._bids_to_make_stack.append(self._best_backup_bid) + else: + self._bids_to_make_stack.append(top[0]) + else: + top = filtered[:min(10, len(filtered))] + util_predictor = lambda bid: opponent.getUtility(bid) + top = sorted(top, key=util_predictor, reverse=True) + + # Proposing the bid from the opponent with the best utility so far + if len(top) >= 2: + # push two bids on stack if 2 were found else just the one + if profile.getUtility(self._best_backup_bid) >= profile.getUtility(top[1]): + self._bids_to_make_stack.append(self._best_backup_bid) + self._bids_to_make_stack.append(top[0]) + # Return the next best calculated bid + return self._bids_to_make_stack.pop() + + def _pickBestOpponentUtility(self, bidlist: ImmutableList[Bid]) -> List[Bid]: + + outBids: List[Bid] = sorted(bidlist, key=self._opp_utility, reverse=True) + + return outBids[0:2] + + def _getUtilityGoal( + self, t: float, e: float, minUtil: Decimal, maxUtil: Decimal + ) -> Decimal: + """ + @param t the time in [0,1] where 0 means start of nego and 1 the + end of nego (absolute time/round limit) + @param e the e value that determinses how fast the party makes + concessions with time. Typically around 1. 0 means no + concession, 1 linear concession, >1 faster than linear + concession. + @param minUtil the minimum utility possible in our profile + @param maxUtil the maximum utility possible in our profile + @return the utility goal for this time and e value + """ + + # Minimum util value cannot be less than reservation bid value. + if minUtil < self._resBidValue: + minUtil = self._resBidValue + + ft1 = Decimal(1) + if e != 0: + ft1 = round(Decimal(1 - t * e), 6) # defaults ROUND_HALF_UP + return max(min((minUtil + (maxUtil - minUtil) * ft1), maxUtil), minUtil) + + def _isGood(self, bid: Bid) -> bool: + """ + @param bid the bid to check + @return true iff bid is good for us according to three criterias mentioned in the report. + """ + if bid == None or self._profileint == None: + return False + profile = cast(LinearAdditive, self._profileint.getProfile()) + + time = self._progress.get(round(clock() * 1000)) + + #Accept final round + if (time >= 0.99): + return True + + leniency = self.leniencyThresh() + bidUtil = profile.getUtility(bid) + utilGoal = float(self._getUtilityGoal( + time, + self.getE(), + self._extendedspace.getMin(), + self._extendedspace.getMax(), + )) + + return bidUtil >= self._resBidValue \ + and bidUtil >= (self._utilW * utilGoal + self._leniencyW * leniency) \ + and bidUtil >= profile.getUtility(self._best_backup_bid) + + def _joinedSubList(self, list: JoinedList, start: int, end: int): + res = [] + for i in range(start, end): + res.append(list.get(i)) + return res diff --git a/agents_test/agent68/bidding/extended_util_space.py b/agents_test/agent68/bidding/extended_util_space.py new file mode 100644 index 00000000..abfec098 --- /dev/null +++ b/agents_test/agent68/bidding/extended_util_space.py @@ -0,0 +1,79 @@ +from geniusweb.bidspace.BidsWithUtility import BidsWithUtility +from geniusweb.bidspace.Interval import Interval +from geniusweb.bidspace.IssueInfo import IssueInfo +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Value import Value +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from tudelft.utilities.immutablelist.ImmutableList import ImmutableList +from decimal import Decimal +from typing import List + + +class ExtendedUtilSpace: + """ + Inner class for TimeDependentParty, made public for testing purposes. This + class may change in the future, use at your own risk. + """ + + def __init__(self, space: LinearAdditive): + self._utilspace = space + self._bidutils = BidsWithUtility.create(self._utilspace) + self._computeMinMax() + self._tolerance = self._computeTolerance() + + def _computeMinMax(self): + """ + Computes the fields minutil and maxUtil. +

+ TODO this is simplistic, very expensive method and may cause us to run + out of time on large domains. +

+ Assumes that utilspace and bidutils have been set properly. + """ + range = self._bidutils.getRange() + self._minUtil = range.getMin() + self._maxUtil = range.getMax() + + rvbid = self._utilspace.getReservationBid() + if rvbid != None: + rv = self._utilspace.getUtility(rvbid) + if rv > self._minUtil: + self._minUtil = rv + + def _computeTolerance(self) -> Decimal: + """ + Tolerance is the Interval we need when searching bids. When we are close + to the maximum utility, this value has to be the distance between the + best and one-but-best utility. + + @return the minimum tolerance required, which is the minimum difference + between the weighted utility of the best and one-but-best issue + value. + """ + tolerance = Decimal(1) + for iss in self._bidutils.getInfo(): + if iss.getValues().size() > 1: + # we have at least 2 values. + values: List[Decimal] = [] + for val in iss.getValues(): + values.append(iss.getWeightedUtil(val)) + values.sort() + values.reverse() + tolerance = min(tolerance, values[0] - values[1]) + return tolerance + + def getMin(self) -> Decimal: + return self._minUtil + + def getMax(self) -> Decimal: + return self._maxUtil + + def getBids(self, utilityGoal: Decimal) -> ImmutableList[Bid]: + """ + @param utilityGoal the requested utility + @return bids with utility inside [utilitygoal-{@link #tolerance}, + utilitygoal] + """ + return self._bidutils.getBids( + Interval(utilityGoal - self._tolerance, utilityGoal) + ) diff --git a/agents_test/agent68/opponent/opponent.py b/agents_test/agent68/opponent/opponent.py new file mode 100644 index 00000000..3088a00a --- /dev/null +++ b/agents_test/agent68/opponent/opponent.py @@ -0,0 +1,78 @@ +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain + +class Opponent: + + def __init__(self): + self._domain: Domain = None + self._freqDict: dict = {} + self._allBids = [] + self._lastBid: Bid = None + self._firstBid: Bid = None + self._changed_issues = set() + # self._currentBid: Bid = None + + def init_domain(self, domain: Domain): + """ + Initialize the domain. + + :param domain: The domain. + """ + self._domain = domain + initValue = 1 / len(self._domain.getIssues()) + for issue in domain.getIssues(): + self._freqDict[issue] = initValue + + def log_bid(self, bid: Bid): + """ + Main method of the opponent class + it handles logging the bids the opponent made + for learning purposes. + + :param bid: The bid that the agent received. + """ + if self._firstBid is None: + self._firstBid = bid + self._update_freq_dict(bid, 0.1) + self._allBids.append(bid) + self._lastBid = bid + + def get_issue_weight(self, issue: str) -> float: + return self._freqDict[issue] + + def get_value_weight(self, issue: str, value: str) -> float: + return 1 / len(self._domain.getValues(issue)) + + def get_utility(self, bid: Bid) -> float: + """ + Given a bid return the predicted utility + + :param bid: The bid to calculate utility value on. + """ + pass + + def _update_freq_dict(self, received_bid: Bid, step: float): + if self._lastBid is None or received_bid is None: + return + + for issue in received_bid.getIssues(): + # Might fail if we receive partial bid. + if issue not in self._changed_issues and received_bid.getValue(issue) == self._firstBid.getValue(issue): + self._freqDict[issue] += step + else: + self._changed_issues.add(issue) + + print("=========") + print("Last bid: " + str(self._lastBid)) + print("Curr bid: " + str(received_bid)) + print("Before: " + str(self._freqDict)) + self._freqDict = self.normalize(self._freqDict, 1) + print("After: " + str(self._freqDict)) + print("=========") + + def normalize(self, d: dict, target=1.0): + raw = sum(d.values()) + factor = target / raw + # d.items() + return {key: value * factor for key, value in d.items()} + diff --git a/agents_test/agent68/utils/PlotTournament.py b/agents_test/agent68/utils/PlotTournament.py new file mode 100644 index 00000000..5addde2a --- /dev/null +++ b/agents_test/agent68/utils/PlotTournament.py @@ -0,0 +1,93 @@ +import os + +import plotly.graph_objects as go +import numpy as np +from collections import defaultdict + +class PlotTournament(): + + def __init__(self, results_summaries, my_agent): + self.utilities = defaultdict(list) + self.opponent_utilities = defaultdict(list) + self.nash_products = defaultdict(list) + self.social_welfares = defaultdict(list) + self.results_summaries = results_summaries + self.my_agent = my_agent + + def update_tournament_results(self): + for match in self.results_summaries: + # only interested in the matches where our agent appears. + if self.my_agent in match.values(): + agent1 = None + util1 = None + agent2 = None + util2 = None + for key in match.keys(): + if key.startswith("agent_"): + if agent1 == None: + agent1 = match[key] + else: + agent2 = match[key] + if key.startswith("utility_"): + if util1 == None: + util1 = match[key] + else: + util2 = match[key] + + if agent1 != self.my_agent: + self.utilities[agent1].append(util2) + self.nash_products[agent1].append(match["nash_product"]) + self.social_welfares[agent1].append(match["social_welfare"]) + + if agent1 == self.my_agent: + self.opponent_utilities[agent2].append(util2) + + if agent2 != self.my_agent: + self.utilities[agent2].append(util1) + self.nash_products[agent2].append(match["nash_product"]) + self.social_welfares[agent2].append(match["social_welfare"]) + + if agent2 == self.my_agent: + self.opponent_utilities[agent1].append(util1) + + + def plot_tournament_utilities(self, plot_file): + self.update_tournament_results() + + x_data = list(self.utilities.keys()) + + trace1 = go.Bar( + x = x_data, + y = [np.mean(value) for value in self.utilities.values()], + name = self.my_agent + " Utility" + ) + + trace2 = go.Bar( + x = x_data, + y = [np.mean(value) for value in self.nash_products.values()], + name = "Nash Product" + ) + + trace3 = go.Bar( + x = x_data, + y = [np.mean(value) for value in self.social_welfares.values()], + name = "Social Welfare" + ) + + trace4 = go.Bar( + x = [agent for agent in self.opponent_utilities.keys()], + y = [np.mean(value) for value in self.opponent_utilities.values()], + name = "Opponent Utility" + ) + + data = [trace1, trace4, trace2, trace3] + + layout = go.Layout(barmode = 'group') + fig = go.Figure(data = data, layout = layout) + + title = "Average performance of " + self.my_agent + " against " \ + "other agents" + fig.update_layout(title_text=title, title_x=0.5) + fig.update_yaxes(title_text="Average Score", ticks="outside") + + fig.write_html(f"{os.path.splitext(plot_file)[0]}.html") \ No newline at end of file diff --git a/agents_test/agent68/utils/grid_search.py b/agents_test/agent68/utils/grid_search.py new file mode 100644 index 00000000..4e76103b --- /dev/null +++ b/agents_test/agent68/utils/grid_search.py @@ -0,0 +1,81 @@ +import numpy as np +import os + +from utils.runners import run_tournament +from utils.PlotTournament import PlotTournament + + +if not os.path.exists("results"): + os.mkdir("results") + +class TournamentInfo(PlotTournament): + def __init__(self, results_summaries, my_agent): + super().__init__(results_summaries, my_agent) + + def getTournamentInfo(self): + self.update_tournament_results() + + utilAvgs = [np.mean(value) for value in self.utilities.values()] + + nashAvgs = [np.mean(value) for value in self.nash_products.values()] + + socialAvgs = [np.mean(value) for value in self.social_welfares.values()] + + return (np.average(utilAvgs), np.average(nashAvgs), np.average(socialAvgs)) + +def scoringFunction(utilScore, nashProduct, socialWelfare): + #Normalize socialWelfare [0,2] and add more weight to utilScore? + #Higher score == better + return ((1.5*utilScore) + nashProduct + (socialWelfare/2))/(1.5 + 1 + 1) + +e1_min = 0.1 +e1_max = 0.6 +e2_min = 0.1 +e2_max = 0.6 +e3_min = 0.1 +e3_max = 0.6 +utilGoalW_min = 0.4 +utilGoalW_max = 1.0 +leniBaseW_min = 0.0 +leniBaseW_max = 0.5 +step = 0.5 + +# with open("../results/gridSearch.csv", "w") as f: +# f.write("") +# f.close() +for e1 in np.arange(e1_min, e1_max, step): + for e2 in np.arange(e2_min, e2_max, step): + for e3 in np.arange(e3_min, e3_max, step): + for utilGoal in np.arange(utilGoalW_min, utilGoalW_max, step): + for leniBase in np.arange(leniBaseW_min, leniBaseW_max, step): + tournament_settings = { + "agents": [ + # "agents.boulware_agent.boulware_agent.BoulwareAgent", + "agents.conceder_agent.conceder_agent.ConcederAgent", + # "agents.linear_agent.linear_agent.LinearAgent", + # "agents.random_agent.random_agent.RandomAgent", + # "agents.template_agent.template_agent.TemplateAgent", + "main.threephase_agent.threephase_agent.ThreePhaseAgent", + ], + "profile_sets": [ + ["domains/domain00/profileA.json", "domains/domain00/profileB.json"], + # ["domains/domain01/profileA.json", "domains/domain01/profileB.json"], + ], + "deadline_rounds": 200, + "parameters": {"e1": e1, "e2":e2, "e3":e3, "utilWeight" : utilGoal, "leniencyWeight" : (1-utilGoal), "leniencyBase" : leniBase}, + } + # run a session and obtain results in dictionaries + print("Touring\n", flush=True) + tournament, results_summaries = run_tournament(tournament_settings) + + print("Calcing\n", flush=True) + + my_agent = "ThreePhaseAgent" + tour = TournamentInfo(results_summaries, my_agent) + util, nash, social = tour.getTournamentInfo() + score = scoringFunction(util, nash, social) + params = f'{e1},{e2},{e3},{utilGoal},{1-utilGoal},{leniBase}' + print("Writing\n", flush=True) + with open("results/gridSearch.csv", "a") as f: + f.write("{},{}\n".format(params, score)) + diff --git a/agents_test/agent68/utils/plot_pareto.py b/agents_test/agent68/utils/plot_pareto.py new file mode 100644 index 00000000..bd6926fd --- /dev/null +++ b/agents_test/agent68/utils/plot_pareto.py @@ -0,0 +1,146 @@ +import os +from collections import defaultdict +from typing import List, cast, Set + +import plotly.graph_objects as go +from geniusweb.bidspace.pareto.GenericPareto import GenericPareto +from geniusweb.bidspace.pareto.ParetoLinearAdditive import ParetoLinearAdditive +from geniusweb.issuevalue.Bid import Bid +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from geniusweb.profileconnection.ProfileConnectionFactory import ProfileConnectionFactory +from geniusweb.profileconnection.ProfileInterface import ProfileInterface +from uri.uri import URI + + +def compute_pareto_frontier(settings_profiles: List[str]): + profiles = dict() + for profile_url in [f"file:{x}" for x in settings_profiles]: + profileInt: ProfileInterface = ProfileConnectionFactory.create( + URI(profile_url), DefaultParty.getReporter) + profile: LinearAdditive = cast(LinearAdditive, profileInt.getProfile()) + profiles[profile_url] = profile + + pareto: ParetoLinearAdditive = ParetoLinearAdditive(list(profiles.values())) + pareto_points: Set[Bid] = pareto.getPoints() + + pareto_frontier = dict() + for pareto_bid in pareto_points: + pareto_frontier[pareto_bid] = dict() + for profile_name, profile in profiles.items(): + pareto_frontier[pareto_bid][profile_name] = float(profile.getUtility(pareto_bid)) + + return pareto_frontier + +def plot_pareto(results_trace: dict, pareto_frontier: dict, plot_file: str): + utilities = defaultdict(lambda: {"x": [], "y": [], "bids": []}) + profiles = results_trace["connections"] + x_axis = profiles[0] + x_label = "_".join(x_axis.split("_")[-2:]) + y_axis = profiles[1] + y_label = "_".join(y_axis.split("_")[-2:]) + + accept = {"x": [], "y": [], "bids": []} + for action in results_trace["actions"]: + if "Offer" in action: + offer = action["Offer"] + actor = offer["actor"] + for agent, util in offer["utilities"].items(): + if agent == x_axis: + utilities[actor]["x"].append(util) + else: + utilities[actor]["y"].append(util) + + utilities[actor]["bids"].append(offer["bid"]["issuevalues"]) + + elif "Accept" in action: + offer = action["Accept"] + actor = offer["actor"] + for agent, util in offer["utilities"].items(): + if agent == x_axis: + accept["x"].append(util) + else: + accept["y"].append(util) + + accept["bids"].append(offer["bid"]["issuevalues"]) + + fig = go.Figure() + fig.add_trace( + go.Scatter( + mode="markers", + x=accept["x"], + y=accept["y"], + name="agreement", + marker={"color": "green", "size": 15}, + hoverinfo="skip", + ) + ) + + color = {0: "red", 1: "blue"} + for i, (agent, utility) in enumerate(utilities.items()): + name = "_".join(agent.split("_")[-2:]) + + text = [] + for bid, util_x, util_y in zip(utility["bids"], utility["x"], utility["y"]): + text.append( + "
".join( + [x_label + f": {util_x:.3f}
"] + + [y_label + f": {util_y:.3f}
"] + + [f"{i}: {v}" for i, v in bid.items()] + ) + ) + + fig.add_trace( + go.Scatter( + x=utility["x"], + y=utility["y"], + marker={"color": color[i]}, + name=f"{name}", + hovertext = text, + hoverinfo = "text" + ) + ) + + x_axis_profile = None + y_axis_profile = None + party_profiles = results_trace["partyprofiles"] + for party_prof in party_profiles: + if x_axis in party_prof: + x_axis_profile = party_profiles[x_axis]["profile"] + elif y_axis in party_prof: + y_axis_profile = party_profiles[y_axis]["profile"] + + pareto_bids = {"x": [], "y": [], "bids": []} + for bid, utilities in pareto_frontier.items(): + for profile, util in utilities.items(): + if profile == x_axis_profile: + pareto_bids["x"].append(util) + elif profile == y_axis_profile: + pareto_bids["y"].append(util) + + pareto_bids["bids"].append(bid) + + fig.add_trace( + go.Scatter( + x=pareto_bids["x"], + y=pareto_bids["y"], + mode='markers', + name='pareto frontier point' + ) + ) + + fig.update_layout( + width=800, + height=800, + legend={ + "yanchor": "bottom", + "y": 1, + "xanchor": "left", + "x": 0, + }, + ) + + fig.update_layout(title_text='Negotiation traces', title_x=0.5) + fig.update_xaxes(title_text="Utility of " + x_label, ticks="outside") + fig.update_yaxes(title_text="Utility of " + y_label, ticks="outside") + fig.write_html(f"{os.path.splitext(plot_file)[0]}.html") diff --git a/agents_test/agentfish/.DS_Store b/agents_test/agentfish/.DS_Store new file mode 100644 index 00000000..a86968c7 Binary files /dev/null and b/agents_test/agentfish/.DS_Store differ diff --git a/agents_test/agentfish/__init__.py b/agents_test/agentfish/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents/template_agent/template_agent.py b/agents_test/agentfish/agentfish.py old mode 100755 new mode 100644 similarity index 68% rename from agents/template_agent/template_agent.py rename to agents_test/agentfish/agentfish.py index d777d767..4b7410c4 --- a/agents/template_agent/template_agent.py +++ b/agents_test/agentfish/agentfish.py @@ -1,22 +1,30 @@ import logging -from random import randint +from random import randint,uniform from time import time from typing import cast from geniusweb.actions.Accept import Accept from geniusweb.actions.Action import Action +from geniusweb.actions.LearningDone import LearningDone from geniusweb.actions.Offer import Offer from geniusweb.actions.PartyId import PartyId +from geniusweb.actions.Vote import Vote +from geniusweb.actions.Votes import Votes from geniusweb.bidspace.AllBidsList import AllBidsList from geniusweb.inform.ActionDone import ActionDone from geniusweb.inform.Finished import Finished from geniusweb.inform.Inform import Inform +from geniusweb.inform.OptIn import OptIn from geniusweb.inform.Settings import Settings +from geniusweb.inform.Voting import Voting from geniusweb.inform.YourTurn import YourTurn from geniusweb.issuevalue.Bid import Bid from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value +from geniusweb.issuevalue.ValueSet import ValueSet from geniusweb.party.Capabilities import Capabilities from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import ( LinearAdditiveUtilitySpace, ) @@ -29,8 +37,19 @@ from .utils.opponent_model import OpponentModel +from geniusweb.progress.ProgressRounds import ProgressRounds +from geniusweb.utils import val +from geniusweb.profileconnection.ProfileInterface import ProfileInterface +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from geniusweb.progress.Progress import Progress +from tudelft.utilities.immutablelist.ImmutableList import ImmutableList +from decimal import Decimal +import sys +from .extended_util_space import ExtendedUtilSpace +from tudelft_utilities_logging.Reporter import Reporter -class TemplateAgent(DefaultParty): + +class AgentFish(DefaultParty): """ Template of a Python geniusweb agent. """ @@ -52,6 +71,13 @@ def __init__(self): self.opponent_model: OpponentModel = None self.logger.log(logging.INFO, "party is initialized") + self.profileint: ProfileInterface = None # type:ignore + self.utilspace: LinearAdditive = None # type:ignore + self.extendedspace: ExtendedUtilSpace = None # type:ignore + self.e: float = 1.2 + self.lastvotes: Votes = None # type:ignore + self.opponent_max = 0.0 + def notifyChange(self, data: Inform): """MUST BE IMPLEMENTED This is the entry point of all interaction with your agent after is has been initialised. @@ -108,6 +134,10 @@ def notifyChange(self, data: Inform): else: self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data)) + def getE(self) -> float: + + return self.e + def getCapabilities(self) -> Capabilities: """MUST BE IMPLEMENTED Method to indicate to the protocol what the capabilities of this agent are. @@ -137,7 +167,7 @@ def getDescription(self) -> str: Returns: str: Agent description """ - return "Template agent for the ANL 2022 competition" + return "Foot in the Door agent for the ANL 2022 competition" def opponent_action(self, action): """Process an action that was received from the opponent. @@ -158,22 +188,75 @@ def opponent_action(self, action): # set bid as last received self.last_received_bid = bid + if self.opponent_max < self.profile.getUtility(bid): + self.opponent_max = self.profile.getUtility(bid) + def my_turn(self): """This method is called when it is our turn. It should decide upon an action to perform and send this action to the opponent. """ # check if the last received offer is good enough + self.updateUtilSpace() if self.accept_condition(self.last_received_bid): # if so, accept the offer action = Accept(self.me, self.last_received_bid) else: # if not, find a bid to propose as counter offer - bid = self.find_bid() + bid = self.makeBid() action = Offer(self.me, bid) # send the action self.send_action(action) + def updateUtilSpace(self) -> LinearAdditive: # throws IOException + newutilspace = self.profile + if not newutilspace == self.utilspace: + self.utilspace = cast(LinearAdditive, newutilspace) + self.extendedspace = ExtendedUtilSpace(self.utilspace) + return self.utilspace + + def makeBid(self) -> Bid: + """ + @return next possible bid with current target utility, or null if no such + bid. + """ + time_to_deadline = self.progress.get(time() * 1000) + + utilityGoal = self.getUtilityGoal( + time_to_deadline, + self.getE(), + self.extendedspace.getMin(), + self.extendedspace.getMax(), + ) + options: ImmutableList[Bid] = self.extendedspace.getBids(utilityGoal) + if options.size() == 0: + # if we can't find good bid, get max util bid.... + options = self.extendedspace.getBids(self.extendedspace.getMax()) + # pick a random one. + return options.get(randint(0, options.size() - 1)) + + def getUtilityGoal( + self, t: float, e: float, minUtil: Decimal, maxUtil: Decimal + ) -> Decimal: + """ + @param t the time in [0,1] where 0 means start of nego and 1 the + end of nego (absolute time/round limit) + @param e the e value that determinses how fast the party makes + concessions with time. Typically around 1. 0 means no + concession, 1 linear concession, >1 faster than linear + concession. + @param minUtil the minimum utility possible in our profile + @param maxUtil the maximum utility possible in our profile + @return the utility goal for this time and e value + """ + + ft1 = Decimal(1) + if e != 0: + ft1 = round(Decimal(1 - pow(t, 1 / e)), 6) # defaults ROUND_HALF_UP + myoffer = max(min((maxUtil - (maxUtil - minUtil) * ft1), maxUtil), minUtil) + return max(myoffer, self.opponent_max) + + def save_data(self): """This method is called after the negotiation is finished. It can be used to store data for learning capabilities. Note that no extensive calculations can be done within this method. @@ -196,28 +279,16 @@ def accept_condition(self, bid: Bid) -> bool: # very basic approach that accepts if the offer is valued above 0.7 and # 95% of the time towards the deadline has passed + maxAccept = 0.8 + minAccept = 0.6 + ft = round(Decimal(1 - progress), 6) # defaults ROUND_HALF_UP + acceptline = max(min((minAccept + (maxAccept - minAccept) * float(ft)), maxAccept), minAccept) conditions = [ - self.profile.getUtility(bid) > 0.8, + float(self.profile.getUtility(bid)) > acceptline, + progress > uniform(0.8, 0.95), progress > 0.95, ] - return all(conditions) - - def find_bid(self) -> Bid: - # compose a list of all possible bids - domain = self.profile.getDomain() - all_bids = AllBidsList(domain) - - best_bid_score = 0.0 - best_bid = None - - # take 500 attempts to find a bid according to a heuristic score - for _ in range(500): - bid = all_bids.get(randint(0, all_bids.size() - 1)) - bid_score = self.score_bid(bid) - if bid_score > best_bid_score: - best_bid_score, best_bid = bid_score, bid - - return best_bid + return all([any(conditions), progress > 0.1]) def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float: """Calculate heuristic score for a bid diff --git a/agents_test/agentfish/extended_util_space.py b/agents_test/agentfish/extended_util_space.py new file mode 100644 index 00000000..0ab817c6 --- /dev/null +++ b/agents_test/agentfish/extended_util_space.py @@ -0,0 +1,79 @@ +from geniusweb.bidspace.BidsWithUtility import BidsWithUtility +from geniusweb.bidspace.Interval import Interval +from geniusweb.bidspace.IssueInfo import IssueInfo +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Value import Value +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from tudelft.utilities.immutablelist.ImmutableList import ImmutableList +from decimal import Decimal +from typing import List + + +class ExtendedUtilSpace: + """ + Inner class for TimeDependentParty, made public for testing purposes. This + class may change in the future, use at your own risk. + """ + + def __init__(self, space: LinearAdditive): + self._utilspace = space + self._bidutils = BidsWithUtility.create(self._utilspace) + self._computeMinMax() + self._tolerance = self._computeTolerance() + + def _computeMinMax(self): + """ + Computes the fields minutil and maxUtil. +

+ TODO this is simplistic, very expensive method and may cause us to run + out of time on large domains. +

+ Assumes that utilspace and bidutils have been set properly. + """ + range = self._bidutils.getRange() + self._minUtil = Decimal("0.7")*range.getMax() + self._maxUtil = range.getMax() + + rvbid = self._utilspace.getReservationBid() + if rvbid != None: + rv = self._utilspace.getUtility(rvbid) + if rv > self._minUtil: + self._minUtil = rv + + def _computeTolerance(self) -> Decimal: + """ + Tolerance is the Interval we need when searching bids. When we are close + to the maximum utility, this value has to be the distance between the + best and one-but-best utility. + + @return the minimum tolerance required, which is the minimum difference + between the weighted utility of the best and one-but-best issue + value. + """ + tolerance = Decimal(1) + for iss in self._bidutils.getInfo(): + if iss.getValues().size() > 1: + # we have at least 2 values. + values: List[Decimal] = [] + for val in iss.getValues(): + values.append(iss.getWeightedUtil(val)) + values.sort() + values.reverse() + tolerance = min(tolerance, values[0] - values[1]) + return tolerance + + def getMin(self) -> Decimal: + return self._minUtil + + def getMax(self) -> Decimal: + return self._maxUtil + + def getBids(self, utilityGoal: Decimal) -> ImmutableList[Bid]: + """ + @param utilityGoal the requested utility + @return bids with utility inside [utilitygoal-{@link #tolerance}, + utilitygoal] + """ + return self._bidutils.getBids( + Interval(utilityGoal - self._tolerance, utilityGoal) + ) diff --git a/agents_test/agentfish/utils/__init__.py b/agents_test/agentfish/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents_test/agentfish/utils/opponent_model.py b/agents_test/agentfish/utils/opponent_model.py new file mode 100644 index 00000000..14d7456b --- /dev/null +++ b/agents_test/agentfish/utils/opponent_model.py @@ -0,0 +1,121 @@ +from collections import defaultdict + +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value + + +class OpponentModel: + def __init__(self, domain: Domain): + self.offers = [] + self.domain = domain + + self.issue_estimators = { + i: IssueEstimator(v) for i, v in domain.getIssuesValues().items() + } + + def update(self, bid: Bid): + # keep track of all bids received + self.offers.append(bid) + + # update all issue estimators with the value that is offered for that issue + for issue_id, issue_estimator in self.issue_estimators.items(): + issue_estimator.update(bid.getValue(issue_id)) + + def get_predicted_utility(self, bid: Bid): + if len(self.offers) == 0 or bid is None: + return 0 + + # initiate + total_issue_weight = 0.0 + value_utilities = [] + issue_weights = [] + + for issue_id, issue_estimator in self.issue_estimators.items(): + # get the value that is set for this issue in the bid + value: Value = bid.getValue(issue_id) + + # collect both the predicted weight for the issue and + # predicted utility of the value within this issue + value_utilities.append(issue_estimator.get_value_utility(value)) + issue_weights.append(issue_estimator.weight) + + total_issue_weight += issue_estimator.weight + + # normalise the issue weights such that the sum is 1.0 + if total_issue_weight == 0.0: + issue_weights = [1 / len(issue_weights) for _ in issue_weights] + else: + issue_weights = [iw / total_issue_weight for iw in issue_weights] + + # calculate predicted utility by multiplying all value utilities with their issue weight + predicted_utility = sum( + [iw * vu for iw, vu in zip(issue_weights, value_utilities)] + ) + + return predicted_utility + + +class IssueEstimator: + def __init__(self, value_set: DiscreteValueSet): + if not isinstance(value_set, DiscreteValueSet): + raise TypeError( + "This issue estimator only supports issues with discrete values" + ) + + self.bids_received = 0 + self.max_value_count = 0 + self.num_values = value_set.size() + self.value_trackers = defaultdict(ValueEstimator) + self.weight = 0 + + def update(self, value: Value): + self.bids_received += 1 + + # get the value tracker of the value that is offered + value_tracker = self.value_trackers[value] + + # register that this value was offered + value_tracker.update() + + # update the count of the most common offered value + self.max_value_count = max([value_tracker.count, self.max_value_count]) + + # update predicted issue weight + # the intuition here is that if the values of the receiverd offers spread out over all + # possible values, then this issue is likely not important to the opponent (weight == 0.0). + # If all received offers proposed the same value for this issue, + # then the predicted issue weight == 1.0 + equal_shares = self.bids_received / self.num_values + self.weight = (self.max_value_count - equal_shares) / ( + self.bids_received - equal_shares + ) + + # recalculate all value utilities + for value_tracker in self.value_trackers.values(): + value_tracker.recalculate_utility(self.max_value_count, self.weight) + + def get_value_utility(self, value: Value): + if value in self.value_trackers: + return self.value_trackers[value].utility + + return 0 + + +class ValueEstimator: + def __init__(self): + self.count = 0 + self.utility = 0 + + def update(self): + self.count += 1 + + def recalculate_utility(self, max_value_count: int, weight: float): + if weight < 1: + mod_value_count = ((self.count + 1) ** (1 - weight)) - 1 + mod_max_value_count = ((max_value_count + 1) ** (1 - weight)) - 1 + + self.utility = mod_value_count / mod_max_value_count + else: + self.utility = 1 diff --git a/agents_test/boulware_agent/boulware_agent.py b/agents_test/boulware_agent/boulware_agent.py new file mode 100644 index 00000000..87d346c2 --- /dev/null +++ b/agents_test/boulware_agent/boulware_agent.py @@ -0,0 +1,23 @@ +from agents.time_dependent_agent.time_dependent_agent import TimeDependentAgent +from tudelft_utilities_logging.Reporter import Reporter + + +class BoulwareAgent(TimeDependentAgent): + """ + A simple party that places random bids and accepts when it receives an offer + with sufficient utility. + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + + # Override + def getDescription(self) -> str: + return ( + "Boulware: reluctant to concede. Parameters minPower (default 1) " + + "and maxPower (default infinity) are used when voting" + ) + + # Override + def getE(self) -> float: + return 0.2 diff --git a/agents_test/charging_boul/charging_boul.py b/agents_test/charging_boul/charging_boul.py new file mode 100644 index 00000000..f490dc8e --- /dev/null +++ b/agents_test/charging_boul/charging_boul.py @@ -0,0 +1,246 @@ +from .extended_util_space import ExtendedUtilSpace +from .utils.opponent_model import OpponentModel +from decimal import Decimal +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from geniusweb.profileconnection.ProfileConnectionFactory import ProfileConnectionFactory +from geniusweb.profileconnection.ProfileInterface import ProfileInterface +from geniusweb.progress.Progress import Progress +from geniusweb.references.Parameters import Parameters +from json import dump, load +from random import randint +from statistics import mean +from time import time as clock +from tudelft.utilities.immutablelist.ImmutableList import ImmutableList +from tudelft_utilities_logging.Reporter import Reporter +import logging + + +class ChargingBoul(DefaultParty): + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + self.best_received_bid: Bid = None + self.best_received_util: Decimal = Decimal(0) + self.domain: Domain = None + self.e: float = 0.1 + self.extended_space: ExtendedUtilSpace = None + self.filepath: str = None + self.final_rounds: int = 90 + self.last_received_bid: Bid = None + self.last_received_util: Decimal = None + self.max_util = Decimal(1) + self.me: PartyId = None + self.min_util = Decimal(0.5) + self.opponent_model: OpponentModel = None + self.opponent_strategy: str = None + self.other: str = None + self.parameters: Parameters = None + self.profile_int: ProfileInterface = None + self.progress: Progress = None + self.received_bids: list = [] + self.received_utils: list = [] + self.settings: Settings = None + self.storage_dir: str = None + self.summary: dict = None + self.util_space: LinearAdditive = None + self.getReporter().log(logging.INFO, "party is initialized") + + def notifyChange(self, info: Inform): + """MUST BE IMPLEMENTED + This is the entry point of all interaction with your agent after is has been initialised. + How to handle the received data is based on its class type. + + Args: + info (Inform): Contains either a request for action or information. + """ + try: + if isinstance(info, Settings): + self.settings = info + self.me = self.settings.getID() + self.parameters = self.settings.getParameters() + self.profile_int = ProfileConnectionFactory.create( + self.settings.getProfile().getURI(), self.getReporter() + ) + self.progress = self.settings.getProgress() + self.storage_dir = self.parameters.get("storage_dir") + self.util_space = self.profile_int.getProfile() + self.domain = self.util_space.getDomain() + self.extended_space = ExtendedUtilSpace(self.util_space) + self.detect_strategy() + elif isinstance(info, ActionDone): + other_act: Action = info.getAction() + actor = other_act.getActor() + if actor != self.me: + self.other = str(actor).rsplit("_", 1)[0] + self.filepath = f"{self.storage_dir}/{self.other}.json" + if isinstance(other_act, Offer): + # create opponent model if it was not yet initialised + if self.opponent_model is None: + self.opponent_model = OpponentModel(self.domain) + self.last_received_bid = other_act.getBid() + self.last_received_util = self.util_space.getUtility(self.last_received_bid) + # update opponent model with bid + self.opponent_model.update(self.last_received_bid) + elif isinstance(info, YourTurn): + self.my_turn() + elif isinstance(info, Finished): + self.getReporter().log(logging.INFO, "Final outcome:" + str(info)) + self.terminate() + # stop this party and free resources. + except Exception as ex: + self.getReporter().log(logging.CRITICAL, "Failed to handle info", ex) + + def getCapabilities(self) -> Capabilities: + """MUST BE IMPLEMENTED + Method to indicate to the protocol what the capabilities of this agent are. + Leave it as is for the ANL 2022 competition + + Returns: + Capabilities: Capabilities representation class + """ + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + def getDescription(self) -> str: + """MUST BE IMPLEMENTED + Returns a description of your agent. 1 or 2 sentences. + + Returns: + str: Agent description + """ + return ( + "Increasingly random Boulwarish agent. Last second concessions based on opponent's strategy." + ) + + ##################### private support funcs ######################### + + def detect_strategy(self): + if self.filepath is not None: + with open(self.filepath, "w") as f: + self.summary = load(f) + if self.summary["ubi"] >= 5: + self.opponent_strategy = "boulware" + self.e = 0.2 * 2**(5 - self.summary["ubi"]) + elif self.summary["aui"] <= 2: + self.opponent_strategy = "hardline" + else: + self.opponent_strategy = "concede" + self.min_util = Decimal(0.4) + + def my_turn(self): + # Keep history of received bids and best alternative + if self.last_received_bid is not None: + self.received_bids.append(self.last_received_bid) + self.received_utils.append(self.last_received_util) + if self.last_received_util > self.best_received_util: + self.best_received_bid = self.last_received_bid + self.best_received_util = self.last_received_util + # Create new bid based on the point in the negotiation and opponent's strategy + t = self.progress.get(clock() * 1000) + if self.summary is not None and self.opponent_strategy == "boulware" and t > 1 - (1/2)**min(self.summary["ubi"], 10): + # If Boulware opponent is not going to concede much more, try to make a reasonable concession + bid = self.make_concession() + else: + bid = self.make_bid() + # Check if we've previously gotten a better bid already + if self.best_received_util >= self.util_space.getUtility(bid): + i = self.received_utils.index(self.best_received_util) + bid = self.received_bids.pop(i) + self.received_utils.pop(i) + # Find next bests + self.best_received_util = max(self.received_utils) + i = self.received_utils.index(self.best_received_util) + self.best_received_bid = self.received_bids[i] + # Take action + my_action: Action + if bid == None or ( + self.last_received_bid != None + and self.util_space.getUtility(self.last_received_bid) + >= self.util_space.getUtility(bid) + ): + # if bid==null we failed to suggest next bid. + my_action = Accept(self.me, self.last_received_bid) + else: + my_action = Offer(self.me, bid) + self.getConnection().send(my_action) + + def make_concession(self): + self.min_util = Decimal(0.3) + opponent_util = self.opponent_model.get_predicted_utility(self.best_received_util) + if self.best_received_util > self.min_util and opponent_util < 2*self.min_util: + bid = self.best_received_bid + else: + bid = self.make_bid() + return bid + + def make_bid(self) -> Bid: + time = self.progress.get(clock() * 1000) + utility_goal = self.get_utility_goal(time) + options: ImmutableList[Bid] = self.extended_space.getBids(utility_goal, time) + if options.size() == 0: + # if we can't find good bid, get max util bid.... + options = self.extended_space.getBids(self.max_util, time) + # pick a random one. + return options.get(randint(0, options.size() - 1)) + + def get_utility_goal(self, t: float) -> Decimal: + ft1 = Decimal(1) + if self.e != 0: + ft1 = round(Decimal(1 - pow(t, 1 / self.e)), 6) # defaults ROUND_HALF_UP + return max( + min((self.min_util + (self.max_util - self.min_util) * ft1), self.max_util), + self.min_util + ) + + def terminate(self): + self.save_data() + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self.profile_int != None: + self.profile_int.close() + self.profile_int = None + + def save_data(self): + ubi, aui = self.summarize_opponent() + with open(self.filepath, "w") as f: + dump({ + "ubi": ubi, + "aui": aui + }, f) + + def summarize_opponent(self): + # Detect how much the number of unique bids is increasing + unique_bid_index = 0 + s = round(len(self.received_bids)/2) + left = self.received_bids[:s] + right = self.received_bids[s:] + while len(set(left)) > 0 and len(set(right)) > 0 and len(set(left)) < len(set(right)): + unique_bid_index += 1 + s = round(len(right)/2) + left = right[:s] + right = right[s:] + # Detect how much average utility is increasing + avg_utility_index = 0 + s = round(len(self.received_utils)/2) + left = self.received_utils[:s] + right = self.received_utils[s:] + while len(set(left)) > 0 and len(set(right)) > 0 and mean(left) < mean(right): + avg_utility_index += 1 + s = round(len(right)/2) + left = right[:s] + right = right[s:] + return unique_bid_index, avg_utility_index diff --git a/agents_test/charging_boul/extended_util_space.py b/agents_test/charging_boul/extended_util_space.py new file mode 100644 index 00000000..82979487 --- /dev/null +++ b/agents_test/charging_boul/extended_util_space.py @@ -0,0 +1,32 @@ +from decimal import Decimal +from geniusweb.bidspace.BidsWithUtility import BidsWithUtility +from geniusweb.bidspace.Interval import Interval +from geniusweb.issuevalue.Bid import Bid +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from tudelft.utilities.immutablelist.ImmutableList import ImmutableList +from typing import List + + +class ExtendedUtilSpace: + def __init__(self, space: LinearAdditive): + self.util_space = space + self.bid_utils = BidsWithUtility.create(self.util_space) + self.tolerance = self.compute_tolerance() + + def compute_tolerance(self) -> Decimal: + tolerance = Decimal(1) + for iss in self.bid_utils.getInfo(): + if iss.getValues().size() > 1: + # we have at least 2 values. + values: List[Decimal] = [] + for val in iss.getValues(): + values.append(iss.getWeightedUtil(val)) + values.sort() + values.reverse() + tolerance = min(tolerance, values[0] - values[1]) + return tolerance + + def getBids(self, utilityGoal: Decimal, time: float) -> ImmutableList[Bid]: + return self.bid_utils.getBids( + Interval(utilityGoal - (Decimal(time)*3 + 1)*self.tolerance, utilityGoal + (Decimal(time)*3 + 1)*self.tolerance) + ) diff --git a/agents_test/charging_boul/utils/opponent_model.py b/agents_test/charging_boul/utils/opponent_model.py new file mode 100644 index 00000000..14d7456b --- /dev/null +++ b/agents_test/charging_boul/utils/opponent_model.py @@ -0,0 +1,121 @@ +from collections import defaultdict + +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value + + +class OpponentModel: + def __init__(self, domain: Domain): + self.offers = [] + self.domain = domain + + self.issue_estimators = { + i: IssueEstimator(v) for i, v in domain.getIssuesValues().items() + } + + def update(self, bid: Bid): + # keep track of all bids received + self.offers.append(bid) + + # update all issue estimators with the value that is offered for that issue + for issue_id, issue_estimator in self.issue_estimators.items(): + issue_estimator.update(bid.getValue(issue_id)) + + def get_predicted_utility(self, bid: Bid): + if len(self.offers) == 0 or bid is None: + return 0 + + # initiate + total_issue_weight = 0.0 + value_utilities = [] + issue_weights = [] + + for issue_id, issue_estimator in self.issue_estimators.items(): + # get the value that is set for this issue in the bid + value: Value = bid.getValue(issue_id) + + # collect both the predicted weight for the issue and + # predicted utility of the value within this issue + value_utilities.append(issue_estimator.get_value_utility(value)) + issue_weights.append(issue_estimator.weight) + + total_issue_weight += issue_estimator.weight + + # normalise the issue weights such that the sum is 1.0 + if total_issue_weight == 0.0: + issue_weights = [1 / len(issue_weights) for _ in issue_weights] + else: + issue_weights = [iw / total_issue_weight for iw in issue_weights] + + # calculate predicted utility by multiplying all value utilities with their issue weight + predicted_utility = sum( + [iw * vu for iw, vu in zip(issue_weights, value_utilities)] + ) + + return predicted_utility + + +class IssueEstimator: + def __init__(self, value_set: DiscreteValueSet): + if not isinstance(value_set, DiscreteValueSet): + raise TypeError( + "This issue estimator only supports issues with discrete values" + ) + + self.bids_received = 0 + self.max_value_count = 0 + self.num_values = value_set.size() + self.value_trackers = defaultdict(ValueEstimator) + self.weight = 0 + + def update(self, value: Value): + self.bids_received += 1 + + # get the value tracker of the value that is offered + value_tracker = self.value_trackers[value] + + # register that this value was offered + value_tracker.update() + + # update the count of the most common offered value + self.max_value_count = max([value_tracker.count, self.max_value_count]) + + # update predicted issue weight + # the intuition here is that if the values of the receiverd offers spread out over all + # possible values, then this issue is likely not important to the opponent (weight == 0.0). + # If all received offers proposed the same value for this issue, + # then the predicted issue weight == 1.0 + equal_shares = self.bids_received / self.num_values + self.weight = (self.max_value_count - equal_shares) / ( + self.bids_received - equal_shares + ) + + # recalculate all value utilities + for value_tracker in self.value_trackers.values(): + value_tracker.recalculate_utility(self.max_value_count, self.weight) + + def get_value_utility(self, value: Value): + if value in self.value_trackers: + return self.value_trackers[value].utility + + return 0 + + +class ValueEstimator: + def __init__(self): + self.count = 0 + self.utility = 0 + + def update(self): + self.count += 1 + + def recalculate_utility(self, max_value_count: int, weight: float): + if weight < 1: + mod_value_count = ((self.count + 1) ** (1 - weight)) - 1 + mod_max_value_count = ((max_value_count + 1) ** (1 - weight)) - 1 + + self.utility = mod_value_count / mod_max_value_count + else: + self.utility = 1 diff --git a/agents_test/conceder_agent/conceder_agent.py b/agents_test/conceder_agent/conceder_agent.py new file mode 100644 index 00000000..76ae6e58 --- /dev/null +++ b/agents_test/conceder_agent/conceder_agent.py @@ -0,0 +1,23 @@ +from agents.time_dependent_agent.time_dependent_agent import TimeDependentAgent +from tudelft_utilities_logging.Reporter import Reporter + + +class ConcederAgent(TimeDependentAgent): + """ + A simple party that places random bids and accepts when it receives an offer + with sufficient utility. + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + + # Override + def getDescription(self) -> str: + return ( + "Conceder: going to the reservation value very quickly. " + + "Parameters minPower (default 1) and maxPower (default infinity) are used when voting" + ) + + # Override + def getE(self) -> float: + return 2.0 diff --git a/agents_test/dreamteam109_agent/DreamTeam109 ANL2022 Agent Strategy.pdf b/agents_test/dreamteam109_agent/DreamTeam109 ANL2022 Agent Strategy.pdf new file mode 100644 index 00000000..674fe95f Binary files /dev/null and b/agents_test/dreamteam109_agent/DreamTeam109 ANL2022 Agent Strategy.pdf differ diff --git a/agents_test/dreamteam109_agent/__init__.py b/agents_test/dreamteam109_agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents_test/dreamteam109_agent/dreamteam109_agent.py b/agents_test/dreamteam109_agent/dreamteam109_agent.py new file mode 100644 index 00000000..c1c4b623 --- /dev/null +++ b/agents_test/dreamteam109_agent/dreamteam109_agent.py @@ -0,0 +1,392 @@ +import datetime +import json +import logging +from math import floor +from random import randint +import time +from decimal import Decimal +from os import path +from typing import TypedDict, cast + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.issuevalue.Value import Value +from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import ( + LinearAdditiveUtilitySpace, +) +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressTime import ProgressTime +from geniusweb.references.Parameters import Parameters +from tudelft_utilities_logging.ReportToLogger import ReportToLogger +from .utils.logger import Logger + +from .utils.opponent_model import OpponentModel +from .utils.utils import bid_to_string + +class SessionData(TypedDict): + progressAtFinish: float + utilityAtFinish: float + didAccept: bool + isGood: bool + topBidsPercentage: float + forceAcceptAtRemainingTurns: float + +class DataDict(TypedDict): + sessions: list[SessionData] + +class DreamTeam109Agent(DefaultParty): + + def __init__(self): + super().__init__() + self.logger: Logger = Logger(self.getReporter(), id(self)) + + self.domain: Domain = None + self.parameters: Parameters = None + self.profile: LinearAdditiveUtilitySpace = None + self.progress: ProgressTime = None + self.me: PartyId = None + self.other: PartyId = None + self.other_name: str = None + self.settings: Settings = None + self.storage_dir: str = None + + self.data_dict: DataDict = None + + self.last_received_bid: Bid = None + self.opponent_model: OpponentModel = None + self.all_bids: AllBidsList = None + self.bids_with_utilities: list[tuple[Bid, float]] = None + self.num_of_top_bids: int = 1 + self.min_util: float = 0.9 + + self.round_times: list[Decimal] = [] + self.last_time = None + self.avg_time = None + self.utility_at_finish: float = 0 + self.did_accept: bool = False + self.top_bids_percentage: float = 1 / 300 + self.force_accept_at_remaining_turns: float = 1 + self.force_accept_at_remaining_turns_light: float = 1 + self.opponent_best_bid: Bid = None + self.logger.log(logging.INFO, "party is initialized") + + def notifyChange(self, data: Inform): + """MUST BE IMPLEMENTED + This is the entry point of all interaction with your agent after is has been initialised. + How to handle the received data is based on its class type. + + Args: + info (Inform): Contains either a request for action or information. + """ + + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(data, Settings): + self.settings = cast(Settings, data) + self.me = self.settings.getID() + + # progress towards the deadline has to be tracked manually through the use of the Progress object + self.progress = self.settings.getProgress() + + self.parameters = self.settings.getParameters() + self.storage_dir = self.parameters.get("storage_dir") + + # the profile contains the preferences of the agent over the domain + profile_connection = ProfileConnectionFactory.create( + data.getProfile().getURI(), self.getReporter() + ) + self.profile = profile_connection.getProfile() + self.domain = self.profile.getDomain() + # compose a list of all possible bids + self.all_bids = AllBidsList(self.domain) + + profile_connection.close() + + # ActionDone informs you of an action (an offer or an accept) + # that is performed by one of the agents (including yourself). + elif isinstance(data, ActionDone): + action = cast(ActionDone, data).getAction() + actor = action.getActor() + + # ignore action if it is our action + if actor != self.me: + if self.other is None: + self.other = actor + # obtain the name of the opponent, cutting of the position ID. + self.other_name = str(actor).rsplit("_", 1)[0] + self.attempt_load_data() + self.learn_from_past_sessions(self.data_dict["sessions"]) + + # process action done by opponent + self.opponent_action(action) + # YourTurn notifies you that it is your turn to act + elif isinstance(data, YourTurn): + # execute a turn + self.my_turn() + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(data, Finished): + agreements = cast(Finished, data).getAgreements() + if len(agreements.getMap()) > 0: + agreed_bid = agreements.getMap()[self.me] + self.logger.log(logging.INFO, "agreed_bid = " + bid_to_string(agreed_bid)) + self.utility_at_finish = float(self.profile.getUtility(agreed_bid)) + else: + self.logger.log(logging.INFO, "no agreed bid (timeout? some agent crashed?)") + + self.update_data_dict() + self.save_data() + + # terminate the agent MUST BE CALLED + self.logger.log(logging.INFO, "party is terminating") + super().terminate() + else: + self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data)) + + def getCapabilities(self) -> Capabilities: + """MUST BE IMPLEMENTED + Method to indicate to the protocol what the capabilities of this agent are. + Leave it as is for the ANL 2022 competition + + Returns: + Capabilities: Capabilities representation class + """ + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + def send_action(self, action: Action): + """Sends an action to the opponent(s) + + Args: + action (Action): action of this agent + """ + self.getConnection().send(action) + + # give a description of your agent + def getDescription(self) -> str: + """MUST BE IMPLEMENTED + Returns a description of your agent. 1 or 2 sentences. + + Returns: + str: Agent description + """ + return "DreamTeam109 agent for the ANL 2022 competition" + + def opponent_action(self, action): + """Process an action that was received from the opponent. + + Args: + action (Action): action of opponent + """ + # if it is an offer, set the last received bid + if isinstance(action, Offer): + # create opponent model if it was not yet initialised + if self.opponent_model is None: + self.opponent_model = OpponentModel(self.domain, self.logger) + + bid = cast(Offer, action).getBid() + + # update opponent model with bid + self.opponent_model.update(bid) + # set bid as last received + self.last_received_bid = bid + + if self.opponent_best_bid is None: + self.opponent_best_bid = bid + elif self.profile.getUtility(bid) > self.profile.getUtility(self.opponent_best_bid): + self.opponent_best_bid = bid + + def my_turn(self): + """This method is called when it is our turn. It should decide upon an action + to perform and send this action to the opponent. + """ + + # For calculating average time per round + if self.last_time is not None: + self.round_times.append(datetime.datetime.now().timestamp() - self.last_time.timestamp()) + self.avg_time = sum(self.round_times[-3:])/3 + self.last_time = datetime.datetime.now() + + # check if the last received offer is good enough + # if self.accept_condition(self.last_received_bid): + if self.accept_condition(self.last_received_bid): + self.logger.log(logging.INFO, "accepting bid : " + bid_to_string(self.last_received_bid)) + # if so, accept the offer + action = Accept(self.me, self.last_received_bid) + self.did_accept = True + else: + # if not, find a bid to propose as counter offer + bid = self.find_bid() + self.logger.log(logging.INFO, "Offering bid : " + bid_to_string(bid)) + action = Offer(self.me, bid) + + # send the action + self.send_action(action) + + def get_data_file_path(self) -> str: + return f"{self.storage_dir}/{self.other_name}.json" + + def attempt_load_data(self): + if path.exists(self.get_data_file_path()): + with open(self.get_data_file_path()) as f: + self.data_dict = json.load(f) + self.logger.log(logging.INFO, "Loaded previous data about opponent: " + self.other_name) + self.logger.log(logging.INFO, "data_dict = " + str(self.data_dict)) + else: + self.logger.log(logging.WARN, "No previous data saved about opponent: " + self.other_name) + # initialize an empty data dict + self.data_dict = { + "sessions": [] + } + + def update_data_dict(self): + # NOTE: We shouldn't do extensive calculations in this method (see note in save_data method) + + progress_at_finish = self.progress.get(time.time() * 1000) + + session_data: SessionData = { + "progressAtFinish": progress_at_finish, + "utilityAtFinish": self.utility_at_finish, + "didAccept": self.did_accept, + "isGood": self.utility_at_finish >= self.min_util, + "topBidsPercentage": self.top_bids_percentage, + "forceAcceptAtRemainingTurns": self.force_accept_at_remaining_turns + } + + self.logger.log(logging.INFO, "Updating data dict with session data: " + str(session_data)) + self.data_dict["sessions"].append(session_data) + + def save_data(self): + """This method is called after the negotiation is finished. It can be used to store data + for learning capabilities. Note that no extensive calculations can be done within this method. + Taking too much time might result in your agent being killed, so use it for storage only. + """ + if self.other_name is None: + self.logger.log(logging.WARNING, "Opponent name was not set; skipping save data") + else: + json_data = json.dumps(self.data_dict, sort_keys=True, indent=4) + with open(self.get_data_file_path(), "w") as f: + f.write(json_data) + self.logger.log(logging.INFO, "Saved data about opponent: " + self.other_name) + + def learn_from_past_sessions(self, sessions: list[SessionData]): + accept_levels = [0, 0, 1, 1.1] + light_accept_levels = [0, 1, 1.1] + top_bids_levels = [1 / 300, 1 / 100, 1 / 30] + + self.force_accept_at_remaining_turns = accept_levels[min(len(accept_levels) - 1, len(list(filter(self.did_fail, sessions))))] + self.force_accept_at_remaining_turns_light = light_accept_levels[min(len(light_accept_levels) - 1, len(list(filter(self.did_fail, sessions))))] + self.top_bids_percentage = top_bids_levels[min(len(top_bids_levels) - 1, len(list(filter(self.low_utility, sessions))))] + + def did_fail(self, session: SessionData): + return session["utilityAtFinish"] == 0 + + def low_utility(self, session: SessionData): + return session["utilityAtFinish"] < 0.5 + + def accept_condition(self, bid: Bid) -> bool: + if bid is None: + return False + + # progress of the negotiation session between 0 and 1 (1 is deadline) + progress = self.progress.get(time.time() * 1000) + threshold = 0.98 + light_threshold = 0.95 + + if self.avg_time is not None: + threshold = 1 - 1000 * self.force_accept_at_remaining_turns * self.avg_time / self.progress.getDuration() + light_threshold = 1 - 5000 * self.force_accept_at_remaining_turns_light * self.avg_time / self.progress.getDuration() + + conditions = [ + self.profile.getUtility(bid) >= self.min_util, + progress >= threshold, + progress > light_threshold and self.profile.getUtility(bid) >= self.bids_with_utilities[floor(len(self.bids_with_utilities) / 5) - 1][1] + ] + return any(conditions) + + def find_bid(self) -> Bid: + self.logger.log(logging.INFO, "finding bid...") + + num_of_bids = self.all_bids.size() + + if self.bids_with_utilities is None: + self.logger.log(logging.INFO, "calculating bids_with_utilities...") + startTime = time.time() + self.bids_with_utilities = [] + + for index in range(num_of_bids): + bid = self.all_bids.get(index) + bid_utility = float(self.profile.getUtility(bid)) + self.bids_with_utilities.append((bid, bid_utility)) + + self.bids_with_utilities.sort(key=lambda tup: tup[1], reverse=True) + + endTime = time.time() + self.logger.log(logging.INFO, "calculating bids_with_utilities took (in seconds): " + str(endTime - startTime)) + + self.num_of_top_bids = max(5, num_of_bids * self.top_bids_percentage) + + if (self.last_received_bid is None): + return self.bids_with_utilities[0][0] + + progress = self.progress.get(time.time() * 1000) + light_threshold = 0.95 + + if self.avg_time is not None: + light_threshold = 1 - 5000 * self.force_accept_at_remaining_turns_light * self.avg_time / self.progress.getDuration() + + if (progress > light_threshold): + return self.opponent_best_bid + + if (num_of_bids < self.num_of_top_bids): + self.num_of_top_bids = num_of_bids / 2 + + self.min_util = self.bids_with_utilities[floor(self.num_of_top_bids) - 1][1] + self.logger.log(logging.INFO, "min_util = " + str(self.min_util)) + + picked_ranking = randint(0, floor(self.num_of_top_bids) - 1) + + return self.bids_with_utilities[picked_ranking][0] + + def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float: + """Calculate heuristic score for a bid + + Args: + bid (Bid): Bid to score + alpha (float, optional): Trade-off factor between self interested and + altruistic behaviour. Defaults to 0.95. + eps (float, optional): Time pressure factor, balances between conceding + and Boulware behaviour over time. Defaults to 0.1. + + Returns: + float: score + """ + progress = self.progress.get(time.time() * 1000) + + our_utility = float(self.profile.getUtility(bid)) + + time_pressure = 1.0 - progress ** (1 / eps) + score = alpha * time_pressure * our_utility + + if self.opponent_model is not None: + opponent_utility = self.opponent_model.get_predicted_utility(bid) + opponent_score = (1.0 - alpha * time_pressure) * opponent_utility + score += opponent_score + + return score diff --git a/agents_test/dreamteam109_agent/utils/__init__.py b/agents_test/dreamteam109_agent/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents_test/dreamteam109_agent/utils/logger.py b/agents_test/dreamteam109_agent/utils/logger.py new file mode 100644 index 00000000..0ede400c --- /dev/null +++ b/agents_test/dreamteam109_agent/utils/logger.py @@ -0,0 +1,10 @@ +from tudelft_utilities_logging.ReportToLogger import ReportToLogger + +class Logger: + + def __init__(self, base_logger: ReportToLogger, id: int): + self.base_logger = base_logger + self.id = id + + def log(self, level:int , msg:str, thrown: BaseException=None) -> None: + self.base_logger.log(level, f"{self.id} - {msg}", thrown) diff --git a/agents_test/dreamteam109_agent/utils/opponent_model.py b/agents_test/dreamteam109_agent/utils/opponent_model.py new file mode 100644 index 00000000..99d57e01 --- /dev/null +++ b/agents_test/dreamteam109_agent/utils/opponent_model.py @@ -0,0 +1,127 @@ +from collections import defaultdict +import logging + +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value + +from .logger import Logger + +from .utils import bid_to_string + +class OpponentModel: + def __init__(self, domain: Domain, logger: Logger): + self.offers = [] + self.domain = domain + self.logger = logger + + self.issue_estimators = { + i: IssueEstimator(v) for i, v in domain.getIssuesValues().items() + } + + def update(self, bid: Bid): + self.logger.log(logging.INFO, "updating opponent model with received bid = " + bid_to_string(bid)) + # keep track of all bids received + self.offers.append(bid) + + # update all issue estimators with the value that is offered for that issue + for issue_id, issue_estimator in self.issue_estimators.items(): + issue_estimator.update(bid.getValue(issue_id)) + + def get_predicted_utility(self, bid: Bid): + if len(self.offers) == 0 or bid is None: + return 0 + + # initiate + total_issue_weight = 0.0 + value_utilities = [] + issue_weights = [] + + for issue_id, issue_estimator in self.issue_estimators.items(): + # get the value that is set for this issue in the bid + value: Value = bid.getValue(issue_id) + + # collect both the predicted weight for the issue and + # predicted utility of the value within this issue + value_utilities.append(issue_estimator.get_value_utility(value)) + issue_weights.append(issue_estimator.weight) + + total_issue_weight += issue_estimator.weight + + # normalise the issue weights such that the sum is 1.0 + if total_issue_weight == 0.0: + issue_weights = [1 / len(issue_weights) for _ in issue_weights] + else: + issue_weights = [iw / total_issue_weight for iw in issue_weights] + + # calculate predicted utility by multiplying all value utilities with their issue weight + predicted_utility = sum( + [iw * vu for iw, vu in zip(issue_weights, value_utilities)] + ) + + return predicted_utility + + +class IssueEstimator: + def __init__(self, value_set: DiscreteValueSet): + if not isinstance(value_set, DiscreteValueSet): + raise TypeError( + "This issue estimator only supports issues with discrete values" + ) + + self.bids_received = 0 + self.max_value_count = 0 + self.num_values = value_set.size() + self.value_trackers = defaultdict(ValueEstimator) + self.weight = 0 + + def update(self, value: Value): + self.bids_received += 1 + + # get the value tracker of the value that is offered + value_tracker = self.value_trackers[value] + + # register that this value was offered + value_tracker.update() + + # update the count of the most common offered value + self.max_value_count = max([value_tracker.count, self.max_value_count]) + + # update predicted issue weight + # the intuition here is that if the values of the receiverd offers spread out over all + # possible values, then this issue is likely not important to the opponent (weight == 0.0). + # If all received offers proposed the same value for this issue, + # then the predicted issue weight == 1.0 + equal_shares = self.bids_received / self.num_values + self.weight = (self.max_value_count - equal_shares) / ( + self.bids_received - equal_shares + ) + + # recalculate all value utilities + for value_tracker in self.value_trackers.values(): + value_tracker.recalculate_utility(self.max_value_count, self.weight) + + def get_value_utility(self, value: Value): + if value in self.value_trackers: + return self.value_trackers[value].utility + + return 0 + + +class ValueEstimator: + def __init__(self): + self.count = 0 + self.utility = 0 + + def update(self): + self.count += 1 + + def recalculate_utility(self, max_value_count: int, weight: float): + if weight < 1: + mod_value_count = ((self.count + 1) ** (1 - weight)) - 1 + mod_max_value_count = ((max_value_count + 1) ** (1 - weight)) - 1 + + self.utility = mod_value_count / mod_max_value_count + else: + self.utility = 1 diff --git a/agents_test/dreamteam109_agent/utils/utils.py b/agents_test/dreamteam109_agent/utils/utils.py new file mode 100644 index 00000000..802dbe44 --- /dev/null +++ b/agents_test/dreamteam109_agent/utils/utils.py @@ -0,0 +1,4 @@ +from geniusweb.issuevalue.Bid import Bid + +def bid_to_string(bid: Bid) -> str: + return str(dict(sorted(bid.getIssueValues().items()))) diff --git a/agents_test/hardliner_agent/hardliner_agent.py b/agents_test/hardliner_agent/hardliner_agent.py new file mode 100644 index 00000000..0e2925a6 --- /dev/null +++ b/agents_test/hardliner_agent/hardliner_agent.py @@ -0,0 +1,23 @@ +from agents.time_dependent_agent.time_dependent_agent import TimeDependentAgent +from tudelft_utilities_logging.Reporter import Reporter + + +class HardlinerAgent(TimeDependentAgent): + """ + A simple party that places random bids and accepts when it receives an offer + with sufficient utility. + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + + # Override + def getDescription(self) -> str: + return ( + "Hardliner: does not concede. " + + "Parameters minPower (default 1) and maxPower (default infinity) are used when voting" + ) + + # Override + def getE(self) -> float: + return 0.0 diff --git a/agents_test/learning_agent/LearnedData.py b/agents_test/learning_agent/LearnedData.py new file mode 100644 index 00000000..eecb942d --- /dev/null +++ b/agents_test/learning_agent/LearnedData.py @@ -0,0 +1,218 @@ +import math +from math import sqrt + +from .NegotiationData import NegotiationData + + +class LearnedData: + """This class hold the learned data of our agent. + """ + + __tSplit: int = 40 + __tPhase: float = 0.2 + __newWeight: float = 0.3 + __newWeightForReject: float = 0.3 + __smoothWidth: int = 3 # from each side of the element + __smoothWidthForReject: int = 3 # from each side of the element + __opponentDecrease: float = 0.65 + __defualtAlpha: float = 10.7 + + def __init__(self): + + self.__opponentName: str = None + # average utility of agreement + self.__avgUtility: float = 0.0 + # num of negotiations against this opponent + self.__numEncounters: int = 0 + self.__avgMaxUtilityOpponent: float = 0.0 + + # our new data structures + self.__stdUtility: float = 0.0 + self.__negoResults: list = [] + self.__avgOpponentUtility: float = 0.0 + self.__opponentAlpha: float = 0.0 + self.__opponentUtilByTime: list = [] + self.__opponentMaxReject: list = [0.0] * self.__tSplit + + def encode(self, paramList: list): + """ This function get deserialize json + """ + self.__opponentName = paramList[0] + self.__avgUtility = paramList[1] + self.__numEncounters = paramList[2] + self.__avgMaxUtilityOpponent = paramList[3] + self.__stdUtility = paramList[4] + self.__negoResults = paramList[5] + self.__avgOpponentUtility = paramList[6] + self.__opponentAlpha = paramList[7] + self.__opponentUtilByTime = paramList[8] + self.__opponentMaxReject = paramList[9] + + def update(self, negotiationData: NegotiationData): + """ Update the learned data with a negotiation data of a previous negotiation + session + negotiationData NegotiationData class holding the negotiation data + that is obtain during a negotiation session. + """ + # Keep track of the average utility that we obtained Double + newUtil = negotiationData.getAgreementUtil() if (negotiationData.getAgreementUtil() > 0) \ + else self.__avgUtility - 1.1 * pow(self.__stdUtility, 2) + + self.__avgUtility = (self.__avgUtility * self.__numEncounters + newUtil) \ + / (self.__numEncounters + 1) + + # add utility to UtiList calculate std deviation of results + self.__negoResults.append(negotiationData.getAgreementUtil()) + self.__stdUtility = 0.0 + + for util in self.__negoResults: + self.__stdUtility += pow(util - self.__avgUtility, 2) + self.__stdUtility = sqrt(self.__stdUtility / (self.__numEncounters + 1)) + + # Track the average value of the maximum that an opponent has offered us across + # multiple negotiation sessions Double + self.__avgMaxUtilityOpponent = ( + self.__avgMaxUtilityOpponent * self.__numEncounters + negotiationData.getMaxReceivedUtil()) \ + / (self.__numEncounters + 1) + + self.__avgOpponentUtility = ( + self.__avgOpponentUtility * self.__numEncounters + negotiationData.getOpponentUtil()) \ + / (self.__numEncounters + 1) + + # update opponent utility over time + opponentTimeUtil: list = [0.0] * self.__tSplit if self.__opponentUtilByTime == [] else self.__opponentUtilByTime + # update opponent reject over time + opponentMaxReject: list = [0.0] * self.__tSplit if self.__opponentMaxReject == [] else self.__opponentMaxReject + + # update values in the array + newUtilData: list = negotiationData.getOpponentUtilByTime() + newOpponentMaxReject = negotiationData.getOpponentMaxReject() + + if self.__numEncounters == 0: + self.__opponentUtilByTime = newUtilData + self.__opponentMaxReject = newOpponentMaxReject + + else: + # find the ratio of decrease in the array, for updating 0 - s in the array + ratio: float = ((1 - self.__newWeight) * opponentTimeUtil[0] + self.__newWeight * newUtilData[0]) / \ + opponentTimeUtil[0] \ + if opponentTimeUtil[0] > 0.0 else 1 + + # update the array + for i in range(self.__tSplit): + if (newUtilData[i] > 0): + opponentTimeUtil[i] = ( + (1 - self.__newWeight) * opponentTimeUtil[i] + self.__newWeight * newUtilData[i]) + else: + opponentTimeUtil[i] *= ratio + + self.__opponentUtilByTime = opponentTimeUtil + + # find the ratio of decrease in the array, for updating 0 - s in the array + ratio: float = ((1 - self.__newWeightForReject) * opponentMaxReject[0] + self.__newWeightForReject * + newOpponentMaxReject[0]) / \ + opponentMaxReject[0] \ + if opponentMaxReject[0] > 0.0 else 1 + + # update the array + for i in range(self.__tSplit): + if (newOpponentMaxReject[i] > 0): + opponentMaxReject[i] = ( + (1 - self.__newWeightForReject) * opponentMaxReject[i] + self.__newWeightForReject * + newOpponentMaxReject[i]) + else: + opponentMaxReject[i] *= ratio + + self.__opponentMaxReject = opponentMaxReject + + self.__opponentAlpha = self.calcAlpha() + + # Keep track of the number of negotiations that we performed + self.__numEncounters += 1 + + def calcAlpha(self): + # smoothing with smooth width of smoothWidth + alphaArray: list = self.getSmoothThresholdOverTime() + + # find the last index with data in alphaArray + + maxIndex: int = 0 + while maxIndex < self.__tSplit and alphaArray[maxIndex] > 0.2: + maxIndex += 1 + + # find t, time that threshold decrease by 50 % + maxValue: float = alphaArray[0] + minValue: float = alphaArray[max(maxIndex - self.__smoothWidth - 1, 0)] + + # if there is no clear trend-line, return default value + if maxValue - minValue < 0.1: + return self.__defualtAlpha + + t: int = 0 + while t < maxIndex and alphaArray[t] > (maxValue - self.__opponentDecrease * (maxValue - minValue)): + t += 1 + + calibratedPolynom: list = [572.83, -1186.7, 899.29, -284.68, 32.911] + alpha: float = calibratedPolynom[0] + + tTime: float = self.__tPhase + (1 - self.__tPhase) * ( + maxIndex * (float(t) / self.__tSplit) + (self.__tSplit - maxIndex) * 0.85) / self.__tSplit + for i in range(1, len(calibratedPolynom)): + alpha = alpha * tTime + calibratedPolynom[i] + + return alpha + + def getSmoothThresholdOverTime(self): + # smoothing with smooth width of smoothWidth + smoothedTimeUtil: list = [0.0] * self.__tSplit + + # ignore zeros in end of the array + tSplitWithoutZero = self.__tSplit - 1 + while self.__opponentUtilByTime[tSplitWithoutZero] == 0 and tSplitWithoutZero > 0: + tSplitWithoutZero -= 1 + tSplitWithoutZero += 1 + for i in range(tSplitWithoutZero): + for j in range(max(i - self.__smoothWidth, 0), min(i + self.__smoothWidth + 1, tSplitWithoutZero)): + smoothedTimeUtil[i] += self.__opponentUtilByTime[j] + smoothedTimeUtil[i] /= (min(i + self.__smoothWidth + 1, tSplitWithoutZero) - max(i - self.__smoothWidth, 0)) + + return smoothedTimeUtil + + def getSmoothRejectOverTime(self): + # smoothing with smooth width of smoothWidth + smoothedRejectUtil: list = [0.0] * self.__tSplit + + # ignore zeros in end of the array + tSplitWithoutZero = self.__tSplit - 1 + while self.__opponentMaxReject[tSplitWithoutZero] == 0 and tSplitWithoutZero > 0: + tSplitWithoutZero -= 1 + tSplitWithoutZero += 1 + for i in range(tSplitWithoutZero): + for j in range(max(i - self.__smoothWidthForReject, 0), + min(i + self.__smoothWidthForReject + 1, tSplitWithoutZero)): + smoothedRejectUtil[i] += self.__opponentMaxReject[j] + smoothedRejectUtil[i] /= (min(i + self.__smoothWidthForReject + 1, tSplitWithoutZero) - max( + i - self.__smoothWidthForReject, 0)) + + return smoothedRejectUtil + + def getAvgUtility(self): + return self.__avgUtility + + def getStdUtility(self): + return self.__stdUtility + + def getOpponentAlpha(self): + return self.__opponentAlpha + + def getOpUtility(self): + return self.__avgOpponentUtility + + def getAvgMaxUtility(self): + return self.__avgMaxUtilityOpponent + + def getOpponentEncounters(self): + return self.__numEncounters + + def setOpponentName(self, opponentName): + self.__opponentName = opponentName diff --git a/agents_test/learning_agent/NegotiationData.py b/agents_test/learning_agent/NegotiationData.py new file mode 100644 index 00000000..a276796f --- /dev/null +++ b/agents_test/learning_agent/NegotiationData.py @@ -0,0 +1,65 @@ +class NegotiationData: + """The class hold the negotiation data that is obtain during a negotiation + session.It will be saved to disk after the negotiation has finished. + this negotiation used to update the learning data of the agent. + """ + __tSplit = 40 + + def __init__(self): + self.__maxReceivedUtil: float = 0.0 + self.__agreementUtil: float = 0.0 + self.__opponentName: str = None + self.__opponentUtil: float = 0.0 + self.__opponentMaxReject: list = [0.0] * self.__tSplit + self.__opponentUtilByTime: list = [0.0] * self.__tSplit + + def encode(self, paramList: list): + """ This function get deserialize json + """ + self.__maxReceivedUtil = paramList[0] + self.__agreementUtil = paramList[1] + self.__opponentName = paramList[2] + self.__opponentUtil = paramList[3] + self.__opponentMaxReject = paramList[4] + self.__opponentUtilByTime = paramList[5] + + def addAgreementUtil(self, agreementUtil: float): + self.__agreementUtil = agreementUtil + if (agreementUtil > self.__maxReceivedUtil): + self.__maxReceivedUtil = agreementUtil + + def addBidUtil(self, bidUtil: float): + if (bidUtil > self.__maxReceivedUtil): + self.__maxReceivedUtil = bidUtil + + def addRejectUtil(self, index: int, bidUtil: float): + if (bidUtil > self.__opponentMaxReject[index]): + self.__opponentMaxReject[index] = bidUtil + + def updateOpponentOffers(self, opSum: list, opCounts: list): + for i in range(self.__tSplit): + self.__opponentUtilByTime[i] = opSum[i] / opCounts[i] if opCounts[i] > 0 else 0.0 + + def setOpponentName(self, opponentName: str): + self.__opponentName = opponentName + + def setOpponentUtil(self, oppUtil: float): + self.__opponentUtil = oppUtil + + def getOpponentName(self): + return self.__opponentName + + def getMaxReceivedUtil(self): + return self.__maxReceivedUtil + + def getAgreementUtil(self): + return self.__agreementUtil + + def getOpponentUtil(self): + return self.__opponentUtil + + def getOpponentUtilByTime(self): + return self.__opponentUtilByTime + + def getOpponentMaxReject(self): + return self.__opponentMaxReject diff --git a/agents_test/learning_agent/Pair.py b/agents_test/learning_agent/Pair.py new file mode 100644 index 00000000..18f11311 --- /dev/null +++ b/agents_test/learning_agent/Pair.py @@ -0,0 +1,4 @@ + +class Pair: + vList: {} + type: int = -1 #-1 - An invalid value type, 0 - Discrete value, 1 - Number value diff --git a/agents_test/learning_agent/learning_agent.py b/agents_test/learning_agent/learning_agent.py new file mode 100644 index 00000000..60db078c --- /dev/null +++ b/agents_test/learning_agent/learning_agent.py @@ -0,0 +1,569 @@ +import json +import math +import os +from decimal import Decimal +from os.path import exists + +from geniusweb.inform.Agreements import Agreements +from geniusweb.issuevalue.ValueSet import ValueSet +from geniusweb.issuevalue.Value import Value +from geniusweb.issuevalue.DiscreteValue import DiscreteValue +from geniusweb.issuevalue.NumberValue import NumberValue + +import logging +from random import randint +import time +from typing import cast + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressTime import ProgressTime +from geniusweb.references.Parameters import Parameters +from numpy import long +from tudelft_utilities_logging.ReportToLogger import ReportToLogger + +from .LearnedData import LearnedData +from .NegotiationData import NegotiationData +from .Pair import Pair + +# static vars +defualtAlpha: float = 10.7 +# estimate opponent time - variant threshold function +tSplit: int = 40 +# agent has 2 - phases - learning of the opponent and offering bids while considering opponent utility, this constant define the threshold between those two phases +tPhase: float = 0.2 + + + +class LearningAgent(DefaultParty): + def __init__(self): + super().__init__() + self.logger: ReportToLogger = self.getReporter() + self.lastReceivedBid: Bid = None + self.me: PartyId = None + self.progress: ProgressTime = None + self.protocol: str = None + self.parameters: Parameters = None + self.utilitySpace: UtilitySpace = None + self.domain: Domain = None + self.learnedData: LearnedData = None + self.negotiationData: NegotiationData = None + self.learnedDataPath: str = None + self.negotiationDataPath: str = None + self.storage_dir: str = None + + self.opponentName: str = None + + # Expecting Lower Limit of Concession Function behavior + # The idea here that we will keep for a negotiation scenario the most frequent + # Issues - Values, afterwards, as a counter offer bid for each issue we will select the most frequent value. + self.freqMap: dict = None + + # average and standard deviation of the competition for determine "good" utility threshold + self.avgUtil: float = 0.95 + self.stdUtil: float = 0.15 + self.utilThreshold: float = 0.95 + + self.alpha: float = defualtAlpha + + self.opCounter: list = [0] * tSplit + self.opSum: list = [0.0] * tSplit + self.opThreshold: list = [0.0] * tSplit + self.opReject: list = [0.0] * tSplit + + # Best bid for agent, exists if bid space is small enough to search in + self.MAX_SEARCHABLE_BIDSPACE: long = 50000 + self.MIN_UTILITY: float = 0.6 + self.optimalBid: Bid = None + self.bestOfferBid: Bid = None + self.allBidList: AllBidsList = None + + self.lastOfferBid = None # our last offer to the opponent + + def notifyChange(self, data: Inform): + """ + Args: + data (Inform): Contains either a request for action or information. + """ + try: + # a Settings message is the first message that will be send to your + # agent containing all the information about the negotiation session. + if isinstance(data, Settings): + self.settingsFunction(cast(Settings, data)) + + # ActionDone informs you of an action (an offer or an accept) + # that is performed by one of the agents (including yourself). + elif isinstance(data, ActionDone): + self.actionDoneFunction(cast(ActionDone, data)) + + # YourTurn notifies you that it is your turn to act + elif isinstance(data, YourTurn): + # execute a turn + self.myTurn() + + # Finished will be send if the negotiation has ended (through agreement or deadline) + elif isinstance(data, Finished): + self.finishedFunction(cast(Finished, data)) + + else: + self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data)) + + except: + self.logger.log(logging.ERROR, "error notifyChange") + + def getDescription(self) -> str: + """Returns a description of your agent. + + Returns: + str: Agent description + """ + return "This is party of ANL 2022. It can handle the Learn protocol and learns utility function and threshold of the opponent." + + def getCapabilities(self) -> Capabilities: + """ + Method to indicate to the protocol what the capabilities of this agent are. + Leave it as is for the ANL 2022 competition + + Returns: + Capabilities: Capabilities representation class + """ + return Capabilities( + set(["SAOP"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + def finishedFunction(self, data: Finished): + # object also contains the final agreement( if any). + agreements: Agreements = data.getAgreements() + self.processAgreements(agreements) + + # Write the negotiation data that we collected to the path provided. + if not (self.negotiationDataPath == None or self.negotiationData == None): + try: + with open(self.negotiationDataPath, "w") as f: + # w means overwritten + json.dump(self.negotiationData.__dict__, default=lambda o: o.__dict__, indent=5, fp=f) + + + except: + self.logger.log(logging.ERROR, "Failed to write negotiation data to disk") + + # Write the learned data to the path provided. + if not (self.learnedDataPath == None or self.learnedData == None): + try: + with open(self.learnedDataPath, "w") as f: + # w means overwritten + json.dump(self.learnedData.__dict__, default=lambda o: o.__dict__, indent=9, fp=f) + + + except: + self.logger.log(logging.ERROR, "Failed to learned data to disk") + + self.logger.log(logging.INFO, "party is terminating:") + super().terminate() + + def actionDoneFunction(self, data: ActionDone): + # The info object is an action that is performed by an agent. + action: Action = data.getAction() + actor = action.getActor() + + # Check if this is not our own action + if self.me is not None and not (self.me == actor): + # Check if we already know who we are playing against. + if self.opponentName == None: + # The part behind the last _ is always changing, so we must cut it off. + self.opponentName = str(actor).rsplit("_", 1)[0] + + # path depend on opponent name + self.negotiationDataPath = self.getPath("negotiationData", self.opponentName) + self.learnedDataPath = self.getPath("learnedData", self.opponentName) + + # update and load learnedData + self.updateAndLoadLearnedData() + + # Add name of the opponent to the negotiation data + self.negotiationData.setOpponentName(self.opponentName) + + # avg opponent offer utility + self.opThreshold = self.learnedData.getSmoothThresholdOverTime() \ + if self.learnedData != None else None + if not (self.opThreshold == None): + for i in range(tSplit): + self.opThreshold[i] = self.opThreshold[i] if self.opThreshold[i] > 0 else self.opThreshold[ + i - 1] + + # max offer the opponent reject + self.opReject = self.learnedData.getSmoothRejectOverTime() \ + if self.learnedData != None else None + if not (self.opReject == None): + for i in range(tSplit): + self.opReject[i] = self.opReject[i] if self.opReject[i] > 0 else self.opReject[ + i - 1] + + # decay rate of threshold function + self.alpha = self.learnedData.getOpponentAlpha() if self.learnedData != None else 0.0 + self.alpha = self.alpha if self.alpha > 0.0 else defualtAlpha + + # Process the action of the opponent. + self.processAction(action) + + def settingsFunction(self, data: Settings): + # info is a Settings object that is passed at the start of a negotiation + settings: Settings = data + + # ID of my agent + self.me = settings.getID() + + # The progress object keeps track of the deadline + self.progress = settings.getProgress() + + # Protocol that is initiate for the agent + self.protocol = str(settings.getProtocol().getURI().getPath()) + + # Parameters for the agent (can be passed through the GeniusWeb GUI, or a JSON-file) + self.parameters = settings.getParameters() + + self.storage_dir = self.parameters.get("storage_dir") + + # We are in the negotiation step. + # Create a new NegotiationData object to store information on this negotiation. + # See 'NegotiationData.py'. + + self.negotiationData = NegotiationData() + + # Obtain our utility space, i.e.the problem we are negotiating and our + # preferences over it. + try: + # the profile contains the preferences of the agent over the domain + profile_connection = ProfileConnectionFactory.create(data.getProfile().getURI(), self.getReporter()) + self.domain = profile_connection.getProfile().getDomain() + + # Create a Issues-Values frequency map + if self.freqMap == None: + # Map wasn't created before, create a new instance now + self.freqMap = {} + else: + # Map was created before, but this is a new negotiation scenario, clear the old map. + self.freqMap.clear() + + # Obtain all of the issues in the current negotiation domain + issues: set = self.domain.getIssues() + for s in issues: + # create new list of all the values for + p: Pair = Pair() + p.vList = {} + + # gather type of issue based on the first element + vs: ValueSet = self.domain.getValues(s) + if isinstance(vs.get(0), DiscreteValue): + p.type = 0 + elif isinstance(vs.get(0), NumberValue): + p.type = 1 + + # Obtain all of the values for an issue "s" + for v in vs: + # Add a new entry in the frequency map for each(s, v, typeof(v)) + vStr: str = self.valueToStr(v, p) + p.vList[vStr] = 0 + + self.freqMap[s] = p + + except: + self.logger.log(logging.ERROR, "error settingsFunction") + + # self.utilitySpace = cast(profile_connection.getProfile(), UtilitySpace) + self.utilitySpace = profile_connection.getProfile() + profile_connection.close() + + self.allBidList = AllBidsList(self.domain) + + # Attempt to find the optimal bid in a search-able bid space, if bid space size + # is small / equal to MAX_SEARCHABLE_BIDSPACE + if self.allBidList.size() <= self.MAX_SEARCHABLE_BIDSPACE: + mx_util: Decimal = Decimal(0) + for i in range(self.allBidList.size()): + b: Bid = self.allBidList.get(i) + canidate: Decimal = self.utilitySpace.getUtility(b) + if canidate > mx_util: + mx_util = canidate + self.optimalBid = b + + else: + mx_util: Decimal = Decimal(0) + # Iterate randomly through list of bids until we find a good bid + for attempt in range(self.MAX_SEARCHABLE_BIDSPACE.intValue()): + i: long = randint(0, self.allBidList.size()) + b: Bid = self.allBidList.get(i) + canidate: Decimal = self.utilitySpace.getUtility(b) + if canidate > mx_util: + mx_util = canidate + self.optimalBid = b + + def isNearNegotiationEnd(self): + return 0 if self.progress.get(int(time.time() * 1000)) < tPhase else 1 + + def processAction(self, action: Action): + """Processes an Action performed by the opponent.""" + if isinstance(action, Offer): + # If the action was an offer: Obtain the bid + self.lastReceivedBid = cast(Offer, action).getBid() + self.updateFreqMap(self.lastReceivedBid) + + # add it's value to our negotiation data. + utilVal: float = float(self.utilitySpace.getUtility(self.lastReceivedBid)) + self.negotiationData.addBidUtil(utilVal) + + def processAgreements(self, agreements: Agreements): + + """ This method is called when the negotiation has finished. It can process the" + final agreement. + """ + # Check if we reached an agreement (walking away or passing the deadline + # results in no agreement) + if agreements.getMap() != None and not (agreements.getMap() == {}): + # Get the bid that is agreed upon and add it's value to our negotiation data + agreement: Bid = list(agreements.getMap().values())[0] + self.negotiationData.addAgreementUtil(float(self.utilitySpace.getUtility(agreement))) + self.negotiationData.setOpponentUtil(self.calcOpValue(agreement)) + + # negotiation failed + else: + if not (self.bestOfferBid == None): + self.negotiationData.addAgreementUtil(float(self.utilitySpace.getUtility(self.bestOfferBid))) + + # update opponent reject list + if self.lastOfferBid != None: + self.negotiationData.addRejectUtil(tSplit - 1, self.calcOpValue(self.lastOfferBid)) + + # update the opponent offers map, regardless of achieving agreement or not + try: + self.negotiationData.updateOpponentOffers(self.opSum, self.opCounter); + except: + self.logger.log(logging.ERROR, "error processAgreements") + + # send our next offer + def myTurn(self): + action: Action = None + + # save average of the last avgSplit offers (only when frequency table is stabilized) + if self.isNearNegotiationEnd() > 0: + index: int = (int)((tSplit - 1) / (1 - tPhase) * (self.progress.get(int(time.time() * 1000)) - tPhase)) + + if self.lastReceivedBid != None: + self.opSum[index] += self.calcOpValue(self.lastReceivedBid) + self.opCounter[index] += 1 + + if self.lastOfferBid != None: + self.negotiationData.addRejectUtil(index, self.calcOpValue(self.lastOfferBid)) + + # evaluate the offer and accept or give counter-offer + if self.isGood(self.lastReceivedBid): + # If the last received bid is good: create Accept action + action = Accept(self.me, self.lastReceivedBid) + else: + # there are 3 phases in the negotiation process: + # 1. Send random bids that considered to be GOOD for our agent + # 2. Send random bids that considered to be GOOD for both of the agents + bid: Bid = None + + if self.bestOfferBid == None: + self.bestOfferBid = self.lastReceivedBid + elif self.lastReceivedBid != None and self.utilitySpace.getUtility(self.lastReceivedBid) > self.utilitySpace \ + .getUtility(self.bestOfferBid): + self.bestOfferBid = self.lastReceivedBid + + isNearNegotiationEnd = self.isNearNegotiationEnd() + if isNearNegotiationEnd == 0: + attempt = 0 + while attempt < 1000 and not self.isGood(bid): + attempt += 1 + i: long = randint(0, self.allBidList.size()) + bid = self.allBidList.get(i) + + bid = bid if (self.isGood( + bid)) else self.optimalBid # if the last bid isn't good, offer (default) the optimal bid + + elif isNearNegotiationEnd == 1: + if self.progress.get(int(time.time() * 1000)) > 0.95: + maxOpponentUtility: float = 0.0 + maxBid: Bid = None + i = 0 + while i < 10000 and self.progress.get(int(time.time() * 1000)) < 0.99: + i: long = randint(0, self.allBidList.size()) + bid = self.allBidList.get(i) + if self.isGood(bid) and self.isOpGood(bid): + opValue = self.calcOpValue(bid) + if opValue > maxOpponentUtility: + maxOpponentUtility = opValue + maxBid = bid + i += 1 + bid = maxBid + else: + # look for bid with max utility for opponent + maxOpponentUtility: float = 0.0 + maxBid: Bid = None + for i in range(2000): + i: long = randint(0, self.allBidList.size()) + bid = self.allBidList.get(i) + if self.isGood(bid) and self.isOpGood(bid): + opValue = self.calcOpValue(bid) + if opValue > maxOpponentUtility: + maxOpponentUtility = opValue + maxBid = bid + bid = maxBid + + bid = self.bestOfferBid if (self.progress.get(int(time.time() * 1000)) > 0.99) and self.isGood( + self.bestOfferBid) else bid + bid = bid if self.isGood( + bid) else self.optimalBid # if the last bid isn't good, offer (default) the optimal bid + + # Create offer action + action = Offer(self.me, bid) + self.lastOfferBid = bid + + # Send action + self.getConnection().send(action) + + def isGood(self, bid: Bid): + """ The method checks if a bid is good. + param bid the bid to check + return true iff bid is good for us. + """ + if bid == None: + return False + maxVlue: float = 0.95 * float( + self.utilitySpace.getUtility(self.optimalBid)) if not self.optimalBid == None else 0.95 + avgMaxUtility: float = self.learnedData.getAvgMaxUtility() \ + if self.learnedData != None \ + else self.avgUtil + + self.utilThreshold = maxVlue \ + - (maxVlue - 0.55 * self.avgUtil - 0.4 * avgMaxUtility + 0.5 * pow(self.stdUtil, 2)) \ + * (math.exp(self.alpha * self.progress.get(int(time.time() * 1000))) - 1) \ + / (math.exp(self.alpha) - 1) + + if (self.utilThreshold < self.MIN_UTILITY): + self.utilThreshold = self.MIN_UTILITY + + return float(self.utilitySpace.getUtility(bid)) >= self.utilThreshold + + def calcOpValue(self, bid: Bid): + value: float = 0 + + issues = bid.getIssues() + valUtil: list = [0] * len(issues) + issWeght: list = [0] * len(issues) + k: int = 0 # index + + for s in issues: + p: Pair = self.freqMap[s] + v: Value = bid.getValue(s) + vs: str = self.valueToStr(v, p) + + # calculate utility of value (in the issue) + sumOfValues: int = 0 + maxValue: int = 1 + for vString in p.vList.keys(): + sumOfValues += p.vList[vString] + maxValue = max(maxValue, p.vList[vString]) + + # calculate estimated utility of the issuevalue + valUtil[k] = p.vList.get(vs) / maxValue + + # calculate the inverse std deviation of the array + mean: float = sumOfValues / len(p.vList) + for vString in p.vList.keys(): + issWeght[k] += pow(p.vList.get(vString) - mean, 2) + issWeght[k] = 1.0 / math.sqrt((issWeght[k] + 0.1) / len(p.vList)) + + k += 1 + + sumOfWght: float = 0 + for k in range(len(issues)): + value += valUtil[k] * issWeght[k] + sumOfWght += issWeght[k] + + return value / sumOfWght + + def isOpGood(self, bid: Bid): + if bid == None: + return False + + value: float = self.calcOpValue(bid) + index: int = int(((tSplit - 1) / (1 - tPhase) * (self.progress.get(int( + time.time() * 1000)) - tPhase))) + # change + opThreshold: float = max(max(2 * self.opThreshold[index] - 1, self.opReject[index]), + 0.2) if self.opThreshold != None and self.opReject != None else 0.6 + return value > opThreshold + + def updateFreqMap(self, bid: Bid): + if not (bid == None): + issues = bid.getIssues() + + for s in issues: + p: Pair = self.freqMap.get(s) + v: Value = bid.getValue(s) + + vs: str = self.valueToStr(v, p) + p.vList[vs] = (p.vList.get(vs) + 1) + + def valueToStr(self, v: Value, p: Pair): + v_str: str = "" + if p.type == 0: + v_str = cast(DiscreteValue, v).getValue() + elif p.type == 1: + v_str = cast(NumberValue, v).getValue() + + if v_str == "": + print("Warning: Value wasn't found") + return v_str + + def getPath(self, dataType: str, opponentName: str): + return os.path.join(self.storage_dir, dataType + "_" + opponentName + ".json") + + def updateAndLoadLearnedData(self): + # we didn't meet this opponent before + if exists(self.negotiationDataPath): + try: + # Load the negotiation data object of a previous negotiation + with open(self.negotiationDataPath, "r") as f: + negotiationData: NegotiationData = NegotiationData() + negotiationData.encode(list(json.load(f).values())) + + except: + self.logger.log(logging.ERROR, "Negotiation data does not exist") + + if exists(self.learnedDataPath): + try: + # Load the negotiation data object of a previous negotiation + with open(self.learnedDataPath, "r") as f: + self.learnedData = LearnedData() + self.learnedData.encode(list(json.load(f).values())) + + except: + self.logger.log(logging.ERROR, "learned data does not exist") + + else: + self.learnedData = LearnedData() + + # Process the negotiation data in our learned Data + self.learnedData.update(negotiationData) + self.avgUtil = self.learnedData.getAvgUtility() + self.stdUtil = self.learnedData.getStdUtility() diff --git a/agents_test/learning_agent/report.pdf b/agents_test/learning_agent/report.pdf new file mode 100644 index 00000000..9612c083 Binary files /dev/null and b/agents_test/learning_agent/report.pdf differ diff --git a/agents_test/linear_agent/linear_agent.py b/agents_test/linear_agent/linear_agent.py new file mode 100644 index 00000000..a09e61fd --- /dev/null +++ b/agents_test/linear_agent/linear_agent.py @@ -0,0 +1,23 @@ +from agents.time_dependent_agent.time_dependent_agent import TimeDependentAgent +from tudelft_utilities_logging.Reporter import Reporter + + +class LinearAgent(TimeDependentAgent): + """ + A simple party that places random bids and accepts when it receives an offer + with sufficient utility. + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + + # Override + def getDescription(self) -> str: + return ( + "Linear: concedes linearly with time. " + + "Parameters minPower (default 1) and maxPower (default infinity) are used when voting" + ) + + # Override + def getE(self) -> float: + return 1.0 diff --git a/agents_test/random_agent/random_agent.py b/agents_test/random_agent/random_agent.py new file mode 100644 index 00000000..f70299a2 --- /dev/null +++ b/agents_test/random_agent/random_agent.py @@ -0,0 +1,141 @@ +import logging +from random import randint +import traceback +from typing import cast, Dict, List, Set, Collection + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.LearningDone import LearningDone +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.actions.Vote import Vote +from geniusweb.actions.Votes import Votes +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.OptIn import OptIn +from geniusweb.inform.Settings import Settings +from geniusweb.inform.Voting import Voting +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value +from geniusweb.issuevalue.ValueSet import ValueSet +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressRounds import ProgressRounds +from geniusweb.utils import val + + +class RandomAgent(DefaultParty): + """ + Offers random bids until a bid with sufficient utility is offered. + """ + + def __init__(self): + super().__init__() + self.getReporter().log(logging.INFO, "party is initialized") + self._profile = None + self._lastReceivedBid: Bid = None + + # Override + def notifyChange(self, info: Inform): + # self.getReporter().log(logging.INFO,"received info:"+str(info)) + if isinstance(info, Settings): + self._settings: Settings = cast(Settings, info) + self._me = self._settings.getID() + self._protocol: str = str(self._settings.getProtocol().getURI()) + self._progress = self._settings.getProgress() + if "Learn" == self._protocol: + self.getConnection().send(LearningDone(self._me)) # type:ignore + else: + self._profile = ProfileConnectionFactory.create( + info.getProfile().getURI(), self.getReporter() + ) + elif isinstance(info, ActionDone): + action: Action = cast(ActionDone, info).getAction() + if isinstance(action, Offer): + self._lastReceivedBid = cast(Offer, action).getBid() + elif isinstance(info, YourTurn): + self._myTurn() + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + elif isinstance(info, Finished): + self.terminate() + elif isinstance(info, Voting): + # MOPAC protocol + self._lastvotes = self._vote(cast(Voting, info)) + val(self.getConnection()).send(self._lastvotes) + elif isinstance(info, OptIn): + val(self.getConnection()).send(self._lastvotes) + else: + self.getReporter().log( + logging.WARNING, "Ignoring unknown info " + str(info) + ) + + # Override + def getCapabilities(self) -> Capabilities: + return Capabilities( + set(["SAOP", "Learn", "MOPAC"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + # Override + def getDescription(self) -> str: + return "Offers random bids until a bid with sufficient utility is offered. Parameters minPower and maxPower can be used to control voting behaviour." + + # Override + def terminate(self): + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self._profile != None: + self._profile.close() + self._profile = None + + def _myTurn(self): + if self._isGood(self._lastReceivedBid): + action = Accept(self._me, self._lastReceivedBid) + else: + for _attempt in range(20): + bid = self._getRandomBid(self._profile.getProfile().getDomain()) + if self._isGood(bid): + break + action = Offer(self._me, bid) + self.getConnection().send(action) + + def _isGood(self, bid: Bid) -> bool: + if bid == None: + return False + profile = self._profile.getProfile() + if isinstance(profile, UtilitySpace): + return profile.getUtility(bid) > 0.6 + raise Exception("Can not handle this type of profile") + + def _getRandomBid(self, domain: Domain) -> Bid: + allBids = AllBidsList(domain) + return allBids.get(randint(0, allBids.size() - 1)) + + def _vote(self, voting: Voting) -> Votes: + """ + @param voting the {@link Voting} object containing the options + + @return our next Votes. + """ + val = self._settings.getParameters().get("minPower") + minpower: int = val if isinstance(val, int) else 2 + val = self._settings.getParameters().get("maxPower") + maxpower: int = val if isinstance(val, int) else 9999999 + + votes: Set[Vote] = set( + [ + Vote(self._me, offer.getBid(), minpower, maxpower) + for offer in voting.getOffers() + if self._isGood(offer.getBid()) + ] + ) + return Votes(self._me, votes) diff --git a/agents_test/storage_dir/Agent007/data.md b/agents_test/storage_dir/Agent007/data.md new file mode 100644 index 00000000..ca62b931 --- /dev/null +++ b/agents_test/storage_dir/Agent007/data.md @@ -0,0 +1 @@ +Data for learning (see README.md) \ No newline at end of file diff --git a/agents_test/storage_dir/Agent68/data.md b/agents_test/storage_dir/Agent68/data.md new file mode 100644 index 00000000..ca62b931 --- /dev/null +++ b/agents_test/storage_dir/Agent68/data.md @@ -0,0 +1 @@ +Data for learning (see README.md) \ No newline at end of file diff --git a/agents_test/storage_dir/Agent68Our/data.md b/agents_test/storage_dir/Agent68Our/data.md new file mode 100644 index 00000000..ca62b931 --- /dev/null +++ b/agents_test/storage_dir/Agent68Our/data.md @@ -0,0 +1 @@ +Data for learning (see README.md) \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_agent68_agent68_Agent68.json b/agents_test/storage_dir/ChargingBoul/agents_agent68_agent68_Agent68.json new file mode 100644 index 00000000..292a5cde --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_agent68_agent68_Agent68.json @@ -0,0 +1 @@ +{"ubi": 3, "aui": 7} \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_test_agent007_agent007_Agent007.json b/agents_test/storage_dir/ChargingBoul/agents_test_agent007_agent007_Agent007.json new file mode 100644 index 00000000..eb2a4491 --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_test_agent007_agent007_Agent007.json @@ -0,0 +1 @@ +{"ubi": 0, "aui": 2} \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_test_agent24_agent24_Agent24.json b/agents_test/storage_dir/ChargingBoul/agents_test_agent24_agent24_Agent24.json new file mode 100644 index 00000000..c2f70948 --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_test_agent24_agent24_Agent24.json @@ -0,0 +1 @@ +{"ubi": 0, "aui": 0} \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_test_agent68_agent68_Agent68.json b/agents_test/storage_dir/ChargingBoul/agents_test_agent68_agent68_Agent68.json new file mode 100644 index 00000000..bc41d894 --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_test_agent68_agent68_Agent68.json @@ -0,0 +1 @@ +{"ubi": 3, "aui": 3} \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_test_boulware_agent_boulware_agent_BoulwareAgent.json b/agents_test/storage_dir/ChargingBoul/agents_test_boulware_agent_boulware_agent_BoulwareAgent.json new file mode 100644 index 00000000..f3d439c6 --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_test_boulware_agent_boulware_agent_BoulwareAgent.json @@ -0,0 +1 @@ +{"ubi": 7, "aui": 4} \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_test_conceder_agent_conceder_agent_ConcederAgent.json b/agents_test/storage_dir/ChargingBoul/agents_test_conceder_agent_conceder_agent_ConcederAgent.json new file mode 100644 index 00000000..8c17de60 --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_test_conceder_agent_conceder_agent_ConcederAgent.json @@ -0,0 +1 @@ +{"ubi": 4, "aui": 3} \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_test_dreamteam109_agent_dreamteam109_agent_DreamTeam109Agent.json b/agents_test/storage_dir/ChargingBoul/agents_test_dreamteam109_agent_dreamteam109_agent_DreamTeam109Agent.json new file mode 100644 index 00000000..04d36d0f --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_test_dreamteam109_agent_dreamteam109_agent_DreamTeam109Agent.json @@ -0,0 +1 @@ +{"ubi": 0, "aui": 1} \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_test_linear_agent_linear_agent_LinearAgent.json b/agents_test/storage_dir/ChargingBoul/agents_test_linear_agent_linear_agent_LinearAgent.json new file mode 100644 index 00000000..67cd3195 --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_test_linear_agent_linear_agent_LinearAgent.json @@ -0,0 +1 @@ +{"ubi": 9, "aui": 5} \ No newline at end of file diff --git a/agents_test/storage_dir/ChargingBoul/agents_test_random_agent_random_agent_RandomAgent.json b/agents_test/storage_dir/ChargingBoul/agents_test_random_agent_random_agent_RandomAgent.json new file mode 100644 index 00000000..04d36d0f --- /dev/null +++ b/agents_test/storage_dir/ChargingBoul/agents_test_random_agent_random_agent_RandomAgent.json @@ -0,0 +1 @@ +{"ubi": 0, "aui": 1} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_agent68_agent68_Agent68.json b/agents_test/storage_dir/DreamTeam109Agent/agents_agent68_agent68_Agent68.json new file mode 100644 index 00000000..7ee88cbe --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_agent68_agent68_Agent68.json @@ -0,0 +1,76 @@ +{ + "sessions": [ + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.957161767578125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9341922922 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9585703125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9757534099 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.940762158203125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9341922922 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9471062255859375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9037133998 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9590896728515625, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9696269791 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.951909619140625, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9837 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.8091537109375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9988662832 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9158619140625, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9969699314 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9286119873046875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9363462253 + } + ] +} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent007_agent007_Agent007.json b/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent007_agent007_Agent007.json new file mode 100644 index 00000000..f4b614c8 --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent007_agent007_Agent007.json @@ -0,0 +1,36 @@ +{ + "sessions": [ + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9700472412109375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9696269791 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9710421142578125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9677895672 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9233310546875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9042363112 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.8208852783203126, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9170218941 + } + ] +} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent24_agent24_Agent24.json b/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent24_agent24_Agent24.json new file mode 100644 index 00000000..177520d9 --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent24_agent24_Agent24.json @@ -0,0 +1,36 @@ +{ + "sessions": [ + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.990175, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9402612499 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.990634912109375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9530274604 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 1, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9042363112 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 1, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.92239954 + } + ] +} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent68_agent68_Agent68.json b/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent68_agent68_Agent68.json new file mode 100644 index 00000000..d4c9d48e --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_test_agent68_agent68_Agent68.json @@ -0,0 +1,36 @@ +{ + "sessions": [ + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.99531298828125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.936799756 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9915083984375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9446238176 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 1, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9295812855 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.990290283203125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9032796952 + } + ] +} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_test_boulware_agent_boulware_agent_BoulwareAgent.json b/agents_test/storage_dir/DreamTeam109Agent/agents_test_boulware_agent_boulware_agent_BoulwareAgent.json new file mode 100644 index 00000000..a0f932d9 --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_test_boulware_agent_boulware_agent_BoulwareAgent.json @@ -0,0 +1,36 @@ +{ + "sessions": [ + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.933292138671875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9472565835 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.91854296875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9446238176 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.9102955078125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9030149314 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.848887451171875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9365422407 + } + ] +} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_test_charging_boul_charging_boul_ChargingBoul.json b/agents_test/storage_dir/DreamTeam109Agent/agents_test_charging_boul_charging_boul_ChargingBoul.json new file mode 100644 index 00000000..5323c5ed --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_test_charging_boul_charging_boul_ChargingBoul.json @@ -0,0 +1,36 @@ +{ + "sessions": [ + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": false, + "progressAtFinish": 1, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": false, + "progressAtFinish": 0.9439434326171875, + "topBidsPercentage": 0.01, + "utilityAtFinish": 0.5282021763 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.926807763671875, + "topBidsPercentage": 0.01, + "utilityAtFinish": 0.9437391852 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": false, + "progressAtFinish": 0.9915614990234375, + "topBidsPercentage": 0.01, + "utilityAtFinish": 0.7119328585 + } + ] +} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_test_conceder_agent_conceder_agent_ConcederAgent.json b/agents_test/storage_dir/DreamTeam109Agent/agents_test_conceder_agent_conceder_agent_ConcederAgent.json new file mode 100644 index 00000000..c71bc48c --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_test_conceder_agent_conceder_agent_ConcederAgent.json @@ -0,0 +1,36 @@ +{ + "sessions": [ + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.45264306640625, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9037133998 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.303983740234375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.95814 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.2549241943359375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9042363112 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.1114242919921875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9712429215 + } + ] +} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_test_linear_agent_linear_agent_LinearAgent.json b/agents_test/storage_dir/DreamTeam109Agent/agents_test_linear_agent_linear_agent_LinearAgent.json new file mode 100644 index 00000000..2d98b242 --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_test_linear_agent_linear_agent_LinearAgent.json @@ -0,0 +1,36 @@ +{ + "sessions": [ + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.54376396484375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.95814 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.703678076171875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9472565835 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.350488720703125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9437391852 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.6235896484375, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.8939709073 + } + ] +} \ No newline at end of file diff --git a/agents_test/storage_dir/DreamTeam109Agent/agents_test_random_agent_random_agent_RandomAgent.json b/agents_test/storage_dir/DreamTeam109Agent/agents_test_random_agent_random_agent_RandomAgent.json new file mode 100644 index 00000000..a35d20a8 --- /dev/null +++ b/agents_test/storage_dir/DreamTeam109Agent/agents_test_random_agent_random_agent_RandomAgent.json @@ -0,0 +1,36 @@ +{ + "sessions": [ + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.83893251953125, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9871743368 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.1838621337890625, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9757534099 + }, + { + "didAccept": false, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.0158310546875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9365422407 + }, + { + "didAccept": true, + "forceAcceptAtRemainingTurns": 0, + "isGood": true, + "progressAtFinish": 0.799075732421875, + "topBidsPercentage": 0.0033333333333333335, + "utilityAtFinish": 0.9030149314 + } + ] +} \ No newline at end of file diff --git a/agents_test/stupid_agent/stupid_agent.py b/agents_test/stupid_agent/stupid_agent.py new file mode 100644 index 00000000..b7b38de7 --- /dev/null +++ b/agents_test/stupid_agent/stupid_agent.py @@ -0,0 +1,80 @@ +import json +import logging +import sys +from typing import Dict, Any +from typing import cast + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.Settings import Settings +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.utils import val + + +class StupidAgent(DefaultParty): + """ + A Stupid party that places empty bids because it can't download the profile, + and accepts the first incoming offer. + """ + + def __init__(self): + super().__init__() + self.getReporter().log(logging.INFO, "party is initialized") + self._profile = None + self._lastReceivedBid: Bid = None + + # Override + def notifyChange(self, info: Inform): + # self.getReporter().log(logging.INFO,"received info:"+str(info)) + if isinstance(info, Settings): + settings: Settings = cast(Settings, info) + self._me = settings.getID() + self._protocol = settings.getProtocol() + elif isinstance(info, ActionDone): + action: Action = cast(ActionDone, info).getAction() + if isinstance(action, Offer): + self._lastReceivedBid = cast(Offer, action).getBid() + elif isinstance(info, YourTurn): + # This is a stupid party + if self._lastReceivedBid != None: + self.getReporter().log(logging.INFO, "sending accept:") + accept = Accept(self._me, self._lastReceivedBid) + val(self.getConnection()).send(accept) + else: + # We have no clue about our profile + offer: Offer = Offer(self._me, Bid({})) + self.getReporter().log(logging.INFO, "sending empty offer:") + val(self.getConnection()).send(offer) + self.getReporter().log(logging.INFO, "sent empty offer:") + elif isinstance(info, Finished): + self.terminate() + else: + self.getReporter().log( + logging.WARNING, "Ignoring unknown info " + str(info) + ) + + # Override + def getCapabilities(self): # -> Capabilities + return Capabilities( + set(["SAOP"]), set(["geniusweb.profile.utilityspace.LinearAdditive"]) + ) + + # Override + def getDescription(self): + return "Offers null bids and accept other party's first bid" + + # Override + def terminate(self): + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self._profile != None: + self._profile.close() + self._profile = None diff --git a/agents_test/time_dependent_agent/extended_util_space.py b/agents_test/time_dependent_agent/extended_util_space.py new file mode 100644 index 00000000..abfec098 --- /dev/null +++ b/agents_test/time_dependent_agent/extended_util_space.py @@ -0,0 +1,79 @@ +from geniusweb.bidspace.BidsWithUtility import BidsWithUtility +from geniusweb.bidspace.Interval import Interval +from geniusweb.bidspace.IssueInfo import IssueInfo +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Value import Value +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from tudelft.utilities.immutablelist.ImmutableList import ImmutableList +from decimal import Decimal +from typing import List + + +class ExtendedUtilSpace: + """ + Inner class for TimeDependentParty, made public for testing purposes. This + class may change in the future, use at your own risk. + """ + + def __init__(self, space: LinearAdditive): + self._utilspace = space + self._bidutils = BidsWithUtility.create(self._utilspace) + self._computeMinMax() + self._tolerance = self._computeTolerance() + + def _computeMinMax(self): + """ + Computes the fields minutil and maxUtil. +

+ TODO this is simplistic, very expensive method and may cause us to run + out of time on large domains. +

+ Assumes that utilspace and bidutils have been set properly. + """ + range = self._bidutils.getRange() + self._minUtil = range.getMin() + self._maxUtil = range.getMax() + + rvbid = self._utilspace.getReservationBid() + if rvbid != None: + rv = self._utilspace.getUtility(rvbid) + if rv > self._minUtil: + self._minUtil = rv + + def _computeTolerance(self) -> Decimal: + """ + Tolerance is the Interval we need when searching bids. When we are close + to the maximum utility, this value has to be the distance between the + best and one-but-best utility. + + @return the minimum tolerance required, which is the minimum difference + between the weighted utility of the best and one-but-best issue + value. + """ + tolerance = Decimal(1) + for iss in self._bidutils.getInfo(): + if iss.getValues().size() > 1: + # we have at least 2 values. + values: List[Decimal] = [] + for val in iss.getValues(): + values.append(iss.getWeightedUtil(val)) + values.sort() + values.reverse() + tolerance = min(tolerance, values[0] - values[1]) + return tolerance + + def getMin(self) -> Decimal: + return self._minUtil + + def getMax(self) -> Decimal: + return self._maxUtil + + def getBids(self, utilityGoal: Decimal) -> ImmutableList[Bid]: + """ + @param utilityGoal the requested utility + @return bids with utility inside [utilitygoal-{@link #tolerance}, + utilitygoal] + """ + return self._bidutils.getBids( + Interval(utilityGoal - self._tolerance, utilityGoal) + ) diff --git a/agents_test/time_dependent_agent/time_dependent_agent.py b/agents_test/time_dependent_agent/time_dependent_agent.py new file mode 100644 index 00000000..03d7750b --- /dev/null +++ b/agents_test/time_dependent_agent/time_dependent_agent.py @@ -0,0 +1,316 @@ +import logging +from random import randint, random +import traceback +from typing import cast, Dict, List, Set, Collection + +from geniusweb.actions.Accept import Accept +from geniusweb.actions.Action import Action +from geniusweb.actions.LearningDone import LearningDone +from geniusweb.actions.Offer import Offer +from geniusweb.actions.PartyId import PartyId +from geniusweb.actions.Vote import Vote +from geniusweb.actions.Votes import Votes +from geniusweb.bidspace.AllBidsList import AllBidsList +from geniusweb.inform.ActionDone import ActionDone +from geniusweb.inform.Finished import Finished +from geniusweb.inform.Inform import Inform +from geniusweb.inform.OptIn import OptIn +from geniusweb.inform.Settings import Settings +from geniusweb.inform.Voting import Voting +from geniusweb.inform.YourTurn import YourTurn +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value +from geniusweb.issuevalue.ValueSet import ValueSet +from geniusweb.party.Capabilities import Capabilities +from geniusweb.party.DefaultParty import DefaultParty +from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace +from geniusweb.profileconnection.ProfileConnectionFactory import ( + ProfileConnectionFactory, +) +from geniusweb.progress.ProgressRounds import ProgressRounds +from geniusweb.utils import val +from geniusweb.profileconnection.ProfileInterface import ProfileInterface +from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive +from geniusweb.progress.Progress import Progress +from tudelft.utilities.immutablelist.ImmutableList import ImmutableList +from time import sleep, time as clock +from decimal import Decimal +import sys +from agents.time_dependent_agent.extended_util_space import ExtendedUtilSpace +from tudelft_utilities_logging.Reporter import Reporter + + +class TimeDependentAgent(DefaultParty): + """ + General time dependent party. +

+ Supports parameters as follows + + + + + + + + + + + + + + + + + + + + + + +
parameters
ee determines how fast the party makes concessions with time. Typically + around 1. 0 means no concession, 1 linear concession, >1 faster than + linear concession.
minPowerThis value is used as minPower for placed {@link Vote}s. Default value is + 1.
maxPowerThis value is used as maxPower for placed {@link Vote}s. Default value is + infinity.
delayThe average time in seconds to wait before responding to a YourTurn. The + actual waiting time will be random in [0.5*time, 1.5*time]. This can be used + to simulate human users that take thinking time.
+

+ TimeDependentParty requires a {@link UtilitySpace} + """ + + def __init__(self, reporter: Reporter = None): + super().__init__(reporter) + self._profileint: ProfileInterface = None # type:ignore + self._utilspace: LinearAdditive = None # type:ignore + self._me: PartyId = None # type:ignore + self._progress: Progress = None # type:ignore + self._lastReceivedBid: Bid = None # type:ignore + self._extendedspace: ExtendedUtilSpace = None # type:ignore + self._e: float = 1.2 + self._lastvotes: Votes = None # type:ignore + self._settings: Settings = None # type:ignore + self.getReporter().log(logging.INFO, "party is initialized") + + # Override + def getCapabilities(self) -> Capabilities: + return Capabilities( + set(["SAOP", "Learn", "MOPAC"]), + set(["geniusweb.profile.utilityspace.LinearAdditive"]), + ) + + # Override + def notifyChange(self, info: Inform): + try: + if isinstance(info, Settings): + self._settings = info + self._me = self._settings.getID() + self._progress = self._settings.getProgress() + newe = self._settings.getParameters().get("e") + if newe != None: + if isinstance(newe, float): + self._e = newe + else: + self.getReporter().log( + logging.WARNING, + "parameter e should be Double but found " + str(newe), + ) + protocol: str = str(self._settings.getProtocol().getURI()) + if "Learn" == protocol: + val(self.getConnection()).send(LearningDone(self._me)) + else: + self._profileint = ProfileConnectionFactory.create( + self._settings.getProfile().getURI(), self.getReporter() + ) + + elif isinstance(info, ActionDone): + otheract: Action = info.getAction() + if isinstance(otheract, Offer): + self._lastReceivedBid = otheract.getBid() + elif isinstance(info, YourTurn): + self._delayResponse() + self._myTurn() + elif isinstance(info, Finished): + self.getReporter().log(logging.INFO, "Final ourcome:" + str(info)) + self.terminate() + # stop this party and free resources. + elif isinstance(info, Voting): + lastvotes = self._vote(info) + val(self.getConnection()).send(lastvotes) + elif isinstance(info, OptIn): + val(self.getConnection()).send(lastvotes) + except Exception as ex: + self.getReporter().log(logging.CRITICAL, "Failed to handle info", ex) + self._updateRound(info) + + def getE(self) -> float: + """ + @return the E value that controls the party's behaviour. Depending on the + value of e, extreme sets show clearly different patterns of + behaviour [1]: + + 1. Boulware: For this strategy e < 1 and the initial offer is + maintained till time is almost exhausted, when the agent concedes + up to its reservation value. + + 2. Conceder: For this strategy e > 1 and the agent goes to its + reservation value very quickly. + + 3. When e = 1, the price is increased linearly. + + 4. When e = 0, the agent plays hardball. + """ + return self._e + + # Override + def getDescription(self) -> str: + return ( + "Time-dependent conceder. Aims at utility u(t) = scale * t^(1/e) " + + "where t is the time (0=start, 1=end), e is the concession speed parameter (default 1.1), and scale such that u(0)=minimum and " + + "u(1) = maximum possible utility. Parameters minPower (default 1) and maxPower (default infinity) are used " + + "when voting" + ) + + # Override + def terminate(self): + self.getReporter().log(logging.INFO, "party is terminating:") + super().terminate() + if self._profileint != None: + self._profileint.close() + self._profileint = None + + ##################### private support funcs ######################### + + def _updateRound(self, info: Inform): + """ + Update {@link #progress}, depending on the protocol and last received + {@link Inform} + + @param info the received info. + """ + if self._settings == None: # not yet initialized + return + protocol: str = str(self._settings.getProtocol().getURI()) + + if "SAOP" == protocol or "SHAOP" == protocol: + if not isinstance(info, YourTurn): + return + elif "MOPAC" == protocol: + if not isinstance(info, OptIn): + return + else: + return + # if we get here, round must be increased. + if isinstance(self._progress, ProgressRounds): + self._progress = self._progress.advance() + + def _myTurn(self): + self._updateUtilSpace() + bid = self._makeBid() + + myAction: Action + if bid == None or ( + self._lastReceivedBid != None + and self._utilspace.getUtility(self._lastReceivedBid) + >= self._utilspace.getUtility(bid) + ): + # if bid==null we failed to suggest next bid. + myAction = Accept(self._me, self._lastReceivedBid) + else: + myAction = Offer(self._me, bid) + self.getConnection().send(myAction) + + def _updateUtilSpace(self) -> LinearAdditive: # throws IOException + newutilspace = self._profileint.getProfile() + if not newutilspace == self._utilspace: + self._utilspace = cast(LinearAdditive, newutilspace) + self._extendedspace = ExtendedUtilSpace(self._utilspace) + return self._utilspace + + def _makeBid(self) -> Bid: + """ + @return next possible bid with current target utility, or null if no such + bid. + """ + time = self._progress.get(round(clock() * 1000)) + + utilityGoal = self._getUtilityGoal( + time, + self.getE(), + self._extendedspace.getMin(), + self._extendedspace.getMax(), + ) + options: ImmutableList[Bid] = self._extendedspace.getBids(utilityGoal) + if options.size() == 0: + # if we can't find good bid, get max util bid.... + options = self._extendedspace.getBids(self._extendedspace.getMax()) + # pick a random one. + return options.get(randint(0, options.size() - 1)) + + def _getUtilityGoal( + self, t: float, e: float, minUtil: Decimal, maxUtil: Decimal + ) -> Decimal: + """ + @param t the time in [0,1] where 0 means start of nego and 1 the + end of nego (absolute time/round limit) + @param e the e value that determinses how fast the party makes + concessions with time. Typically around 1. 0 means no + concession, 1 linear concession, >1 faster than linear + concession. + @param minUtil the minimum utility possible in our profile + @param maxUtil the maximum utility possible in our profile + @return the utility goal for this time and e value + """ + + ft1 = Decimal(1) + if e != 0: + ft1 = round(Decimal(1 - pow(t, 1 / e)), 6) # defaults ROUND_HALF_UP + return max(min((minUtil + (maxUtil - minUtil) * ft1), maxUtil), minUtil) + + def _vote(self, voting: Voting) -> Votes: # throws IOException + """ + @param voting the {@link Voting} object containing the options + + @return our next Votes. + """ + val = self._settings.getParameters().get("minPower") + # max utility requires smallest possible group/power + minpower = val if isinstance(val, int) else 1 + val = self._settings.getParameters().get("maxPower") + maxpower = val if isinstance(val, int) else sys.maxsize + + votes: Set[Vote] = { + Vote(self._me, offer.getBid(), minpower, maxpower) + for offer in set(voting.getOffers()) + if self._isGood(offer.getBid()) + } + + return Votes(self._me, votes) + + def _isGood(self, bid: Bid) -> bool: + """ + @param bid the bid to check + @return true iff bid is good for us. + """ + if bid == None or self._profileint == None: + return False + profile = cast(LinearAdditive, self._profileint.getProfile()) + # the profile MUST contain UtilitySpace + time = self._progress.get(round(clock() * 1000)) + return profile.getUtility(bid) >= self._getUtilityGoal( + time, + self.getE(), + self._extendedspace.getMin(), + self._extendedspace.getMax(), + ) + + def _delayResponse(self): # throws InterruptedException + """ + Do random delay of provided delay in seconds, randomized by factor in + [0.5, 1.5]. Does not delay if set to 0. + + @throws InterruptedException + """ + delay = self._settings.getParameters().getDouble("delay", 0, 0, 10000000) + if delay > 0: + sleep(delay * (0.5 + random())) diff --git a/baseline_test.py b/baseline_test.py new file mode 100644 index 00000000..ecc39029 --- /dev/null +++ b/baseline_test.py @@ -0,0 +1,87 @@ +import json +import os +from pathlib import Path +import random +import time + +from utils.runners import run_tournament + +RESULTS_DIR = Path("baseline_results", time.strftime('%Y%m%d-%H%M%S')) + +# create results directory if it does not exist +if not RESULTS_DIR.exists(): + RESULTS_DIR.mkdir(parents=True) + +numbers = [f"{i:02}" for i in range(50)] +random_selection = random.sample(numbers, 2) + +# Settings to run a negotiation session: +# You need to specify the classpath of 2 agents to start a negotiation. Parameters for the agent can be added as a dict (see example) +# You need to specify the preference profiles for both agents. The first profile will be assigned to the first agent. +# You need to specify a time deadline (is milliseconds (ms)) we are allowed to negotiate before we end without agreement. +tournament_settings = { + "agents": [ + { + "name" : "Agent007", + "class": "agents_test.agent007.agent007.Agent007", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent007"}, + }, + { + "name" : "Agent55", + "class": "agents_test.agent55.agent55.Agent55", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent55"}, + }, + { + "name" : "BoulwareAgent", + "class": "agents_test.boulware_agent.boulware_agent.BoulwareAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/BoulwareAgent"}, + }, + { + "name" : "ConcederAgent", + "class": "agents_test.conceder_agent.conceder_agent.ConcederAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/ConcederAgent"}, + }, + { + "name" : "DreamTeam109Agent", + "class": "agents_test.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agents_test/storage_dir/DreamTeam109Agent"}, + }, + { + "name" : "LinearAgent", + "class": "agents_test.linear_agent.linear_agent.LinearAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/LinearAgent"}, + }, + { + "name" : "RandomAgent", + "class": "agents_test.random_agent.random_agent.RandomAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/RandomAgent"}, + }, + { + "name" : "StupidAgent", + "class": "agents_test.stupid_agent.stupid_agent.StupidAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/StupidAgent"}, + }, + { + "name" : "ChargingBoul", + "class": "agents_test.charging_boul.charging_boul.ChargingBoul", + "parameters": {"storage_dir": "agents_test/storage_dir/ChargingBoul"}, + } + ], + "profile_sets": [ + ["domains/domain" + random_selection[0] + "/profileA.json", "domains/domain" + random_selection[0] + "/profileB.json"], + ["domains/domain" + random_selection[1] + "/profileA.json", "domains/domain" + random_selection[1] + "/profileB.json"], + ], + "deadline_time_ms": 10000, +} + +# run a session and obtain results in dictionaries +tournament_steps, tournament_results, tournament_results_summary = run_tournament(tournament_settings) + +# save the tournament settings for reference +with open(RESULTS_DIR.joinpath("tournament_steps.json"), "w", encoding="utf-8") as f: + f.write(json.dumps(tournament_steps, indent=2)) +# save the tournament results +with open(RESULTS_DIR.joinpath("tournament_results.json"), "w", encoding="utf-8") as f: + f.write(json.dumps(tournament_results, indent=2)) +# save the tournament results summary +tournament_results_summary.to_csv(RESULTS_DIR.joinpath("tournament_results_summary.csv")) diff --git a/final_test.py b/final_test.py new file mode 100644 index 00000000..f0536410 --- /dev/null +++ b/final_test.py @@ -0,0 +1,142 @@ +import json +import os +from pathlib import Path +import random +import time + +from utils.runners import run_tournament + +RESULTS_DIR = Path("final_results", time.strftime('%Y%m%d-%H%M%S')) + +# create results directory if it does not exist +if not RESULTS_DIR.exists(): + RESULTS_DIR.mkdir(parents=True) + +numbers = [f"{i:02}" for i in range(50)] +random_selection = random.sample(numbers, 2) + +# Settings to run a negotiation session: +# You need to specify the classpath of 2 agents to start a negotiation. Parameters for the agent can be added as a dict (see example) +# You need to specify the preference profiles for both agents. The first profile will be assigned to the first agent. +# You need to specify a time deadline (is milliseconds (ms)) we are allowed to negotiate before we end without agreement. +tournament_settings = { + "agents": [ + { + "name" : "Agent68", + "class": "agents.agent68.agent68.Agent68", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent68"}, + }, + { + "name" : "Agent007", + "class": "agents_test.agent007.agent007.Agent007", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent007"}, + }, + { + "name" : "Agent24", + "class": "agents_test.agent24.agent24.Agent24", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent24"}, + }, + { + "name" : "Agent55", + "class": "agents_test.agent55.agent55.Agent55", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent55"}, + }, + { + "name" : "BoulwareAgent", + "class": "agents_test.boulware_agent.boulware_agent.BoulwareAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/BoulwareAgent"}, + }, + { + "name" : "ConcederAgent", + "class": "agents_test.conceder_agent.conceder_agent.ConcederAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/ConcederAgent"}, + }, + { + "name" : "DreamTeam109Agent", + "class": "agents_test.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agents_test/storage_dir/DreamTeam109Agent"}, + }, + { + "name" : "LinearAgent", + "class": "agents_test.linear_agent.linear_agent.LinearAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/LinearAgent"}, + }, + { + "name" : "RandomAgent", + "class": "agents_test.random_agent.random_agent.RandomAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/RandomAgent"}, + }, + { + "name" : "StupidAgent", + "class": "agents_test.stupid_agent.stupid_agent.StupidAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/StupidAgent"}, + }, + { + "name" : "ChargingBoul", + "class": "agents_test.charging_boul.charging_boul.ChargingBoul", + "parameters": {"storage_dir": "agents_test/storage_dir/ChargingBoul"}, + }, + { + "name" : "HardLiner", + "class": "agents_test.hardliner_agent.hardliner_agent.HardlinerAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/HardlinerAgent"}, + }, + { + "name" : "TimeDependentAgent", + "class": "agents_test.time_dependent_agent.time_dependent_agent.TimeDependentAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/TimeDependentAgent"}, + }, + { + "name" : "CompromisingAgent", + "class": "agents_test.compromising_agent.compromising_agent.CompromisingAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/CompromisingAgent"}, + }, + { + "name" : "LearningAgent", + "class": "agents_test.learning_agent.learning_agent.LearningAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/LearningAgent"}, + }, + { + "name" : "agentfish", + "class": "agents_test.agentfish.agentfish.AgentFish", + "parameters": {"storage_dir": "agents_test/storage_dir/AgentFish"}, + }, + { + "name" : "Agent3", + "class": "agents_test.agent3.agent3.Agent3", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent3"}, + }, + { + "name" : "Agent32", + "class": "agents_test.agent32.agent32.Agent32", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent32"}, + }, + { + "name" : "Agent61", + "class": "agents_test.agent61.agent61.Agent61", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent61"}, + }, + { + "name" : "Agent67", + "class": "agents_test.agent67.agent67.Agent67", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent67"}, + } + ], + "profile_sets": [ + ["domains/domain" + random_selection[0] + "/profileA.json", "domains/domain" + random_selection[0] + "/profileB.json"], + ["domains/domain" + random_selection[1] + "/profileA.json", "domains/domain" + random_selection[1] + "/profileB.json"], + ], + "deadline_time_ms": 10000, +} + +# run a session and obtain results in dictionaries +tournament_steps, tournament_results, tournament_results_summary = run_tournament(tournament_settings) + +# save the tournament settings for reference +with open(RESULTS_DIR.joinpath("tournament_steps.json"), "w", encoding="utf-8") as f: + f.write(json.dumps(tournament_steps, indent=2)) +# save the tournament results +with open(RESULTS_DIR.joinpath("tournament_results.json"), "w", encoding="utf-8") as f: + f.write(json.dumps(tournament_results, indent=2)) +# save the tournament results summary +tournament_results_summary.to_csv(RESULTS_DIR.joinpath("tournament_results_summary.csv")) diff --git a/plot_steps.py b/plot_steps.py new file mode 100644 index 00000000..1e3faef3 --- /dev/null +++ b/plot_steps.py @@ -0,0 +1,70 @@ +import json +import matplotlib.pyplot as plt + +def plot_bids(filename, output_file="bids_plot.png"): + print(f"Processing file: {filename}") + print(f"Saving plot as: {output_file}") + + # Load JSON data + with open(filename, 'r') as file: + data = json.load(file) + + agents = set() + for action in data["actions"]: + if "Offer" in action: + agents.update(action["Offer"]["utilities"].keys()) + + agent1, agent2 = list(agents)[:2] # Extract first two agents dynamically + + agent1_utilities_Agent1 = [] + agent2_utilities_Agent1 = [] + agent1_utilities_Agent2 = [] + agent2_utilities_Agent2 = [] + + # Extract utilities from each offer + for action in data["actions"]: + if "Offer" in action: + utilities = action["Offer"]["utilities"] + agent = action["Offer"]["actor"] + + if agent == agent1: + agent1_utilities_Agent1.append(utilities[agent1]) + agent2_utilities_Agent1.append(utilities[agent2]) + else: + agent1_utilities_Agent2.append(utilities[agent1]) + agent2_utilities_Agent2.append(utilities[agent2]) + + # Plot the utilities + plt.figure(figsize=(8, 6)) + + # Connect dots with lines for Agent1 + plt.plot(agent1_utilities_Agent1, agent2_utilities_Agent1, color='blue', label=f'{agent1} Bids', marker='o') + # Connect dots with lines for Agent2 + plt.plot(agent1_utilities_Agent2, agent2_utilities_Agent2, color='red', label=f'{agent2} Bids', marker='o') + + # Highlight last bid and connect it to both sides + if agent1_utilities_Agent1 or agent1_utilities_Agent2: + last_agent1 = agent1_utilities_Agent1[-1] if agent1_utilities_Agent1 else agent1_utilities_Agent2[-1] + last_agent2 = agent2_utilities_Agent1[-1] if agent2_utilities_Agent1 else agent2_utilities_Agent2[-1] + + # Draw line to connect last bid to both sides + if agent1_utilities_Agent1: + plt.plot([agent1_utilities_Agent1[-2], last_agent1], [agent2_utilities_Agent1[-2], last_agent2], color='blue') + if agent1_utilities_Agent2: + plt.plot([agent1_utilities_Agent2[-2], last_agent1], [agent2_utilities_Agent2[-2], last_agent2], color='red') + + # Plot the last bid with a larger circle + plt.scatter(last_agent1, last_agent2, color='green', s=100, edgecolors='black', label='Last Bid') + + plt.xlabel(f"{agent1} Utility") + plt.ylabel(f"{agent2} Utility") + plt.title("Bids Over Time") + plt.legend(loc='lower left') + plt.grid() + + # Save plot to file + plt.savefig(output_file) + plt.show() + +# Use this to divert the path +plot_bids("results/20250327-163512/session_results_trace.json") diff --git a/regular_test.py b/regular_test.py new file mode 100644 index 00000000..bf24040c --- /dev/null +++ b/regular_test.py @@ -0,0 +1,156 @@ +from collections import defaultdict +import csv +import glob +import json +import os +import random +import shutil +import time +from pathlib import Path + +from matplotlib import pyplot as plt +import pandas as pd + +from utils.plot_trace import plot_trace +from utils.runners import run_session + +RESULTS_DIR = Path("regular_results", time.strftime('%Y%m%d-%H%M%S')) + +# create results directory if it does not exist +if not RESULTS_DIR.exists(): + RESULTS_DIR.mkdir(parents=True) + +# Settings to run a negotiation session: +# You need to specify the classpath of 2 agents to start a negotiation. Parameters for the agent can be added as a dict (see example) +# You need to specify the preference profiles for both agents. The first profile will be assigned to the first agent. +# You need to specify a time deadline (is milliseconds (ms)) we are allowed to negotiate before we end without agreement + +our_agent = { + "name" : "Agent68", + "class": "agents.agent68.agent68.Agent68", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent68"}, + } +agents = [ + { + "name" : "Agent007", + "class": "agents_test.agent007.agent007.Agent007", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent007"}, + }, + { + "name" : "Agent24", + "class": "agents_test.agent24.agent24.Agent24", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent24"}, + }, + { + "name" : "Agent55", + "class": "agents_test.agent55.agent55.Agent55", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent55"}, + }, + { + "name" : "BoulwareAgent", + "class": "agents_test.boulware_agent.boulware_agent.BoulwareAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/BoulwareAgent"}, + }, + { + "name" : "ConcederAgent", + "class": "agents_test.conceder_agent.conceder_agent.ConcederAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/ConcederAgent"}, + }, + { + "name" : "DreamTeam109Agent", + "class": "agents_test.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agents_test/storage_dir/DreamTeam109Agent"}, + }, + { + "name" : "LinearAgent", + "class": "agents_test.linear_agent.linear_agent.LinearAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/LinearAgent"}, + }, + { + "name" : "RandomAgent", + "class": "agents_test.random_agent.random_agent.RandomAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/RandomAgent"}, + }, + { + "name" : "StupidAgent", + "class": "agents_test.stupid_agent.stupid_agent.StupidAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/StupidAgent"}, + }, + { + "name" : "ChargingBoul", + "class": "agents_test.charging_boul.charging_boul.ChargingBoul", + "parameters": {"storage_dir": "agents_test/storage_dir/ChargingBoul"}, + } +] + +numbers = [f"{i:02}" for i in range(50)] +random_selection = random.sample(numbers, 2) + +overall_results = [] +for i in range(len(agents)): + for j in range(len(random_selection)): + settings = { + "agents": [ + our_agent, + agents[i], + ], + "profiles": ["domains/domain" + random_selection[j] + "/profileA.json", "domains/domain" + random_selection[j] + "/profileB.json"], + "deadline_time_ms": 10000, + } + + session_results_trace, session_results_summary = run_session(settings) + last_round_results = { + "agent_1": session_results_summary["agent_" + str(i * 4 + j * 2 + 1)], + "agent_2": session_results_summary["agent_" + str(i * 4 + j * 2 + 2)], + "utility_1": session_results_summary["utility_" + str(i * 4 + j * 2 + 1)], + "utility_2": session_results_summary["utility_" + str(i * 4 + j * 2 + 2)], + "nash_product": session_results_summary["nash_product"], + "social_welfare": session_results_summary["social_welfare"], + "agreement": session_results_summary["result"], + } + overall_results.append(last_round_results) + +aggregated_results = defaultdict(lambda: { + "utility_1": 0, "utility_2": 0, "nash_product": 0, + "social_welfare": 0, "agreement_count": 0, "count": 0 +}) + +# Aggregate values +for result in overall_results: + key = (result["agent_1"], result["agent_2"]) + aggregated_results[key]["utility_1"] += result["utility_1"] + aggregated_results[key]["utility_2"] += result["utility_2"] + aggregated_results[key]["nash_product"] += result["nash_product"] + aggregated_results[key]["social_welfare"] += result["social_welfare"] + aggregated_results[key]["agreement_count"] += 1 if result["agreement"] == "agreement" else 0 # Convert to int for counting + aggregated_results[key]["count"] += 1 + +# Compute averages and format final results +final_results = [] +for (agent_1, agent_2), values in aggregated_results.items(): + count = values["count"] + final_results.append({ + "agent_1": agent_1, + "agent_2": agent_2, + "utility_1": values["utility_1"] / count, + "utility_2": values["utility_2"] / count, + "nash_product": values["nash_product"] / count, + "social_welfare": values["social_welfare"] / count, + "agreement_count": values["agreement_count"], # Total agreements + }) + +# write results to file + +df = pd.DataFrame(final_results) + +average_row = df.mean(numeric_only=True).to_dict() +average_row["agent_1"] = "Average" +average_row["agent_2"] = "Average" + +# Append the average row to the DataFrame +df = pd.concat([df, pd.DataFrame([average_row])], ignore_index=True) + +# Save DataFrame to CSV +df.to_csv(RESULTS_DIR.joinpath("regular_results_summary.csv"), index=False) + + diff --git a/run_tournament.py b/run_tournament.py index 41dd7242..46582c4a 100644 --- a/run_tournament.py +++ b/run_tournament.py @@ -18,102 +18,55 @@ tournament_settings = { "agents": [ { - "class": "agents.template_agent.template_agent.TemplateAgent", - "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, + "name" : "Agent007", + "class": "agents_test.agent007.agent007.Agent007", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent007"}, }, { - "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", + "name" : "Agent24", + "class": "agents_test.agent24.agent24.Agent24", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent24"}, }, { - "class": "agents.conceder_agent.conceder_agent.ConcederAgent", + "name" : "Agent68", + "class": "agents_test.agent68.agent68.Agent68", + "parameters": {"storage_dir": "agents_test/storage_dir/Agent68"}, }, { - "class": "agents.hardliner_agent.hardliner_agent.HardlinerAgent", + "name" : "BoulwareAgent", + "class": "agents_test.boulware_agent.boulware_agent.BoulwareAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/BoulwareAgent"}, }, { - "class": "agents.linear_agent.linear_agent.LinearAgent", + "name" : "ConcederAgent", + "class": "agents_test.conceder_agent.conceder_agent.ConcederAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/ConcederAgent"}, }, { - "class": "agents.random_agent.random_agent.RandomAgent", + "name" : "DreamTeam109Agent", + "class": "agents_test.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agents_test/storage_dir/DreamTeam109Agent"}, }, { - "class": "agents.stupid_agent.stupid_agent.StupidAgent", + "name" : "LinearAgent", + "class": "agents_test.linear_agent.linear_agent.LinearAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/LinearAgent"}, }, { - "class": "agents.CSE3210.agent2.agent2.Agent2", + "name" : "RandomAgent", + "class": "agents_test.random_agent.random_agent.RandomAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/RandomAgent"}, }, { - "class": "agents.CSE3210.agent3.agent3.Agent3", + "name" : "StupidAgent", + "class": "agents_test.stupid_agent.stupid_agent.StupidAgent", + "parameters": {"storage_dir": "agents_test/storage_dir/StupidAgent"}, }, { - "class": "agents.CSE3210.agent7.agent7.Agent7", - }, - { - "class": "agents.CSE3210.agent11.agent11.Agent11", - }, - { - "class": "agents.CSE3210.agent14.agent14.Agent14", - }, - { - "class": "agents.CSE3210.agent18.agent18.Agent18", - }, - { - "class": "agents.CSE3210.agent19.agent19.Agent19", - }, - { - "class": "agents.CSE3210.agent22.agent22.Agent22", - }, - { - "class": "agents.CSE3210.agent24.agent24.Agent24", - }, - { - "class": "agents.CSE3210.agent25.agent25.Agent25", - }, - { - "class": "agents.CSE3210.agent26.agent26.Agent26", - }, - { - "class": "agents.CSE3210.agent27.agent27.Agent27", - }, - { - "class": "agents.CSE3210.agent29.agent29.Agent29", - }, - { - "class": "agents.CSE3210.agent32.agent32.Agent32", - }, - { - "class": "agents.CSE3210.agent33.agent33.Agent33", - }, - { - "class": "agents.CSE3210.agent41.agent41.Agent41", - }, - { - "class": "agents.CSE3210.agent43.agent43.Agent43", - }, - { - "class": "agents.CSE3210.agent50.agent50.Agent50", - }, - { - "class": "agents.CSE3210.agent52.agent52.Agent52", - }, - { - "class": "agents.CSE3210.agent55.agent55.Agent55", - }, - { - "class": "agents.CSE3210.agent58.agent58.Agent58", - }, - { - "class": "agents.CSE3210.agent61.agent61.Agent61", - }, - { - "class": "agents.CSE3210.agent64.agent64.Agent64", - }, - { - "class": "agents.CSE3210.agent67.agent67.Agent67", - }, - { - "class": "agents.CSE3210.agent68.agent68.Agent68", - }, + "name" : "ChargingBoul", + "class": "agents_test.charging_boul.charging_boul.ChargingBoul", + "parameters": {"storage_dir": "agents_test/storage_dir/ChargingBoul"}, + } ], "profile_sets": [ ["domains/domain00/profileA.json", "domains/domain00/profileB.json"], diff --git a/submission.zip b/submission.zip new file mode 100644 index 00000000..5d027399 Binary files /dev/null and b/submission.zip differ