1 | package geniusweb.profile.utilityspace;
|
---|
2 |
|
---|
3 | import java.math.BigDecimal;
|
---|
4 | import java.util.Arrays;
|
---|
5 | import java.util.HashMap;
|
---|
6 | import java.util.HashSet;
|
---|
7 | import java.util.LinkedList;
|
---|
8 | import java.util.List;
|
---|
9 | import java.util.Map;
|
---|
10 | import java.util.Set;
|
---|
11 |
|
---|
12 | import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
---|
13 | import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
---|
14 | import com.fasterxml.jackson.annotation.JsonCreator;
|
---|
15 | import com.fasterxml.jackson.annotation.JsonProperty;
|
---|
16 |
|
---|
17 | import geniusweb.issuevalue.Bid;
|
---|
18 | import geniusweb.issuevalue.Domain;
|
---|
19 | import geniusweb.issuevalue.NumberValueSet;
|
---|
20 | import geniusweb.issuevalue.Value;
|
---|
21 | import geniusweb.profile.DefaultProfile;
|
---|
22 |
|
---|
23 | /**
|
---|
24 | * This is a utility space that defines the utility of bids as a sum of the
|
---|
25 | * utilities of a number of subsets, or groups, of the issue values.
|
---|
26 | * <p>
|
---|
27 | * A group defines the utility of a non-empty subset of the issues in the
|
---|
28 | * domain. The parts are non-overlapping. Missing issue values have utility 0.
|
---|
29 | * <p>
|
---|
30 | * This space enables handling {@link UtilitySpace}s that are not simply linear
|
---|
31 | * additive, but with interacting issue values. For example, if you would like
|
---|
32 | * to have a car with good speakers but only if there is also a good hifi set,
|
---|
33 | * you can make a part that has {@link PartsUtilities} like
|
---|
34 | * <code> { { speakers:yes, hifi:yes }:1, {speakers, no: hifh:yes}:0.2, ...} </code>
|
---|
35 | * <p>
|
---|
36 | * NOTICE this space is completely discrete. There is no equivalent of the
|
---|
37 | * {@link NumberValueSet} here.
|
---|
38 | */
|
---|
39 | @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE, isGetterVisibility = Visibility.NONE)
|
---|
40 | public class SumOfGroupsUtilitySpace extends DefaultProfile
|
---|
41 | implements UtilitySpace {
|
---|
42 | /**
|
---|
43 | * the key is the part name, the value is the valuesetutilities for this
|
---|
44 | * issue. Should be immutable so do not return direct access to this field.
|
---|
45 | */
|
---|
46 | private final HashMap<String, PartsUtilities> partUtilities = new HashMap<>();
|
---|
47 |
|
---|
48 | /**
|
---|
49 | * @param domain the {@link Domain} in which this profile is defined.
|
---|
50 | * @param name the name of this profile. Must be simple name (a-Z, 0-9)
|
---|
51 | * @param partUtilities a map with key: part names (String) and value: the
|
---|
52 | * {@link PartsUtilities} for that part. There MUST NOT be
|
---|
53 | * a null part name. All values MUST NOT be null. The
|
---|
54 | * PartsUtilities must match the
|
---|
55 | * @param reservationBid the reservation bid. Only bids that are
|
---|
56 | * {@link #isPreferredOrEqual(Bid, Bid)} should be
|
---|
57 | * accepted. Can be null, meaning that there is no
|
---|
58 | * reservation bid and any agreement is better than no
|
---|
59 | * agreement.
|
---|
60 | * @throws NullPointerException if values are incorrectly null.
|
---|
61 | * @throws IllegalArgumentException if preconditions not met.
|
---|
62 | */
|
---|
63 | @JsonCreator
|
---|
64 | public SumOfGroupsUtilitySpace(@JsonProperty("domain") Domain domain,
|
---|
65 | @JsonProperty("name") String name,
|
---|
66 | @JsonProperty("partUtilities") HashMap<String, PartsUtilities> partUtilities,
|
---|
67 | @JsonProperty("reservationBid") Bid reservationBid) {
|
---|
68 | super(name, domain, reservationBid);
|
---|
69 | this.partUtilities.putAll(partUtilities);
|
---|
70 |
|
---|
71 | String err = checkParts();
|
---|
72 | if (err != null) {
|
---|
73 | throw new IllegalArgumentException(err);
|
---|
74 | }
|
---|
75 | }
|
---|
76 |
|
---|
77 | /**
|
---|
78 | * Copy settings in las. This will have the exact same utilities as the las
|
---|
79 | * but this then gives you the power to change it into a non-linear space by
|
---|
80 | * grouping.
|
---|
81 | *
|
---|
82 | * @param las the {@link LinearAdditive} to be converted/copied.
|
---|
83 | *
|
---|
84 | */
|
---|
85 | public SumOfGroupsUtilitySpace(LinearAdditive las) {
|
---|
86 | this(las.getDomain(), las.getName(), las2parts(las),
|
---|
87 | las.getReservationBid());
|
---|
88 | }
|
---|
89 |
|
---|
90 | @Override
|
---|
91 | public BigDecimal getUtility(Bid bid) {
|
---|
92 | //#PY return sum( [ self._util(partname, self._bid) for partname in self._partUtilities.keys() ] )
|
---|
93 | return partUtilities.keySet().stream()
|
---|
94 | .map(partname -> util(partname, bid))
|
---|
95 | .reduce(BigDecimal.ZERO, BigDecimal::add);
|
---|
96 | }
|
---|
97 |
|
---|
98 | @Override
|
---|
99 | public String toString() {
|
---|
100 | return "PartialAdditive[" + partUtilities + "," + getReservationBid()
|
---|
101 | + "]";
|
---|
102 | }
|
---|
103 |
|
---|
104 | /**
|
---|
105 | *
|
---|
106 | * @param partnames the partnames to remove from this. There must be at
|
---|
107 | * least 2 parts
|
---|
108 | * @param newpartname the name of the new part that contains all partnames,
|
---|
109 | * grouped into 1 "issue". This name must not be an issue
|
---|
110 | * in this.
|
---|
111 | * @return new {@link SumOfGroupsUtilitySpace} that takes all partnames out
|
---|
112 | * of this, and makes a newaprtname that contains these.
|
---|
113 | */
|
---|
114 | public SumOfGroupsUtilitySpace group(List<String> partnames,
|
---|
115 | String newpartname) {
|
---|
116 | final Set<String> allpartnames = partUtilities.keySet();
|
---|
117 |
|
---|
118 | if (partnames.size() < 2) {
|
---|
119 | throw new IllegalArgumentException(
|
---|
120 | "Group must contain at least 2 parts");
|
---|
121 | }
|
---|
122 | if (allpartnames.contains(newpartname)) {
|
---|
123 | throw new IllegalArgumentException(
|
---|
124 | "newpartname " + newpartname + " is already in use");
|
---|
125 | }
|
---|
126 | for (String name : partnames) {
|
---|
127 | if (!allpartnames.contains(name)) {
|
---|
128 | throw new IllegalArgumentException("Unknown part name " + name);
|
---|
129 | }
|
---|
130 | }
|
---|
131 |
|
---|
132 | HashMap<String, PartsUtilities> newutils = new HashMap<String, PartsUtilities>();
|
---|
133 |
|
---|
134 | PartsUtilities newpartutils = null;
|
---|
135 |
|
---|
136 | for (String name : partUtilities.keySet()) {
|
---|
137 | if (partnames.contains(name)) {
|
---|
138 | if (newpartutils == null) {
|
---|
139 | newpartutils = partUtilities.get(name);
|
---|
140 | } else {
|
---|
141 | newpartutils = newpartutils.add(partUtilities.get(name));
|
---|
142 | }
|
---|
143 | } else {
|
---|
144 | newutils.put(name, partUtilities.get(name));
|
---|
145 | }
|
---|
146 | }
|
---|
147 | newutils.put(newpartname, newpartutils);
|
---|
148 |
|
---|
149 | return new SumOfGroupsUtilitySpace(getDomain(), getName(), newutils,
|
---|
150 | getReservationBid());
|
---|
151 | }
|
---|
152 |
|
---|
153 | @Override
|
---|
154 | public int hashCode() {
|
---|
155 | final int prime = 31;
|
---|
156 | int result = super.hashCode();
|
---|
157 | result = prime * result
|
---|
158 | + ((partUtilities == null) ? 0 : partUtilities.hashCode());
|
---|
159 | return result;
|
---|
160 | }
|
---|
161 |
|
---|
162 | @Override
|
---|
163 | public boolean equals(Object obj) {
|
---|
164 | if (this == obj)
|
---|
165 | return true;
|
---|
166 | if (!super.equals(obj))
|
---|
167 | return false;
|
---|
168 | if (getClass() != obj.getClass())
|
---|
169 | return false;
|
---|
170 | SumOfGroupsUtilitySpace other = (SumOfGroupsUtilitySpace) obj;
|
---|
171 | if (partUtilities == null) {
|
---|
172 | if (other.partUtilities != null)
|
---|
173 | return false;
|
---|
174 | } else if (!partUtilities.equals(other.partUtilities))
|
---|
175 | return false;
|
---|
176 | return true;
|
---|
177 | }
|
---|
178 |
|
---|
179 | /***************************** private funcs ***************************/
|
---|
180 | /**
|
---|
181 | *
|
---|
182 | * @param partname the name of the part to get the utility of
|
---|
183 | * @param bid the bid
|
---|
184 | * @return weighted util of just the part: utilities[part].getUtility(part
|
---|
185 | * of bid)
|
---|
186 | */
|
---|
187 | private BigDecimal util(String partname, Bid bid) {
|
---|
188 | PartsUtilities partutils = partUtilities.get(partname);
|
---|
189 | ProductOfValue value = collectValues(bid, partutils);
|
---|
190 | return partutils.getUtility(value);
|
---|
191 | }
|
---|
192 |
|
---|
193 | /**
|
---|
194 | *
|
---|
195 | * @param bid a full bid
|
---|
196 | * @param partutils the part utils for which a list of values from the bid
|
---|
197 | * is needed
|
---|
198 | * @return a list of values from the given bid, ordered as indicated in
|
---|
199 | * partutils
|
---|
200 | */
|
---|
201 | private ProductOfValue collectValues(Bid bid, PartsUtilities partutils) {
|
---|
202 | List<Value> values = new LinkedList<>();
|
---|
203 | for (String issue : partutils.getIssues()) {
|
---|
204 | values.add(bid.getValue(issue));
|
---|
205 | }
|
---|
206 | return new ProductOfValue(values);
|
---|
207 | }
|
---|
208 |
|
---|
209 | /**
|
---|
210 | *
|
---|
211 | * @param las a {@link LinearAdditive}
|
---|
212 | * @return a Map with partname-PartsUtilities. The partnames are identical
|
---|
213 | * to the issues in the given las.
|
---|
214 | */
|
---|
215 | private static HashMap<String, PartsUtilities> las2parts(
|
---|
216 | LinearAdditive las) {
|
---|
217 |
|
---|
218 | HashMap<String, PartsUtilities> map = new HashMap<>();
|
---|
219 | for (String issue : las.getUtilities().keySet()) {
|
---|
220 | ValueSetUtilities valset = las.getUtilities().get(issue);
|
---|
221 | Map<ProductOfValue, BigDecimal> utilslist = new HashMap<>();
|
---|
222 | BigDecimal weight = las.getWeight(issue);
|
---|
223 | for (Value val : las.getDomain().getValues(issue)) {
|
---|
224 | BigDecimal util = valset.getUtility(val).multiply(weight);
|
---|
225 | if (BigDecimal.ZERO.compareTo(util) != 0) {
|
---|
226 | utilslist.put(new ProductOfValue(Arrays.asList(val)), util);
|
---|
227 | }
|
---|
228 | }
|
---|
229 | map.put(issue, new PartsUtilities(Arrays.asList(issue), utilslist));
|
---|
230 | }
|
---|
231 | return map;
|
---|
232 | }
|
---|
233 |
|
---|
234 | /**
|
---|
235 | *
|
---|
236 | * @return error string, or null if no error (all parts seem fine)
|
---|
237 | */
|
---|
238 | private String checkParts() {
|
---|
239 |
|
---|
240 | Set<String> collectedIssues = new HashSet<>();
|
---|
241 | for (String partname : partUtilities.keySet()) {
|
---|
242 | PartsUtilities part = partUtilities.get(partname);
|
---|
243 | if (part == null) {
|
---|
244 | return "partUtilities " + partname + " contains null value";
|
---|
245 | }
|
---|
246 | List<String> issues = part.getIssues();
|
---|
247 | HashSet<String> intersection = new HashSet<String>(collectedIssues);
|
---|
248 | intersection.retainAll(issues);
|
---|
249 | if (!intersection.isEmpty()) {
|
---|
250 | return "issues " + intersection + " occur multiple times";
|
---|
251 | }
|
---|
252 | collectedIssues.addAll(issues);
|
---|
253 | }
|
---|
254 |
|
---|
255 | if (!collectedIssues.equals(getDomain().getIssuesValues())) {
|
---|
256 | return "parts must cover the domain issues "
|
---|
257 | + getDomain().getIssuesValues() + " but cover " + collectedIssues;
|
---|
258 | }
|
---|
259 | if (getMaxUtility().compareTo(BigDecimal.ONE) > 0) {
|
---|
260 | return "Max utility of the space exceedds 1";
|
---|
261 | }
|
---|
262 | return null;
|
---|
263 |
|
---|
264 | }
|
---|
265 |
|
---|
266 | /**
|
---|
267 | *
|
---|
268 | * @return the max possible utility in this utility space.
|
---|
269 | */
|
---|
270 | private BigDecimal getMaxUtility() {
|
---|
271 | //#PY return sum( [ partutils.getMaxUtility() for partutils in self._partUtilities.values() ] )
|
---|
272 | return partUtilities.values().stream()
|
---|
273 | .map(partutils -> partutils.getMaxUtility())
|
---|
274 | .reduce(BigDecimal.ZERO, BigDecimal::add);
|
---|
275 | }
|
---|
276 |
|
---|
277 | }
|
---|