1 | from decimal import Decimal
|
---|
2 | from typing import Optional, Dict
|
---|
3 |
|
---|
4 | from geniusweb.actions.Action import Action
|
---|
5 | from geniusweb.issuevalue.Bid import Bid
|
---|
6 | from geniusweb.issuevalue.Domain import Domain
|
---|
7 | from geniusweb.issuevalue.Value import Value
|
---|
8 | from geniusweb.opponentmodel.OpponentModel import OpponentModel
|
---|
9 | from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
|
---|
10 | from geniusweb.progress.Progress import Progress
|
---|
11 | from geniusweb.references.Parameters import Parameters
|
---|
12 |
|
---|
13 |
|
---|
14 | class MyOpponentModel(UtilitySpace, OpponentModel):
|
---|
15 |
|
---|
16 | def __init__(self, domain: Domain, frequencies: [str, Dict[Value, int]], total: int, resBid: Optional[Bid]):
|
---|
17 | """
|
---|
18 | Initializes the opponent model with the given parameters.
|
---|
19 | @param domain the domain of the negotiation
|
---|
20 | @param frequencies the frequencies dictionary
|
---|
21 | @param total the total amount of bids that have been recorded in the opponent model
|
---|
22 | """
|
---|
23 | self.domain = domain
|
---|
24 | self.frequencies = frequencies
|
---|
25 | self.total = total
|
---|
26 | self.resBid = resBid
|
---|
27 |
|
---|
28 | # amount of unique bids, we will stop learning after this amount
|
---|
29 | self.stop_learning_after = 30
|
---|
30 | self.seen_bids = set()
|
---|
31 |
|
---|
32 | def With(self, domain: Domain, resBid: Optional[Bid]) -> "OpponentModel":
|
---|
33 | return MyOpponentModel(domain=domain, frequencies={iss: {} for iss in domain.getIssues()}, total=0,
|
---|
34 | resBid=resBid)
|
---|
35 |
|
---|
36 | def WithAction(self, action: Action, progress: Progress) -> "OpponentModel":
|
---|
37 | """
|
---|
38 | Updates the opponent model with the given action.
|
---|
39 | @param action most recent action to update the model with
|
---|
40 | @param progress the progress of the negotiation expressed as a percentage of the amount of rounds
|
---|
41 | that have passed
|
---|
42 | @return The updated opponent model.
|
---|
43 | """
|
---|
44 | bid: Bid = action.getBid()
|
---|
45 | if not bid:
|
---|
46 | raise ValueError('No bid provided!')
|
---|
47 | self.seen_bids.add(bid)
|
---|
48 | if len(self.seen_bids) >= self.stop_learning_after:
|
---|
49 | return self
|
---|
50 | for issue in self.domain.getIssues():
|
---|
51 | value = bid.getValue(issue)
|
---|
52 | if value not in self.frequencies[issue]:
|
---|
53 | self.frequencies[issue][value] = 0
|
---|
54 | self.frequencies[issue][value] += 1
|
---|
55 | self.total += 1
|
---|
56 | return self
|
---|
57 |
|
---|
58 | @staticmethod
|
---|
59 | def create():
|
---|
60 | """
|
---|
61 | Creates an empty opponent model without any of the parameters set
|
---|
62 |
|
---|
63 | @return an empty instance of MyOpponentModel
|
---|
64 | """
|
---|
65 | return MyOpponentModel(None, {}, 0, None)
|
---|
66 |
|
---|
67 | # Override
|
---|
68 | def getUtility(self, bid: Bid) -> Decimal:
|
---|
69 | """
|
---|
70 | Estimates the opponents utility of a certain bid.
|
---|
71 | @param bid the bid to estimate the utility for
|
---|
72 | @return the utility as a decimal value between [0, 1]
|
---|
73 | """
|
---|
74 | if self.total == 0:
|
---|
75 | return Decimal(1)
|
---|
76 |
|
---|
77 | issues = self.domain.getIssues()
|
---|
78 | issue_weights = self._get_issue_weights(issues=issues)
|
---|
79 | value_utilities = self._get_value_utilities_of_bid(issues=issues, bid=bid)
|
---|
80 |
|
---|
81 | total = Decimal(0)
|
---|
82 | for issue in issue_weights:
|
---|
83 | weight_of_value = value_utilities[issue]
|
---|
84 |
|
---|
85 | total += Decimal(issue_weights[issue] * weight_of_value)
|
---|
86 | return total
|
---|
87 |
|
---|
88 | def _get_issue_weights(self, issues) -> dict:
|
---|
89 | """
|
---|
90 | Gets the estimated weight for each issue. It does this by first counting the amount
|
---|
91 | of unique values we have been offered for each
|
---|
92 | issue. Then it divides the total amount of bids by the amount of unique values this issue has.
|
---|
93 | This number is then used to calculate the weight by dividing itself by the total amount of unique values.
|
---|
94 |
|
---|
95 | The logic behind this is that if the opponent only bids a few unique values for a issue, this issue
|
---|
96 | probably has high importance to it since it is unwilling to compromise on this issue, whereas it is
|
---|
97 | happy to concede on other issues.
|
---|
98 |
|
---|
99 | We have verified this method produces the expected outcome.
|
---|
100 |
|
---|
101 | @param issues all the issues to consider
|
---|
102 | @return a dictionary with the estimated weights for each issue.
|
---|
103 | """
|
---|
104 | unique_bids_per_issue = dict()
|
---|
105 | iwl = []
|
---|
106 | issue_weights_dict = dict()
|
---|
107 | total_unique_bids = 0
|
---|
108 | for issue in issues:
|
---|
109 | unique_bids = 0
|
---|
110 | for value in self.frequencies[issue]:
|
---|
111 | if self.frequencies[issue][value]:
|
---|
112 | unique_bids += 1
|
---|
113 | unique_bids_per_issue[issue] = unique_bids
|
---|
114 | total_unique_bids += unique_bids
|
---|
115 | for issue in issues:
|
---|
116 | score = total_unique_bids / unique_bids_per_issue[issue]
|
---|
117 | iwl.append(score)
|
---|
118 | issue_weights_dict[issue] = score
|
---|
119 | total = sum(iwl)
|
---|
120 | res = {k: (lambda x: (x / total))(v) for k, v in issue_weights_dict.items()}
|
---|
121 | # debug purposes:
|
---|
122 | # print('unique bids', unique_bids_per_issue)
|
---|
123 | # print('frequencies', self.frequencies)
|
---|
124 | # print('res', res)
|
---|
125 | return res
|
---|
126 |
|
---|
127 | def _get_value_utilities_of_bid(self, issues, bid: Bid):
|
---|
128 | """
|
---|
129 | This method calculates the estimated utility for each value proposed in the bid.
|
---|
130 | It does this by dividing the frequency of which the value has been offered by the total number of offers.
|
---|
131 | You then get an utility in the range of [0, 1]
|
---|
132 |
|
---|
133 | We have verified this method produces the expected outcome.
|
---|
134 |
|
---|
135 | @param issues all the issues to consider
|
---|
136 | @param bid the bid to calculate this for
|
---|
137 | @return a dictionary with the utilities for each value in the bid
|
---|
138 | """
|
---|
139 | utilities = dict()
|
---|
140 | for issue in issues:
|
---|
141 | utilities[issue] = 0.5
|
---|
142 | value = bid.getValue(issue)
|
---|
143 | if issue in self.frequencies and value in self.frequencies[issue]:
|
---|
144 | # print('Frequencies', self.frequencies[issue])
|
---|
145 | # print('Value we are checking for', value)
|
---|
146 | # print('Result', self.frequencies[issue][value] / self.total)
|
---|
147 | utilities[issue] = self.frequencies[issue][value] / self.total
|
---|
148 | return utilities
|
---|
149 |
|
---|
150 | def WithParameters(self, parameters: Parameters) -> "OpponentModel":
|
---|
151 | """
|
---|
152 | Does nothing, just returns itself.
|
---|
153 | @return itself
|
---|
154 | """
|
---|
155 | return self
|
---|