package geniusweb.profile.utilityspace; import java.math.BigDecimal; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import geniusweb.issuevalue.Bid; import geniusweb.issuevalue.Domain; import geniusweb.issuevalue.NumberValueSet; import geniusweb.issuevalue.Value; import geniusweb.profile.DefaultProfile; /** * This is a utility space that defines the utility of bids as a sum of the * utilities of a number of subsets, or groups, of the issue values. *

* A group defines the utility of a non-empty subset of the issues in the * domain. The parts are non-overlapping. Missing issue values have utility 0. *

* This space enables handling {@link UtilitySpace}s that are not simply linear * additive, but with interacting issue values. For example, if you would like * to have a car with good speakers but only if there is also a good hifi set, * you can make a part that has {@link PartsUtilities} like * { { speakers:yes, hifi:yes }:1, {speakers, no: hifh:yes}:0.2, ...} *

* NOTICE this space is completely discrete. There is no equivalent of the * {@link NumberValueSet} here. */ @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE, isGetterVisibility = Visibility.NONE) public class SumOfGroupsUtilitySpace extends DefaultProfile implements UtilitySpace { /** * the key is the part name, the value is the valuesetutilities for this * issue. Should be immutable so do not return direct access to this field. */ private final HashMap partUtilities = new HashMap<>(); /** * @param domain the {@link Domain} in which this profile is defined. * @param name the name of this profile. Must be simple name (a-Z, 0-9) * @param partutils a map with key: part names (String) and value: the * {@link PartsUtilities} for that part. There MUST NOT be * a null part name. All values MUST NOT be null. The * PartsUtilities must match the * @param resBid the reservation bid. Only bids that are * {@link #isPreferredOrEqual(Bid, Bid)} should be * accepted. Can be null, meaning that there is no * reservation bid and any agreement is better than no * agreement. * @throws NullPointerException if values are incorrectly null. * @throws IllegalArgumentException if preconditions not met. */ @JsonCreator public SumOfGroupsUtilitySpace(@JsonProperty("domain") Domain domain, @JsonProperty("name") String name, @JsonProperty("partUtilities") HashMap partutils, @JsonProperty("reservationBid") Bid resBid) { super(name, domain, resBid); this.partUtilities.putAll(partutils); String err = checkParts(); if (err != null) { throw new IllegalArgumentException(err); } } /** * Copy settings in las. This will have the exact same utilities as the las * but this then gives you the power to change it into a non-linear space by * grouping. * * @param las the {@link LinearAdditive} to be converted/copied. * */ public SumOfGroupsUtilitySpace(LinearAdditive las) { this(las.getDomain(), las.getName(), las2parts(las), las.getReservationBid()); } @Override public BigDecimal getUtility(Bid bid) { return partUtilities.keySet().stream() .map(partname -> util(partname, bid)) .reduce(BigDecimal.ZERO, BigDecimal::add); } @Override public String toString() { return "PartialAdditive[" + partUtilities + "," + getReservationBid() + "]"; } /** * * @param partnames the partnames to remove from this. There must be at * least 2 parts * @param newpartname the name of the new part that contains all partnames, * grouped into 1 "issue". This name must not be an issue * in this. * @return new {@link SumOfGroupsUtilitySpace} that takes all partnames out * of this, and makes a newaprtname that contains these. */ public SumOfGroupsUtilitySpace group(List partnames, String newpartname) { final Set allpartnames = partUtilities.keySet(); if (partnames.size() < 2) { throw new IllegalArgumentException( "Group must contain at least 2 parts"); } if (allpartnames.contains(newpartname)) { throw new IllegalArgumentException( "newpartname " + newpartname + " is already in use"); } for (String name : partnames) { if (!allpartnames.contains(name)) { throw new IllegalArgumentException("Unknown part name " + name); } } HashMap newutils = new HashMap(); PartsUtilities newpartutils = null; for (String name : partUtilities.keySet()) { if (partnames.contains(name)) { if (newpartutils == null) { newpartutils = partUtilities.get(name); } else { newpartutils = newpartutils.add(partUtilities.get(name)); } } else { newutils.put(name, partUtilities.get(name)); } } newutils.put(newpartname, newpartutils); return new SumOfGroupsUtilitySpace(getDomain(), getName(), newutils, getReservationBid()); } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((partUtilities == null) ? 0 : partUtilities.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; if (getClass() != obj.getClass()) return false; SumOfGroupsUtilitySpace other = (SumOfGroupsUtilitySpace) obj; if (partUtilities == null) { if (other.partUtilities != null) return false; } else if (!partUtilities.equals(other.partUtilities)) return false; return true; } /***************************** private funcs ***************************/ /** * * @param partname the name of the part to get the utility of * @param bid the bid * @return weighted util of just the part: utilities[part].getUtility(part * of bid) */ private BigDecimal util(String partname, Bid bid) { PartsUtilities partutils = partUtilities.get(partname); ProductOfValue value = collectValues(bid, partutils); return partutils.getUtility(value); } /** * * @param bid a full bid * @param partutils the part utils for which a list of values from the bid * is needed * @return a list of values from the given bid, ordered as indicated in * partutils */ private ProductOfValue collectValues(Bid bid, PartsUtilities partutils) { List values = new LinkedList<>(); for (String issue : partutils.getIssues()) { values.add(bid.getValue(issue)); } return new ProductOfValue(values); } /** * * @param las a {@link LinearAdditive} * @return a Map with partname-PartsUtilities. The partnames are identical * to the issues in the given las. */ private static HashMap las2parts( LinearAdditive las) { HashMap map = new HashMap<>(); for (String issue : las.getUtilities().keySet()) { ValueSetUtilities valset = las.getUtilities().get(issue); Map utilslist = new HashMap<>(); BigDecimal weight = las.getWeight(issue); for (Value val : las.getDomain().getValues(issue)) { BigDecimal util = valset.getUtility(val).multiply(weight); if (BigDecimal.ZERO.compareTo(util) != 0) { utilslist.put(new ProductOfValue(Arrays.asList(val)), util); } } map.put(issue, new PartsUtilities(Arrays.asList(issue), utilslist)); } return map; } /** * * @return error string, or null if no error (all parts seem fine) */ private String checkParts() { Set collectedIssues = new HashSet<>(); for (String partname : partUtilities.keySet()) { PartsUtilities part = partUtilities.get(partname); if (part == null) { return "partUtilities " + partname + " contains null value"; } List issues = part.getIssues(); HashSet intersection = new HashSet(collectedIssues); intersection.retainAll(issues); if (!intersection.isEmpty()) { return "issues " + intersection + " occur multiple times"; } collectedIssues.addAll(issues); } if (!collectedIssues.equals(getDomain().getIssues())) { return "parts must cover the domain issues " + getDomain().getIssues() + " but cover " + collectedIssues; } if (getMaxUtility().compareTo(BigDecimal.ONE) > 0) { return "Max utility of the space exceedds 1"; } return null; } /** * * @return the max possible utility in this utility space. */ private BigDecimal getMaxUtility() { return partUtilities.values().stream() .map(partutils -> partutils.getMaxUtility()) .reduce(BigDecimal.ZERO, BigDecimal::add); } }