package geniusweb.bidspace; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNull; import geniusweb.issuevalue.Bid; import geniusweb.issuevalue.Domain; import geniusweb.issuevalue.Value; import geniusweb.profile.utilityspace.LinearAdditive; import tudelft.utilities.immutablelist.AbstractImmutableList; import tudelft.utilities.immutablelist.FixedList; import tudelft.utilities.immutablelist.ImmutableList; import tudelft.utilities.immutablelist.JoinedList; import tudelft.utilities.immutablelist.MapList; import tudelft.utilities.immutablelist.Tuple; /** * Tool class containing functions dealing with utilities of all bids in a given * {@link LinearAdditive}. This class caches previously computed values to * accelerate the calls and subsequent calls. Re-use the object to keep/reuse * the cache. *

Rounding

Internally, utilities of bids are rounded to the given * precision. This may cause inclusion/exclusion of some bids in the results. * See {@link #BidsWithUtility(LinearAdditive, int)} for more details */ public class BidsWithUtility { private final @NonNull List<@NonNull IssueInfo> issueInfo; private final int precision; // #digits used for Intervals /** * cache. Key = call arguments for {@link #get(int, Interval)}. Value=return * value of that call. */ private final @NonNull Map<@NonNull Tuple<@NonNull Integer, @NonNull Interval>, @NonNull ImmutableList<@NonNull Bid>> cache = new HashMap<>(); private final @NonNull ImmutableList<@NonNull Bid> emptylist = new FixedList<>(); /** * Default constructor, uses default precision 6. This value seems practical * for the common range of issues, utilities and weights. See * {@link #BidsWithUtility(LinearAdditive, int)} for more details on the * precision. * * @param space the {@link LinearAdditive} to analyze */ public BidsWithUtility(@NonNull LinearAdditive space) { this(space, 6); } /** * * @param space the {@link LinearAdditive} to analyze * @param precision the number of digits to use for computations. In * practice, 6 seems a good default value. *

* All utilities * weight are rounded to this number of * digits. This value should match the max number of * (digits used in the weight of an issue + number of * digits used in the issue utility). To determine the * optimal value, one may consider the step size of the * issues, and the range of interest. For instance if the * utility function has values 1/3 and 2/3, then these have * an 'infinite' number of relevant digits. But if the goal * is to search bids between utility 0.1 and 0.2, then * computing in 2 digits might already be sufficient. *

* This algorithm has memory and space complexity O( * |nissues| 10^precision ). For spaces up to 7 issues, 7 * digits should be feasible; for 9 issues, 6 digits may be * the maximum. */ public BidsWithUtility(@NonNull LinearAdditive space, int precision) { this(getInfo(space, precision), precision); } /** * * @param issuesInfo List of the relevant issues (in order of relevance) and * all info of each issue. Must not be empty. * @param precision the number of digits used in Intervals. */ public BidsWithUtility(@NonNull List<@NonNull IssueInfo> issuesInfo, int precision) { if (issuesInfo == null || issuesInfo.isEmpty()) { throw new IllegalArgumentException( "sortedissues list must contain at least 1 element"); } this.issueInfo = issuesInfo; this.precision = precision; } /** * @return the (rounded) utility {@link Interval} of this space: minimum and * maximum achievable utility. */ public @NonNull Interval getRange() { return getRange(issueInfo.size() - 1); } /** * * @param range the minimum and maximum utility required of the bids. to be * included (both ends inclusive). * @return a list with bids that have a (rounded) utility inside range. * possibly empty. */ public @NonNull ImmutableList<@NonNull Bid> getBids( @NonNull Interval range) { return get(issueInfo.size() - 1, range.round(precision)); } public @NonNull List<@NonNull IssueInfo> getInfo() { return Collections.unmodifiableList(issueInfo); } /** * * @param isMax the extreme bid required * @return the extreme bid, either the minimum if isMax=false or maximum if * isMax=true */ public @NonNull Bid getExtremeBid(boolean isMax) { final @NonNull Map<@NonNull String, @NonNull Value> map = new HashMap<>(); for (final @NonNull IssueInfo info : issueInfo) { map.put(info.getName(), info.getExtreme(isMax)); } return new Bid(map); } /** * Create partial BidsWithUtil list considering only issues 0..n, with * utilities in given range. *

Memory use

Memory use of the return value can be large, if large * domains are given and the requested interval contains large numbers of * bids. To give some idea, here are some typical memory uses: * * * * * * * * * * * * * * * * * * * * * *
domainmem use (MB) of returned objectnr. of selected bids
jobs10.02823
7issues134346327
9issues19837160666
* * @param n the number of issueRanges to consider, we consider 0..n here. * The recursion decreases n until n=0 * @param goal the minimum and maximum utility required of the bids. to be * included (both ends inclusive) * @return BidsWithUtil list, possibly empty. */ @SuppressWarnings("unused") protected @NonNull ImmutableList<@NonNull Bid> get(int n, @NonNull Interval goal) { if (goal == null) { throw new NullPointerException("Interval=null"); } // clamp goal into what is reachable. Avoid caching empty goal = goal.intersect(getRange(n)); if (goal.isEmpty()) { return new FixedList<>(); } final @NonNull Tuple<@NonNull Integer, @NonNull Interval> cachetuple = new Tuple<>( n, goal); final ImmutableList<@NonNull Bid> cached = cache.get(cachetuple); if (cached != null) { // hits++; return cached; } final @NonNull ImmutableList<@NonNull Bid> result = checkedGet(n, goal); cache.put(cachetuple, result); return result; } private static @NonNull List<@NonNull IssueInfo> getInfo( @NonNull LinearAdditive space2, int precision) { final @NonNull Domain dom = space2.getDomain(); /*#PY * return [ IssueInfo(issue, dom.getValues(issue), * space2.getUtilities().get(issue), * space2.getWeight(issue), precision)) * for issue in space2.getDomain().getIssues() ] */ return space2.getDomain().getIssuesValues().stream() .map(issue -> new IssueInfo(issue, dom.getValues(issue), space2.getUtilities().get(issue), space2.getWeight(issue), precision)) .collect(Collectors.toList()); } private @NonNull ImmutableList<@NonNull Bid> checkedGet(int n, final @NonNull Interval goal) { final @NonNull IssueInfo info = issueInfo.get(n); // issue is the first issuesWithRange. final @NonNull String issue = info.getName(); if (n == 0) return new OneIssueSubset(info, goal); // make new list, joining all sub-lists @NonNull ImmutableList<@NonNull Bid> fulllist = emptylist; for (@NonNull Value val : info.getValues()) { BigDecimal weightedutil = info.getWeightedUtil(val); @NonNull Interval subgoal = goal.subtract(weightedutil); // recurse: get list of bids for the subspace @NonNull ImmutableList<@NonNull Bid> partialbids = get(n - 1, subgoal); ImmutableList<@NonNull Bid> fullbids = new MapList<@NonNull Bid, @NonNull Bid>( (Function<@NonNull Bid, @NonNull Bid>) pbid -> pbid .merge(new Bid(issue, val)), partialbids); if (!fullbids.size().equals(BigInteger.ZERO)) fulllist = new JoinedList<@NonNull Bid>(fullbids, fulllist); } return fulllist; } /** * @param n the maximum issuevalue utility to include. Use n=index of last * issue s= (#issues in the domain - 1) for the full range of this * domain. * @return Interval (min, max) of the total weighted utility Interval of * issues 0..n. All weighted utilities have been rounded to the set * {@link #precision} */ private @NonNull Interval getRange(int n) { @NonNull Interval value = Interval.ZERO; for (int i = 0; i <= n; i = i + 1) { value = value.add(issueInfo.get(i).getInterval()); } return value; } } /** * List of all one-issue bids that have utility inside given interval. */ class OneIssueSubset extends AbstractImmutableList<@NonNull Bid> { private final @NonNull IssueInfo info; private final @NonNull Interval interval; private @NonNull BigInteger size; /** * * @param info the {@link IssueInfo} * @param interval a utility interval (weighted) */ OneIssueSubset(@NonNull IssueInfo info, @NonNull Interval interval) { this.info = info; this.interval = interval; this.size = BigInteger.valueOf(info.subsetSize(interval)); } @Override public @NonNull Bid get(@NonNull BigInteger index) { return new Bid(info.getName(), info.subset(interval).get(index.intValue())); } @Override public @NonNull BigInteger size() { return size; } }