source: CSE3210/agent55/Group55OpponentModel.py

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

#6 Added CSE3210 parties

File size: 9.7 KB
Line 
1from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
2from geniusweb.opponentmodel.OpponentModel import OpponentModel
3from decimal import Decimal
4from geniusweb.issuevalue.Domain import Domain
5from geniusweb.issuevalue.Bid import Bid
6from typing import Dict, Optional
7from geniusweb.issuevalue.Value import Value
8from geniusweb.actions.Action import Action
9from geniusweb.progress.Progress import Progress
10from geniusweb.actions.Offer import Offer
11from geniusweb.references.Parameters import Parameters
12from geniusweb.utils import val, HASH, toStr
13
14
15class FrequencyOpponentModel(UtilitySpace, OpponentModel):
16 '''
17 implements an {@link OpponentModel} by counting frequencies of bids placed by
18 the opponent.
19 <p>
20 NOTE: {@link NumberValue}s are also treated as 'discrete', so the frequency
21 of one value does not influence the influence the frequency of nearby values
22 (as you might expect as {@link NumberValueSetUtilities} is only affected by
23 the endpoints).
24 <p>
25 immutable.
26 '''
27
28 _DECIMALS = 4 # accuracy of our computations.
29
30 def __init__(self, domain: Optional[Domain],
31 freqs: Dict[str, Dict[Value, int]], total: int,
32 resBid: Optional[Bid]):
33 '''
34 internal constructor. DO NOT USE, see create. Assumes the freqs keyset is
35 equal to the available issues.
36
37 @param domain the domain. Should not be None
38 @param freqs the observed frequencies for all issue values. This map is
39 assumed to be a fresh private-access only copy.
40 @param total the total number of bids contained in the freqs map. This
41 must be equal to the sum of the Integer values in the
42 {@link #bidFrequencies} for each issue (this is not
43 checked).
44 @param resBid the reservation bid. Can be null
45 '''
46 self._domain = domain
47 self._bidFrequencies = freqs
48 self._totalBids = total
49 self._resBid = resBid
50
51 """
52 These variables are dictionaries with all issues of the domain as their keys. '_BidsChangedFrequency' and
53 '_previousIssueValue' are helper-structures to construct the final structure: '_issueWeights', which holds the
54 estimated weight of any issue.
55 """
56 self._BidsChangedFrequency = {
57 key: 0 for key in self._bidFrequencies.keys()}
58 self._previousIssueValue = {
59 key: None for key in self._bidFrequencies.keys()}
60 self._issueWeights = {key: Decimal(
61 1/len(self._bidFrequencies)) for key in self._bidFrequencies.keys()}
62
63 @staticmethod
64 def create() -> "FrequencyOpponentModel":
65 return FrequencyOpponentModel(None, {}, 0, None)
66
67 # Override
68 def With(self, newDomain: Domain, newResBid: Optional[Bid]) -> "FrequencyOpponentModel":
69 if newDomain == None:
70 raise ValueError("domain is not initialized")
71 # FIXME merge already available frequencies?
72 return FrequencyOpponentModel(newDomain,
73 {iss: {}
74 for iss in newDomain.getIssues()},
75 0, newResBid)
76
77 """
78 The original implementation provided by Geniusweb calculates the utility for a bid with equal weights for each issue:
79 '1 / (all issues present in the domain)' Instead of that we now use individual weights for each issue.
80 """
81 # Override
82
83 def getUtility(self, bid: Bid) -> Decimal:
84 if self._domain == None:
85 raise ValueError("domain is not initialized")
86 if self._totalBids == 0:
87 return Decimal(1)
88 sum = Decimal(0)
89
90 for issue in val(self._domain).getIssues():
91 if issue in bid.getIssues():
92 sum += (self._issueWeights[issue] *
93 self._getFraction(issue, val(bid.getValue(issue))))
94 return round(sum, FrequencyOpponentModel._DECIMALS)
95
96 # Override
97 def getName(self) -> str:
98 if self._domain == None:
99 raise ValueError("domain is not initialized")
100 return "FreqOppModel" + str(hash(self)) + "For" + str(self._domain)
101
102 # Override
103 def getDomain(self) -> Domain:
104 return val(self._domain)
105
106 """
107 Since this method updates the model with every offer, this is also where we update our
108 weights-estimation-variables.
109 """
110 # Override
111
112 def WithAction(self, action: Action, progress: Progress) -> "FrequencyOpponentModel":
113 if self._domain == None:
114 raise ValueError("domain is not initialized")
115
116 if not isinstance(action, Offer):
117 return self
118
119 bid: Bid = action.getBid()
120 newFreqs: Dict[str, Dict[Value, int]
121 ] = self.cloneMap(self._bidFrequencies)
122 for issue in self._domain.getIssues(): # type:ignore
123 freqs: Dict[Value, int] = newFreqs[issue]
124 value = bid.getValue(issue)
125 if value != None:
126
127 """"
128 Added by group55:
129 If this is the first time the issue is mentioned, we do not do anything.
130
131 if the issue is mentioned before, but the value is not changed, we do not do anything.
132
133 if the issue is mentioned before and the value is changed, we update the 'bidsChangedFrequency' for
134 that issue.
135
136 In any case, we do update the 'previousIssueValue' afterwards to now be the current value.
137 """
138 if self._previousIssueValue[issue] is not None:
139 if self._previousIssueValue[issue] is not value:
140 self._BidsChangedFrequency[issue] += 1
141 self._previousIssueValue[issue] = value
142
143 """
144 End of Group55 contribution.
145 """
146
147 oldfreq = 0
148 if value in freqs:
149 oldfreq = freqs[value]
150 freqs[value] = oldfreq + 1 # type:ignore
151
152 """
153 Added Group55:
154 Now that all issues have been processed. We loop through them again to calculate their weights.
155
156 First of all, if the total amount in changes is less then the total amount of issues, we keep the default
157 weights, which are all equal. This is because the calculation below is non-representative with little data.
158 Therefore, this way, at least all issues have had a change to be changed.
159
160 After that point, all issues-weights are updated as follows:
161 they are 1 - (the frequency of their changes divided by the total amount of changes of all issues). This way
162 The more an issue has been changes, the lower the weight. At the end of the loop, all weights will sum up to 1.
163 """
164 totalAmountOfChanges = sum(self._BidsChangedFrequency.values())
165 if totalAmountOfChanges >= len(self._domain.getIssues()):
166 for issue in self._domain.getIssues():
167 self._issueWeights[issue] = Decimal(
168 1 - (self._BidsChangedFrequency / totalAmountOfChanges))
169
170 """
171 End of Group55 contribution
172 """
173
174 return FrequencyOpponentModel(self._domain, newFreqs,
175 self._totalBids+1, self._resBid)
176
177 def getCounts(self, issue: str) -> Dict[Value, int]:
178 '''
179 @param issue the issue to get frequency info for
180 @return a map containing a map of values and the number of times that
181 value was used in previous bids. Values that are possible but not
182 in the map have frequency 0.
183 '''
184 if self._domain == None:
185 raise ValueError("domain is not initialized")
186 if not issue in self._bidFrequencies:
187 return {}
188 return dict(self._bidFrequencies.get(issue)) # type:ignore
189
190 # Override
191 def WithParameters(self, parameters: Parameters) -> OpponentModel:
192 return self # ignore parameters
193
194 def _getFraction(self, issue: str, value: Value) -> Decimal:
195 '''
196 @param issue the issue to check
197 @param value the value to check
198 @return the fraction of the total cases that bids contained given value
199 for the issue.
200 '''
201 if self._totalBids == 0:
202 return Decimal(1)
203 if not (issue in self._bidFrequencies and value in self._bidFrequencies[issue]):
204 return Decimal(0)
205 freq: int = self._bidFrequencies[issue][value]
206 # type:ignore
207 return round(Decimal(freq) / self._totalBids, FrequencyOpponentModel._DECIMALS)
208
209 @staticmethod
210 def cloneMap(freqs: Dict[str, Dict[Value, int]]) -> Dict[str, Dict[Value, int]]:
211 '''
212 @param freqs
213 @return deep copy of freqs map.
214 '''
215 map: Dict[str, Dict[Value, int]] = {}
216 for issue in freqs:
217 map[issue] = dict(freqs[issue])
218 return map
219
220 # Override
221 def getReservationBid(self) -> Optional[Bid]:
222 return self._resBid
223
224 def __eq__(self, other):
225 return isinstance(other, self.__class__) and \
226 self._domain == other._domain and \
227 self._bidFrequencies == other._bidFrequencies and \
228 self._totalBids == other._totalBids and \
229 self._resBid == other._resBid
230
231 def __hash__(self):
232 return HASH((self._domain, self._bidFrequencies, self._totalBids, self._resBid))
233 # Override
234
235 # Override
236 def __repr__(self) -> str:
237 return "FrequencyOpponentModel[" + str(self._totalBids) + "," + \
238 toStr(self._bidFrequencies) + "]"
Note: See TracBrowser for help on using the repository browser.