1 | from copy import copy
|
---|
2 | from decimal import Decimal
|
---|
3 | import re
|
---|
4 | from typing import Dict, cast, Union, Optional
|
---|
5 |
|
---|
6 | from pyson.JsonGetter import JsonGetter
|
---|
7 |
|
---|
8 | from geniusweb.issuevalue.Bid import Bid
|
---|
9 | from geniusweb.issuevalue.Domain import Domain
|
---|
10 | from geniusweb.issuevalue.Value import Value
|
---|
11 | from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
|
---|
12 | from geniusweb.profile.utilityspace.ValueSetUtilities import ValueSetUtilities
|
---|
13 |
|
---|
14 | from geniusweb.utils import toStr
|
---|
15 |
|
---|
16 | class LinearAdditiveUtilitySpace (LinearAdditive):
|
---|
17 | '''
|
---|
18 | Defines a UtilitySpace in terms of a weighted sum of per-issue preferences.
|
---|
19 | immutable. A {@link LinearAdditiveUtilitySpace} works with complete bids.
|
---|
20 |
|
---|
21 | Constructor guarantees that
|
---|
22 | <ul>
|
---|
23 | <li>weights are normalized to 1
|
---|
24 | <li>the issues in the utility map and weights map match those in the domain
|
---|
25 | <li>The utilities for each issue are proper {@link ValueSetUtilities} objects
|
---|
26 | </ul>
|
---|
27 | '''
|
---|
28 |
|
---|
29 |
|
---|
30 | def __init__(self, domain:Domain , name:str, issueUtilities: Dict[str, ValueSetUtilities],\
|
---|
31 | issueWeights: Dict[str, Decimal] , reservationBid:Optional[Bid] = None ):
|
---|
32 | '''
|
---|
33 | @param domain the {@link Domain} in which this profile is defined.
|
---|
34 | @param name the name of this profile. Must be simple name (a-Z, 0-9)
|
---|
35 | @param issueUtilities a map with key: issue names (String) and value: the values
|
---|
36 | for that issue. There MUST NOT be a null issue. All values
|
---|
37 | MUST NOT be null.
|
---|
38 | @param issueWeights the weight of each issue in the computation of the
|
---|
39 | weighted sum. The issues must be the same as those in the
|
---|
40 | utils map. All weights MUST NOT be null. The weights MUST
|
---|
41 | sum to 1.
|
---|
42 | @param reservationBid the reservation bid. Only bids that are
|
---|
43 | {@link #isPreferredOrEqual(Bid, Bid)} should be accepted.
|
---|
44 | Can be None, meaning that there is no reservation bid and
|
---|
45 | any agreement is better than no agreement.
|
---|
46 | @throws NullPointerException if values are incorrectly null.
|
---|
47 | @throws IllegalArgumentException if preconditions not met.
|
---|
48 | '''
|
---|
49 | self._domain = domain
|
---|
50 | self._name = name
|
---|
51 | self._reservationBid=reservationBid
|
---|
52 | self._issueUtilities=copy(issueUtilities)
|
---|
53 | self._issueWeights=copy(issueWeights)
|
---|
54 |
|
---|
55 | if domain == None:
|
---|
56 | raise ValueError("domain=null")
|
---|
57 |
|
---|
58 | if issueUtilities == None :
|
---|
59 | raise ValueError("utils=null")
|
---|
60 |
|
---|
61 | if issueWeights == None:
|
---|
62 | raise ValueError("weights=null");
|
---|
63 |
|
---|
64 | if None in issueUtilities.values():
|
---|
65 | raise ValueError(\
|
---|
66 | "One of the ValueSetUtilities in issueUtilitiesis null:"\
|
---|
67 | + str(issueUtilities))
|
---|
68 |
|
---|
69 | if None in issueWeights.values():
|
---|
70 | raise ValueError("One of the weights is null")
|
---|
71 |
|
---|
72 | if None in issueUtilities.keys():
|
---|
73 | raise ValueError("One of the issue names is null");
|
---|
74 |
|
---|
75 | if name == None or not re.match("[a-zA-Z0-9]+", name) :
|
---|
76 | raise ValueError("Name must be simple (a-Z, 0-9) but got " + name)
|
---|
77 |
|
---|
78 | if issueUtilities.keys() != domain.getIssues():
|
---|
79 | raise ValueError( \
|
---|
80 | "The issues in utilityspace and domain do not match: utilityspace has issues "\
|
---|
81 | + str(issueUtilities.keys()) + " but domain contains "\
|
---|
82 | + str(domain.getIssues()))
|
---|
83 |
|
---|
84 | if issueWeights.keys() != domain.getIssues():
|
---|
85 | raise ValueError(\
|
---|
86 | "The issues in weights and domain do not match: weights has "\
|
---|
87 | + str(issueWeights.keys()) + " but domain contains "\
|
---|
88 | + str(domain.getIssues()))
|
---|
89 |
|
---|
90 | for issue in issueUtilities:
|
---|
91 | message:str = issueUtilities.get(issue).isFitting(domain.getValues(issue)) #type:ignore
|
---|
92 | if message != None:
|
---|
93 | raise ValueError(message);
|
---|
94 |
|
---|
95 | total:Decimal = sum(issueWeights.values()) #type:ignore
|
---|
96 | if total!=Decimal(1):
|
---|
97 | raise ValueError("The sum of the weights ("
|
---|
98 | + str(issueWeights.values()) + ") must be 1")
|
---|
99 |
|
---|
100 | if reservationBid:
|
---|
101 | message1:Optional[str] = domain.isFitting(reservationBid)
|
---|
102 | if message1:
|
---|
103 | raise ValueError("reservationbid is not fitting domain: " + message1)
|
---|
104 |
|
---|
105 |
|
---|
106 | #Override
|
---|
107 | def getUtility(self, bid:Bid ) -> Decimal:
|
---|
108 | return sum([ self._util(iss, bid.getValue(iss)) for iss in self._issueWeights.keys() ]) #type:ignore
|
---|
109 |
|
---|
110 | #Override
|
---|
111 | def getWeight(self,issue:str) -> Decimal :
|
---|
112 | return self._issueWeights[issue]
|
---|
113 |
|
---|
114 | #Override
|
---|
115 | def __repr__(self):
|
---|
116 | return "LinearAdditive[" + toStr(self._issueUtilities) + "," + \
|
---|
117 | toStr(self._issueWeights) + "," + str(self._reservationBid) + "]"
|
---|
118 |
|
---|
119 |
|
---|
120 | #Override
|
---|
121 | def getReservationBid(self) -> Optional[Bid]:
|
---|
122 | return self._reservationBid;
|
---|
123 |
|
---|
124 |
|
---|
125 | def __hash__(self):
|
---|
126 | return hash((self._domain, tuple(self._issueUtilities.items()), tuple(self._issueWeights.items()), self._name, self._reservationBid))
|
---|
127 |
|
---|
128 | def __eq__(self, other):
|
---|
129 | return isinstance(other, self.__class__) and \
|
---|
130 | self._domain == other._domain and \
|
---|
131 | self._issueUtilities == other._issueUtilities and \
|
---|
132 | self._issueWeights == other._issueWeights and \
|
---|
133 | self._name == other._name and \
|
---|
134 | self._reservationBid == other._reservationBid
|
---|
135 |
|
---|
136 | #Override
|
---|
137 | def getDomain(self)->Domain:
|
---|
138 | return self._domain
|
---|
139 |
|
---|
140 | #Override
|
---|
141 | def getName(self)->str:
|
---|
142 | return self._name
|
---|
143 |
|
---|
144 |
|
---|
145 | #Override
|
---|
146 | @JsonGetter("issueUtilities")
|
---|
147 | def getUtilities(self) -> Dict[str, ValueSetUtilities] :
|
---|
148 | return copy(self._issueUtilities)
|
---|
149 |
|
---|
150 |
|
---|
151 | #Override
|
---|
152 | @JsonGetter("issueWeights")
|
---|
153 | def getWeights(self)->Dict[str, Decimal] :
|
---|
154 | return copy(self._issueWeights)
|
---|
155 |
|
---|
156 | def _util(self, issue:str, value:Optional[Value] ) ->Decimal:
|
---|
157 | '''
|
---|
158 | @param issue the issue to get weighted util for
|
---|
159 | @param value the issue value to use (typically coming from a bid). Can be None
|
---|
160 | @return weighted util of just the issue value:
|
---|
161 | issueUtilities[issue].utility(value) * issueWeights[issue)
|
---|
162 | '''
|
---|
163 | if not value:
|
---|
164 | return Decimal(0)
|
---|
165 | return self._issueWeights[issue] * self._issueUtilities[issue].getUtility(value)
|
---|
166 |
|
---|