package geniusweb.profile.utilityspace;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
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. The PartsUtilities must be complete:
* all possible combinations of all parts must cover all
* combinations of issue values and assign a proper utility
* in [0,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. Read the note about partial bids in
* {@link #getUtility(Bid)}.
* @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);
checkParts();
}
/**
* Copy settings in las. This will have the exact same utilities as the
* {@link LinearAdditive} space 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 static SumOfGroupsUtilitySpace create(LinearAdditive las) {
return new SumOfGroupsUtilitySpace(las.getDomain(), las.getName(),
las2parts(las), las.getReservationBid());
}
/**
*
* @return all partsutilities
*/
public Map getPartsUtilities() {
return Collections.unmodifiableMap(partUtilities);
}
/**
*
* Note: If bid is partial for a group, the utility of that group can not be
* evaluated and is set to 0.
*
*/
@Override
public BigDecimal getUtility(Bid bid) {
BigDecimal sum = BigDecimal.ZERO;
for (String partname : partUtilities.keySet()) {
sum = sum.add(util(partname, bid));
}
return sum;
}
@Override
public String toString() {
return "SumOfGroupsUtilitySpace[" + getName() + "," + 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 void checkParts() throws IllegalArgumentException {
Set collectedIssues = new HashSet<>();
for (String partname : partUtilities.keySet()) {
PartsUtilities part = partUtilities.get(partname);
if (part == null) {
throw new IllegalArgumentException(
"partUtilities " + partname + " contains null value");
}
List issues = part.getIssues();
HashSet intersection = new HashSet(collectedIssues);
intersection.retainAll(issues);
if (!intersection.isEmpty()) {
throw new IllegalArgumentException(
"issues " + intersection + " occur multiple times");
}
part.checkComplete(getDomain());
collectedIssues.addAll(issues);
}
if (!collectedIssues.equals(getDomain().getIssues())) {
throw new IllegalArgumentException(
"parts must cover the domain issues "
+ getDomain().getIssues() + " but cover "
+ collectedIssues);
}
if (getMaxUtility().compareTo(BigDecimal.ONE) > 0) {
throw new IllegalArgumentException(
"Max utility of the space exceedds 1");
}
}
/**
*
* @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);
}
}