package geniusweb.opponentmodel;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import geniusweb.actions.Action;
import geniusweb.actions.Offer;
import geniusweb.issuevalue.Bid;
import geniusweb.issuevalue.Domain;
import geniusweb.issuevalue.NumberValue;
import geniusweb.issuevalue.Value;
import geniusweb.profile.utilityspace.NumberValueSetUtilities;
import geniusweb.profile.utilityspace.UtilitySpace;
import geniusweb.progress.Progress;
import geniusweb.references.Parameters;
/**
* implements an {@link OpponentModel} by counting frequencies of bids placed by
* the opponent.
*
* NOTE: {@link NumberValue}s are also treated as 'discrete', so the frequency
* of one value does not influence the influence the frequency of nearby values
* (as you might expect as {@link NumberValueSetUtilities} is only affected by
* the endpoints).
*
* immutable.
*/
public class FrequencyOpponentModel implements UtilitySpace, OpponentModel {
private static final int DECIMALS = 4; // accuracy of our computations.
private final Domain domain;
private final Map> bidFrequencies;
private final BigInteger totalBids;
private final Bid resBid;
public FrequencyOpponentModel() {
this.domain = null;
this.bidFrequencies = null;
this.totalBids = BigInteger.ZERO;
this.resBid = null;
}
/**
* internal constructor. Assumes the freqs keyset is equal to the available
* issues.
*
* @param domain the domain
* @param freqs the observed frequencies for all issue values. This map is
* assumed to be a fresh private-access only copy.
* @param total the total number of bids contained in the freqs map. This
* must be equal to the sum of the Integer values in the
* {@link #bidFrequencies} for each issue (this is not
* checked).
* @param resBid the reservation bid. Can be null
*/
protected FrequencyOpponentModel(Domain domain,
Map> freqs, BigInteger total,
Bid resBid) {
if (domain == null) {
throw new IllegalStateException("domain is not initialized");
}
this.domain = domain;
this.bidFrequencies = freqs;
this.totalBids = total;
this.resBid = resBid;
}
@Override
public FrequencyOpponentModel with(Domain newDomain, Bid newResBid) {
if (newDomain == null) {
throw new NullPointerException("domain is not initialized");
}
// FIXME merge already available frequencies?
return new FrequencyOpponentModel(newDomain,
newDomain.getIssues().stream().collect(
Collectors.toMap(iss -> iss, iss -> new HashMap<>())),
BigInteger.ZERO, newResBid);
}
@Override
public BigDecimal getUtility(Bid bid) {
if (domain == null) {
throw new IllegalStateException("domain is not initialized");
}
if (totalBids == BigInteger.ZERO) {
return BigDecimal.ONE;
}
BigDecimal sum = BigDecimal.ZERO;
// Assume all issues have equal weight.
for (String issue : domain.getIssues()) {
if (bid.getIssues().contains(issue)) {
sum = sum.add(getFraction(issue, bid.getValue(issue)));
}
}
return sum.divide(new BigDecimal(bidFrequencies.size()), DECIMALS,
BigDecimal.ROUND_HALF_UP);
}
@Override
public String getName() {
if (domain == null) {
throw new IllegalStateException("domain is not initialized");
}
return "FreqOppModel" + this.hashCode() + "For" + domain;
}
@Override
public Domain getDomain() {
return domain;
}
@Override
public FrequencyOpponentModel with(Action action, Progress progress) {
if (domain == null) {
throw new IllegalStateException("domain is not initialized");
}
if (!(action instanceof Offer))
return this;
Bid bid = ((Offer) action).getBid();
Map> newFreqs = cloneMap(bidFrequencies);
for (String issue : domain.getIssues()) {
Map freqs = newFreqs.get(issue);
Value value = bid.getValue(issue);
if (value != null) {
Integer oldfreq = freqs.get(value);
if (oldfreq == null) {
oldfreq = 0;
}
freqs.put(value, oldfreq + 1);
}
}
return new FrequencyOpponentModel(domain, newFreqs,
totalBids.add(BigInteger.ONE), resBid);
}
/**
*
* @param issue the issue to get frequency info for
* @return a map containing a map of values and the number of times that
* value was used in previous bids. Values that are possible but not
* in the map have frequency 0.
*/
public Map getCounts(String issue) {
if (domain == null) {
throw new IllegalStateException("domain is not initialized");
}
if (!(bidFrequencies.containsKey(issue))) {
return Collections.emptyMap();
}
return Collections.unmodifiableMap(bidFrequencies.get(issue));
}
@Override
public OpponentModel with(Parameters parameters) {
return this; // ignore parameters
}
/**
*
* @param issue the issue to check
* @param value the value to check
* @return the fraction of the total cases that bids contained given value
* for the issue.
*/
private BigDecimal getFraction(String issue, Value value) {
if (totalBids == BigInteger.ZERO) {
return BigDecimal.ONE;
}
Integer freq = bidFrequencies.get(issue).get(value);
if (freq == null) {
freq = 0;
}
return new BigDecimal(freq).divide(new BigDecimal(totalBids), DECIMALS,
BigDecimal.ROUND_HALF_UP);
}
/**
*
* @param freqs
* @return deep copy of freqs map.
*/
private static Map> cloneMap(
Map> freqs) {
Map> map = new HashMap<>();
for (String issue : freqs.keySet()) {
map.put(issue, new HashMap(freqs.get(issue)));
}
return map;
}
@Override
public Bid getReservationBid() {
return resBid;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((bidFrequencies == null) ? 0 : bidFrequencies.hashCode());
result = prime * result + ((domain == null) ? 0 : domain.hashCode());
result = prime * result
+ ((totalBids == null) ? 0 : totalBids.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;
FrequencyOpponentModel other = (FrequencyOpponentModel) obj;
if (bidFrequencies == null) {
if (other.bidFrequencies != null)
return false;
} else if (!bidFrequencies.equals(other.bidFrequencies))
return false;
if (domain == null) {
if (other.domain != null)
return false;
} else if (!domain.equals(other.domain))
return false;
if (totalBids == null) {
if (other.totalBids != null)
return false;
} else if (!totalBids.equals(other.totalBids))
return false;
return true;
}
@Override
public String toString() {
return "FrequencyOpponentModel[" + totalBids + "," + bidFrequencies
+ "]";
}
}