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
*
* - weights are normalized to 1
*
- the issues in the utility map and weights map match those in the domain
*
- The utilities for each issue are proper {@link ValueSetUtilities} objects
*
*/
@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));
}
}