package geniusweb.profile.utilityspace; import java.math.BigDecimal; import java.util.Collections; import java.util.HashMap; import java.util.Map; 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.Value; /** * Defines a UtilitySpace in terms of a weighted sum of per-issue preferences. * immutable. A {@link LinearAdditiveUtilitySpace} works with complete bids. * * Constructor guarantees that * */ @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE, isGetterVisibility = Visibility.NONE) public class LinearAdditiveUtilitySpace implements LinearAdditive { private final String name; /** * the key is the issue, the value is the valuesetutilities for this issue. * Should be immutable so do not return direct access to this field. */ private final HashMap issueUtilities = new HashMap<>(); /** * The key is the issue, the value is the weight of the utilities for that * issue (utility as returned from the ValueSetUtilities for that issue). */ private final HashMap issueWeights = new HashMap<>(); private final Domain domain; private final Bid reservationBid; /** * @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 utils a map with key: issue names (String) and value: the values * for that issue. There MUST NOT be a null issue. All values * MUST NOT be null. * @param weights the weight of each issue in the computation of the * weighted sum. The issues must be the same as those in the * utils map. All weights MUST NOT be null. The weights MUST * sum to 1. * @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 LinearAdditiveUtilitySpace(@JsonProperty("domain") Domain domain, @JsonProperty("name") String name, @JsonProperty("issueUtilities") Map utils, @JsonProperty("issueWeights") Map weights, @JsonProperty("reservationBid") Bid resBid) { this.domain = domain; this.name = name; this.reservationBid = resBid; this.issueUtilities.putAll(utils); this.issueWeights.putAll(weights); if (domain == null) { throw new NullPointerException("domain=null"); } if (utils == null) { throw new NullPointerException("utils=null"); } if (weights == null) { throw new NullPointerException("weights=null"); } if (utils.values().contains(null)) { throw new NullPointerException( "One of the ValueSetUtilities in issueUtilitiesis null:" + utils); } if (weights.values().contains(null)) { throw new NullPointerException("One of the weights is null"); } if (utils.keySet().contains(null)) { throw new NullPointerException("One of the issue names is null"); } if (name == null || !(name.matches("[a-zA-Z0-9]+"))) { throw new IllegalArgumentException( "Name must be simple (a-Z, 0-9) but got " + name); } if (!(utils.keySet().equals(domain.getIssues()))) { throw new IllegalArgumentException( "The issues in utilityspace and domain do not match: utilityspace has issues " + utils.keySet() + " but domain contains " + domain.getIssues()); } if (!(weights.keySet().equals(domain.getIssues()))) { throw new IllegalArgumentException( "The issues in weights and domain do not match: weights has " + weights.keySet() + " but domain contains " + domain.getIssues()); } for (String issue : issueUtilities.keySet()) { String message = issueUtilities.get(issue) .isFitting(domain.getValues(issue)); if (message != null) throw new IllegalArgumentException(message); } BigDecimal sum = weights.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add); if (BigDecimal.ONE.compareTo(sum) != 0) { // equals does NOT work for // comparing BigDecimals!! throw new IllegalArgumentException("The sum of the weights (" + weights.values() + ") must be 1"); } if (resBid != null) { String message = domain.isFitting(resBid); if (message != null) throw new IllegalArgumentException( "reservationbid is not fitting domain: " + message); } } @Override public BigDecimal getUtility(Bid bid) { return issueWeights.keySet().stream() .map(iss -> util(iss, bid.getValue(iss))) .reduce(BigDecimal.ZERO, BigDecimal::add); } @Override public BigDecimal getWeight(String issue) { return issueWeights.get(issue); } @Override public String toString() { return "LinearAdditive[" + issueUtilities + "," + issueWeights + "," + reservationBid + "]"; } @Override public Bid getReservationBid() { return reservationBid; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((domain == null) ? 0 : domain.hashCode()); result = prime * result + ((issueUtilities == null) ? 0 : issueUtilities.hashCode()); result = prime * result + ((issueWeights == null) ? 0 : issueWeights.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((reservationBid == null) ? 0 : reservationBid.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; LinearAdditiveUtilitySpace other = (LinearAdditiveUtilitySpace) obj; if (domain == null) { if (other.domain != null) return false; } else if (!domain.equals(other.domain)) return false; if (issueUtilities == null) { if (other.issueUtilities != null) return false; } else if (!issueUtilities.equals(other.issueUtilities)) return false; if (issueWeights == null) { if (other.issueWeights != null) return false; } else if (!issueWeights.equals(other.issueWeights)) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (reservationBid == null) { if (other.reservationBid != null) return false; } else if (!reservationBid.equals(other.reservationBid)) return false; return true; } @Override public Domain getDomain() { return domain; } @Override public String getName() { return name; } @Override public Map getUtilities() { return Collections.unmodifiableMap(issueUtilities); } @Override public Map getWeights() { return Collections.unmodifiableMap(issueWeights); } /** * * @param issue the issue to get weighted util for * @param value the issue value to use (typically coming from a bid) * @return weighted util of just the issue value: * issueUtilities[issue].utility(value) * issueWeights[issue) */ private BigDecimal util(String issue, Value value) { return issueWeights.get(issue) .multiply(issueUtilities.get(issue).getUtility(value)); } }