1 | from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
|
---|
2 | from geniusweb.opponentmodel.OpponentModel import OpponentModel
|
---|
3 | from decimal import Decimal
|
---|
4 | from decimal import Context
|
---|
5 | from geniusweb.issuevalue.Domain import Domain
|
---|
6 | from geniusweb.issuevalue.Bid import Bid
|
---|
7 | from typing import Dict, Optional
|
---|
8 | from geniusweb.issuevalue.Value import Value
|
---|
9 | from geniusweb.actions.Action import Action
|
---|
10 | from geniusweb.progress.Progress import Progress
|
---|
11 | from geniusweb.actions.Offer import Offer
|
---|
12 | from geniusweb.references.Parameters import Parameters
|
---|
13 | from geniusweb.utils import val, HASH, toStr
|
---|
14 |
|
---|
15 | class 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, float]], 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 | @staticmethod
|
---|
52 | def create() -> "FrequencyOpponentModel":
|
---|
53 | return FrequencyOpponentModel(None, {}, 0, None)
|
---|
54 |
|
---|
55 | # Override
|
---|
56 | def With(self, newDomain: Domain, newResBid: Optional[Bid]) -> "FrequencyOpponentModel":
|
---|
57 | if newDomain == None:
|
---|
58 | raise ValueError("domain is not initialized")
|
---|
59 | # FIXME merge already available frequencies?
|
---|
60 | return FrequencyOpponentModel(newDomain,
|
---|
61 | {iss: {} for iss in newDomain.getIssues()},
|
---|
62 | 0, newResBid)
|
---|
63 |
|
---|
64 | # Override
|
---|
65 | def getUtility(self, bid: Bid) -> Decimal:
|
---|
66 | if self._domain == None:
|
---|
67 | raise ValueError("domain is not initialized")
|
---|
68 | if self._totalBids == 0:
|
---|
69 | return Decimal(1)
|
---|
70 | sum = Decimal(0)
|
---|
71 |
|
---|
72 | # Assume different weights
|
---|
73 | dict = self.getWeight()
|
---|
74 |
|
---|
75 |
|
---|
76 | for issue in val(self._domain).getIssues():
|
---|
77 | if issue in bid.getIssues():
|
---|
78 | # Using estimated weights, compute the utility of the opponent for a given bid.
|
---|
79 | sum = sum + Context.multiply(Context(), self._getFraction(issue, val(bid.getValue(issue))),
|
---|
80 | Decimal.from_float(dict[issue]))
|
---|
81 |
|
---|
82 | return round(sum, FrequencyOpponentModel._DECIMALS)
|
---|
83 |
|
---|
84 | # Override
|
---|
85 | def getName(self) -> str:
|
---|
86 | if self._domain == None:
|
---|
87 | raise ValueError("domain is not initialized")
|
---|
88 | return "FreqOppModel" + str(hash(self)) + "For" + str(self._domain)
|
---|
89 |
|
---|
90 | # Override
|
---|
91 | def getDomain(self) -> Domain:
|
---|
92 | return val(self._domain)
|
---|
93 |
|
---|
94 | # Override
|
---|
95 | def WithAction(self, action: Action, progress: Progress) -> "FrequencyOpponentModel":
|
---|
96 | if self._domain == None:
|
---|
97 | raise ValueError("domain is not initialized")
|
---|
98 |
|
---|
99 | if not isinstance(action, Offer):
|
---|
100 | return self
|
---|
101 |
|
---|
102 | # Method altered so that it computes utilities in a more accurate way than just frequencies.
|
---|
103 | bid: Bid = action.getBid()
|
---|
104 | newFreqs: Dict[str, Dict[Value, float]] = self.cloneMap(self._bidFrequencies)
|
---|
105 | for issue in self._domain.getIssues(): # type:ignore
|
---|
106 | freqs: Dict[Value, float] = newFreqs[issue]
|
---|
107 | values_in_issue = len(newFreqs[issue])
|
---|
108 | value = bid.getValue(issue)
|
---|
109 | avg_value = 0.5
|
---|
110 | if value != None:
|
---|
111 | oldfreq = 0
|
---|
112 | if value in freqs:
|
---|
113 | oldfreq = freqs[value]
|
---|
114 |
|
---|
115 | for i in freqs:
|
---|
116 | if freqs[i] == 0:
|
---|
117 | freqs[i] = avg_value
|
---|
118 |
|
---|
119 | freqs[value] = oldfreq + 0.05 # type:ignore
|
---|
120 | if freqs[value] > 1:
|
---|
121 | freqs[value] = 1
|
---|
122 | factor = values_in_issue * avg_value / sum(freqs.values())
|
---|
123 | for k in freqs:
|
---|
124 | freqs[k] = freqs[k] * factor
|
---|
125 |
|
---|
126 | return FrequencyOpponentModel(self._domain, newFreqs,
|
---|
127 | self._totalBids + 1, self._resBid)
|
---|
128 |
|
---|
129 | def getCounts(self, issue: str) -> Dict[Value, float]:
|
---|
130 | '''
|
---|
131 | @param issue the issue to get frequency info for
|
---|
132 | @return a map containing a map of values and the number of times that
|
---|
133 | value was used in previous bids. Values that are possible but not
|
---|
134 | in the map have frequency 0.
|
---|
135 | '''
|
---|
136 | if self._domain == None:
|
---|
137 | raise ValueError("domain is not initialized")
|
---|
138 | if not issue in self._bidFrequencies:
|
---|
139 | return {}
|
---|
140 | return dict(self._bidFrequencies.get(issue)) # type:ignore
|
---|
141 |
|
---|
142 | # Override
|
---|
143 | def WithParameters(self, parameters: Parameters) -> OpponentModel:
|
---|
144 | return self # ignore parameters
|
---|
145 |
|
---|
146 | def _getFraction(self, issue: str, value: Value) -> Decimal:
|
---|
147 | '''
|
---|
148 | @param issue the issue to check
|
---|
149 | @param value the value to check
|
---|
150 | @return the fraction of the total cases that bids contained given value
|
---|
151 | for the issue.
|
---|
152 | '''
|
---|
153 | if self._totalBids == 0:
|
---|
154 | return Decimal(0.5)
|
---|
155 | # return Decimal(1)
|
---|
156 | if not (issue in self._bidFrequencies and value in self._bidFrequencies[issue]):
|
---|
157 | return Decimal(0)
|
---|
158 |
|
---|
159 | return Decimal(self._bidFrequencies[issue][value])
|
---|
160 |
|
---|
161 | # return round((Decimal(freq) / self._totalBids), FrequencyOpponentModel._DECIMALS) # type:ignore
|
---|
162 |
|
---|
163 | @staticmethod
|
---|
164 | def cloneMap(freqs: Dict[str, Dict[Value, float]]) -> Dict[str, Dict[Value, float]]:
|
---|
165 | '''
|
---|
166 | @param freqs
|
---|
167 | @return deep copy of freqs map.
|
---|
168 | '''
|
---|
169 | map: Dict[str, Dict[Value, float]] = {}
|
---|
170 | for issue in freqs:
|
---|
171 | map[issue] = dict(freqs[issue])
|
---|
172 | return map
|
---|
173 |
|
---|
174 | # Override
|
---|
175 | def getReservationBid(self) -> Optional[Bid]:
|
---|
176 | return self._resBid
|
---|
177 |
|
---|
178 | def __eq__(self, other):
|
---|
179 | return isinstance(other, self.__class__) and \
|
---|
180 | self._domain == other._domain and \
|
---|
181 | self._bidFrequencies == other._bidFrequencies and \
|
---|
182 | self._totalBids == other._totalBids and \
|
---|
183 | self._resBid == other._resBid
|
---|
184 |
|
---|
185 | def __hash__(self):
|
---|
186 | return HASH((self._domain, self._bidFrequencies, self._totalBids, self._resBid))
|
---|
187 |
|
---|
188 | # Override
|
---|
189 |
|
---|
190 | # Override
|
---|
191 | def __repr__(self) -> str:
|
---|
192 | return "FrequencyOpponentModel[" + str(self._totalBids) + "," + \
|
---|
193 | toStr(self._bidFrequencies) + "]"
|
---|
194 |
|
---|
195 |
|
---|
196 | def toString(self):
|
---|
197 | return f"FrequencyOpponentModel({self._totalBids}, {self._bidFrequencies}"
|
---|
198 |
|
---|
199 | # Obtain estimated weights of issues for the opponent.
|
---|
200 | def getWeight(self):
|
---|
201 | dict = {}
|
---|
202 | total_sum = 0
|
---|
203 | for issue in val(self._domain).getIssues():
|
---|
204 | # get freq of the values used in every issue
|
---|
205 | hash = self.getCounts(issue)
|
---|
206 |
|
---|
207 | # pick out the max freq of the values from each issue
|
---|
208 | value_with_highest_frequency = max(hash, key=hash.get)
|
---|
209 | dict[issue] = hash[value_with_highest_frequency]
|
---|
210 | # keep track of total sum of "weights" (frequencies)
|
---|
211 | total_sum += dict[issue]
|
---|
212 |
|
---|
213 | if total_sum == 0:
|
---|
214 | for issue in dict:
|
---|
215 | dict[issue] = 0
|
---|
216 |
|
---|
217 | # get their "relative importance", relative to all other max frequencies from the other issue
|
---|
218 | else:
|
---|
219 | for issue in dict:
|
---|
220 | dict[issue] = (dict[issue] / total_sum)
|
---|
221 | return dict |
---|