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