source: geniuswebcore/geniusweb/profile/utilityspace/LinearAdditiveUtilitySpace.py@ 78

Last change on this file since 78 was 73, checked in by Bart Vastenhouw, 3 years ago

Fix for IssueValue hashcode.

File size: 5.6 KB
Line 
1from copy import copy
2from decimal import Decimal
3import re
4from typing import Dict, cast, Union, Optional
5
6from pyson.JsonGetter import JsonGetter
7
8from geniusweb.issuevalue.Bid import Bid
9from geniusweb.issuevalue.Domain import Domain
10from geniusweb.issuevalue.Value import Value
11from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
12from geniusweb.profile.utilityspace.ValueSetUtilities import ValueSetUtilities
13
14from geniusweb.utils import toStr
15
16class 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
Note: See TracBrowser for help on using the repository browser.