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);
}
}