1 |
|
---|
2 |
|
---|
3 | from decimal import Decimal
|
---|
4 | from typing import List, Dict
|
---|
5 |
|
---|
6 | from tudelft.utilities.immutablelist.AbstractImmutableList import AbstractImmutableList
|
---|
7 | from tudelft.utilities.immutablelist.FixedList import FixedList
|
---|
8 | from tudelft.utilities.immutablelist.ImmutableList import ImmutableList
|
---|
9 | from tudelft.utilities.immutablelist.JoinedList import JoinedList
|
---|
10 | from tudelft.utilities.immutablelist.MapList import MapList
|
---|
11 | from tudelft.utilities.immutablelist.Tuple import Tuple
|
---|
12 |
|
---|
13 | from geniusweb.bidspace.Interval import Interval
|
---|
14 | from geniusweb.bidspace.IssueInfo import IssueInfo
|
---|
15 | from geniusweb.issuevalue.Bid import Bid
|
---|
16 | from geniusweb.issuevalue.Domain import Domain
|
---|
17 | from geniusweb.issuevalue.Value import Value
|
---|
18 | from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
|
---|
19 | from geniusweb.utils import val
|
---|
20 |
|
---|
21 |
|
---|
22 | class BidsWithUtility:
|
---|
23 | '''
|
---|
24 | Tool class containing functions dealing with utilities of all bids in a given
|
---|
25 | {@link LinearAdditive}. This class caches previously computed values to
|
---|
26 | accelerate the calls and subsequent calls. Re-use the object to keep/reuse
|
---|
27 | the cache.
|
---|
28 | <h2>Rounding</h2> Internally, utilities of bids are rounded to the given
|
---|
29 | precision. This may cause inclusion/exclusion of some bids in the results.
|
---|
30 | See {@link #BidsWithUtility(LinearAdditive, int)} for more details
|
---|
31 | Immutable.
|
---|
32 | '''
|
---|
33 |
|
---|
34 | def __init__(self, issuesInfo:List[IssueInfo] , precision:int):
|
---|
35 | '''
|
---|
36 | @param issuesInfo List of the relevant issues (in order of relevance) and
|
---|
37 | all info of each issue.
|
---|
38 | @param precision the number of digits to use for computations. In
|
---|
39 | practice, 6 seems a good default value.
|
---|
40 | <p>
|
---|
41 | All utilities * weight are rounded to this number of
|
---|
42 | digits. This value should match the max number of
|
---|
43 | (digits used in the weight of an issue + number of
|
---|
44 | digits used in the issue utility). To determine the
|
---|
45 | optimal value, one may consider the step size of the
|
---|
46 | issues, and the range of interest. For instance if the
|
---|
47 | utility function has values 1/3 and 2/3, then these have
|
---|
48 | an 'infinite' number of relevant digits. But if the goal
|
---|
49 | is to search bids between utility 0.1 and 0.2, then
|
---|
50 | computing in 2 digits might already be sufficient.
|
---|
51 | <p>
|
---|
52 | This algorithm has memory and space complexity O(
|
---|
53 | |nissues| 10^precision ). For spaces up to 7 issues, 7
|
---|
54 | digits should be feasible; for 9 issues, 6 digits may be
|
---|
55 | the maximum.
|
---|
56 | '''
|
---|
57 | if issuesInfo == None or len(issuesInfo) == 0:
|
---|
58 | raise ValueError("sortedissues list must contain at least 1 element")
|
---|
59 | self._issueInfo = issuesInfo;
|
---|
60 | self._precision = precision;
|
---|
61 | # cache. Key = call arguments for {@link #get(int, Interval)}. Value=return
|
---|
62 | # value of that call.
|
---|
63 |
|
---|
64 | self._cache:Dict[Tuple[int, Interval], ImmutableList[Bid]] = {}
|
---|
65 |
|
---|
66 | @staticmethod
|
---|
67 | def create(space:LinearAdditive, precision:int=6) -> "BidsWithUtility":
|
---|
68 | '''
|
---|
69 | Support constructor, uses default precision 6. This value seems practical
|
---|
70 | for the common range of issues, utilities and weights. See
|
---|
71 | {@link #BidsWithUtility(LinearAdditive, int)} for more details on the
|
---|
72 | precision.
|
---|
73 |
|
---|
74 | @param space the {@link LinearAdditive} to analyze
|
---|
75 | @param space the {@link LinearAdditive} to analyze. Optional, defaults to 6
|
---|
76 | '''
|
---|
77 | return BidsWithUtility(BidsWithUtility._getInfo(space, precision), precision);
|
---|
78 |
|
---|
79 | def getRange(self) -> Interval:
|
---|
80 | '''
|
---|
81 | @return the (rounded) utility {@link Interval} of this space: minimum and
|
---|
82 | maximum achievable utility.
|
---|
83 | '''
|
---|
84 | return self._getRange(len(self._issueInfo) - 1)
|
---|
85 |
|
---|
86 | def getBids(self, range: Interval) -> ImmutableList[Bid]:
|
---|
87 | '''
|
---|
88 | @param range the minimum and maximum utility required of the bids. to be
|
---|
89 | included (both ends inclusive).
|
---|
90 | @return a list with bids that have a (rounded) utility inside range.
|
---|
91 | possibly empty.
|
---|
92 | '''
|
---|
93 | return self._get(len(self._issueInfo) - 1, range.round(self._precision));
|
---|
94 |
|
---|
95 | def getInfo(self) -> List[IssueInfo]:
|
---|
96 | return self._issueInfo.copy()
|
---|
97 |
|
---|
98 | def getExtremeBid(self, isMax:bool) -> Bid:
|
---|
99 | '''
|
---|
100 | @param isMax the extreme bid required
|
---|
101 | @return the extreme bid, either the minimum if isMax=false or maximum if
|
---|
102 | isMax=true
|
---|
103 | '''
|
---|
104 | map:Dict[str, Value] = {}
|
---|
105 | for info in self._issueInfo:
|
---|
106 | map[info.getName()] = info.getExtreme(isMax)
|
---|
107 | return Bid(map)
|
---|
108 |
|
---|
109 | def _get(self, n:int , goal:Interval) -> ImmutableList[Bid]:
|
---|
110 | '''
|
---|
111 | Create partial BidsWithUtil list considering only issues 0..n, with
|
---|
112 | utilities in given range.
|
---|
113 |
|
---|
114 | @param n the number of issueRanges to consider, we consider 0..n here.
|
---|
115 | The recursion decreases n until n=0
|
---|
116 | @param goal the minimum and maximum utility required of the bids. to be
|
---|
117 | included (both ends inclusive)
|
---|
118 | @return BidsWithUtil list, possibly empty.
|
---|
119 | '''
|
---|
120 | if goal == None:
|
---|
121 | raise ValueError("Interval=null")
|
---|
122 |
|
---|
123 | # clamp goal into what is reachable. Avoid caching empty
|
---|
124 | goal = goal.intersect(self._getRange(n))
|
---|
125 | if (goal.isEmpty()):
|
---|
126 | return FixedList([])
|
---|
127 |
|
---|
128 | cachetuple = Tuple(n, goal)
|
---|
129 | if (cachetuple in self._cache):
|
---|
130 | return self._cache[cachetuple]
|
---|
131 |
|
---|
132 | result = self._checkedGet(n, goal)
|
---|
133 | self._cache[cachetuple] = result
|
---|
134 | return result
|
---|
135 |
|
---|
136 | @staticmethod
|
---|
137 | def _getInfo(space2:LinearAdditive , precision:int) -> List[IssueInfo]:
|
---|
138 | dom = space2.getDomain()
|
---|
139 | return [IssueInfo(issue, dom.getValues(issue), \
|
---|
140 | val(space2.getUtilities().get(issue)), \
|
---|
141 | space2.getWeight(issue), precision) \
|
---|
142 | for issue in dom.getIssues()]
|
---|
143 |
|
---|
144 | def _checkedGet(self, n:int, goal:Interval) -> ImmutableList[Bid]:
|
---|
145 | info = self._issueInfo[n]
|
---|
146 | # issue is the first issuesWithRange.
|
---|
147 | issue = info.getName()
|
---|
148 |
|
---|
149 | if n == 0:
|
---|
150 | return OneIssueSubset(info, goal)
|
---|
151 |
|
---|
152 | # make new list, joining all sub-lists
|
---|
153 | fulllist:ImmutableList[Bid] = FixedList([])
|
---|
154 | for val in info.getValues():
|
---|
155 | weightedutil = info.getWeightedUtil(val)
|
---|
156 | subgoal = goal.subtract(weightedutil)
|
---|
157 | # recurse: get list of bids for the subspace
|
---|
158 | partialbids = self._get(n - 1, subgoal)
|
---|
159 |
|
---|
160 | bid = Bid({issue: val})
|
---|
161 | fullbids = BidsWithUtility.maplist(bid, partialbids)
|
---|
162 | if fullbids.size() != 0:
|
---|
163 | fulllist = JoinedList[Bid]([fullbids, fulllist])
|
---|
164 | return fulllist
|
---|
165 |
|
---|
166 | @staticmethod
|
---|
167 | def maplist(bid: Bid, partialbids: ImmutableList[Bid]) -> ImmutableList[Bid]:
|
---|
168 | '''
|
---|
169 | this is just to force a scope onto bid
|
---|
170 | '''
|
---|
171 | return MapList[Bid, Bid](lambda pbid: pbid.merge(bid), partialbids)
|
---|
172 |
|
---|
173 | def _getRange(self, n:int) -> Interval:
|
---|
174 | '''
|
---|
175 | @param n the maximum issuevalue utility to include. Use n=index of last
|
---|
176 | issue s= (#issues in the domain - 1) for the full range of this
|
---|
177 | domain.
|
---|
178 | @return Interval (min, max) of the total weighted utility Interval of
|
---|
179 | issues 0..n. All weighted utilities have been rounded to the set
|
---|
180 | {@link #precision}
|
---|
181 | '''
|
---|
182 | value = Interval(Decimal(0), Decimal(0))
|
---|
183 | for i in range(0, n + 1): # include end point
|
---|
184 | value = value.add(self._issueInfo[i].getInterval())
|
---|
185 | return value
|
---|
186 |
|
---|
187 |
|
---|
188 | class OneIssueSubset (AbstractImmutableList[Bid]):
|
---|
189 | '''
|
---|
190 | List of all one-issue bids that have utility inside given interval.
|
---|
191 | '''
|
---|
192 |
|
---|
193 | def __init__(self, info:IssueInfo , interval:Interval):
|
---|
194 | '''
|
---|
195 | @param info the {@link IssueInfo}
|
---|
196 | @param interval a utility interval (weighted)
|
---|
197 | '''
|
---|
198 | self._info = info;
|
---|
199 | self._interval = interval;
|
---|
200 | self._size = info._subsetSize(interval)
|
---|
201 |
|
---|
202 | # Override
|
---|
203 | def get(self, index:int) -> Bid:
|
---|
204 | return Bid({self._info.getName():
|
---|
205 | self._info._subset(self._interval)[index]})
|
---|
206 |
|
---|
207 | # Override
|
---|
208 | def size(self) -> int:
|
---|
209 | return self._size
|
---|