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