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