package org.ggp.base.util.gdl.model;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.ggp.base.util.concurrency.ConcurrencyUtils;
import org.ggp.base.util.gdl.GdlUtils;
import org.ggp.base.util.gdl.GdlVisitor;
import org.ggp.base.util.gdl.GdlVisitors;
import org.ggp.base.util.gdl.grammar.Gdl;
import org.ggp.base.util.gdl.grammar.GdlConstant;
import org.ggp.base.util.gdl.grammar.GdlDistinct;
import org.ggp.base.util.gdl.grammar.GdlLiteral;
import org.ggp.base.util.gdl.grammar.GdlNot;
import org.ggp.base.util.gdl.grammar.GdlOr;
import org.ggp.base.util.gdl.grammar.GdlPool;
import org.ggp.base.util.gdl.grammar.GdlRule;
import org.ggp.base.util.gdl.grammar.GdlSentence;
import org.ggp.base.util.gdl.grammar.GdlTerm;
import org.ggp.base.util.gdl.grammar.GdlVariable;
import org.ggp.base.util.gdl.model.SentenceDomainModels.VarDomainOpts;
import org.ggp.base.util.gdl.transforms.VariableConstrainer;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
public class SentenceDomainModelOptimizer {
/**
* Given a SentenceDomainModel, returns an ImmutableSentenceDomainModel
* with Cartesian domains that tries to minimize the domains of sentence
* forms without impacting the game rules. In particular, when sentences
* are restricted to these domains, the answers to queries about terminal,
* legal, goal, next, and init sentences will not change.
*
* Note that if a sentence form is not used in a meaningful way by the
* game, it may end up with an empty domain.
*
* The description for the game must have had the {@link VariableConstrainer}
* applied to it.
*/
public static ImmutableSentenceDomainModel restrictDomainsToUsefulValues(SentenceDomainModel oldModel) throws InterruptedException {
// Start with everything from the current domain model.
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> neededAndPossibleConstantsByForm = Maps.newHashMap();
for (SentenceForm form : oldModel.getSentenceForms()) {
neededAndPossibleConstantsByForm.put(form, HashMultimap.<Integer, GdlConstant>create());
addDomain(neededAndPossibleConstantsByForm.get(form), oldModel.getDomain(form), form);
}
/*
* To minimize the contents of the domains, we repeatedly go through two processes to reduce
* the domain:
*
* 1) We remove unneeded constants from the domain. These are constants which (in their
* position) do not contribute to any sentences with a GDL keyword as its name; that
* is, it never matters whether a sentence with that constant in that position is
* true or false.
* 2) We remove impossible constants from the domain. These are constants which cannot
* end up in their position via any rule or sentence in the game description, given
* the current domain.
*
* Constants removed because of one type of pass or the other may cause other constants
* in other sentence forms to become unneeded or impossible, so we make multiple passes
* until everything is stable.
*/
boolean somethingChanged = true;
while (somethingChanged) {
somethingChanged = removeUnneededConstants(neededAndPossibleConstantsByForm, oldModel);
somethingChanged |= removeImpossibleConstants(neededAndPossibleConstantsByForm, oldModel);
}
return toSentenceDomainModel(neededAndPossibleConstantsByForm, oldModel);
}
private static void addDomain(
SetMultimap<Integer, GdlConstant> setMultimap,
SentenceFormDomain domain,
SentenceForm form) {
for (int i = 0; i < form.getTupleSize(); i++) {
setMultimap.putAll(i, domain.getDomainForSlot(i));
}
}
private static boolean removeImpossibleConstants(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
SentenceFormModel model) throws InterruptedException {
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newPossibleConstantsByForm = Maps.newHashMap();
for (SentenceForm form : curDomains.keySet()) {
newPossibleConstantsByForm.put(form, HashMultimap.<Integer, GdlConstant>create());
}
populateInitialPossibleConstants(newPossibleConstantsByForm, curDomains, model);
boolean somethingChanged = true;
while (somethingChanged) {
somethingChanged = propagatePossibleConstants(newPossibleConstantsByForm, curDomains, model);
}
return retainNewDomains(curDomains, newPossibleConstantsByForm);
}
private static void populateInitialPossibleConstants(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newPossibleConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
SentenceFormModel model) throws InterruptedException {
//Add anything in the head of a rule...
for (GdlRule rule : getRules(model.getDescription())) {
GdlSentence head = rule.getHead();
addConstantsFromSentenceIfInOldDomain(newPossibleConstantsByForm, curDomains, model, head);
}
//... and any true sentences
for (SentenceForm form : model.getSentenceForms()) {
for (GdlSentence sentence : model.getSentencesListedAsTrue(form)) {
addConstantsFromSentenceIfInOldDomain(newPossibleConstantsByForm, curDomains, model, sentence);
}
}
}
private static boolean propagatePossibleConstants(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newPossibleConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomain,
SentenceFormModel model) throws InterruptedException {
//Injection: Go from the intersections of variable values in rules to the
//values in their heads
boolean somethingChanged = false;
for (GdlRule rule : getRules(model.getDescription())) {
GdlSentence head = rule.getHead();
Map<GdlVariable, Set<GdlConstant>> domainsOfHeadVars = Maps.newHashMap();
for (GdlVariable varInHead : ImmutableSet.copyOf(GdlUtils.getVariables(rule.getHead()))) {
Set<GdlConstant> domain = getVarDomainInRuleBody(varInHead, rule, newPossibleConstantsByForm, curDomain, model);
domainsOfHeadVars.put(varInHead, domain);
somethingChanged |= addPossibleValuesToSentence(domain, head, varInHead, newPossibleConstantsByForm, model);
}
}
//Language-based injections
somethingChanged |= applyLanguageBasedInjections(GdlPool.INIT, GdlPool.TRUE, newPossibleConstantsByForm);
somethingChanged |= applyLanguageBasedInjections(GdlPool.NEXT, GdlPool.TRUE, newPossibleConstantsByForm);
somethingChanged |= applyLanguageBasedInjections(GdlPool.LEGAL, GdlPool.DOES, newPossibleConstantsByForm);
return somethingChanged;
}
private static boolean applyLanguageBasedInjections(
GdlConstant curName,
GdlConstant resultingName,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newPossibleConstantsByForm) throws InterruptedException {
boolean somethingChanged = false;
for (SentenceForm form : newPossibleConstantsByForm.keySet()) {
ConcurrencyUtils.checkForInterruption();
if (form.getName() == curName) {
SentenceForm resultingForm = form.withName(resultingName);
SetMultimap<Integer, GdlConstant> curFormDomain = newPossibleConstantsByForm.get(form);
SetMultimap<Integer, GdlConstant> resultingFormDomain = newPossibleConstantsByForm.get(resultingForm);
somethingChanged |= resultingFormDomain.putAll(curFormDomain);
}
}
return somethingChanged;
}
private static Set<GdlConstant> getVarDomainInRuleBody(
GdlVariable varInHead,
GdlRule rule,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newPossibleConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomain,
SentenceFormModel model) {
try {
List<Set<GdlConstant>> domains = Lists.newArrayList();
for (GdlSentence conjunct : getPositiveConjuncts(rule.getBody())) {
if (GdlUtils.getVariables(conjunct).contains(varInHead)) {
domains.add(getVarDomainInSentence(varInHead, conjunct, newPossibleConstantsByForm, curDomain, model));
}
}
return getIntersection(domains);
} catch (RuntimeException e) {
throw new RuntimeException("Error in rule " + rule + " for variable " + varInHead, e);
}
}
private static Set<GdlConstant> getVarDomainInSentence(
GdlVariable var,
GdlSentence conjunct,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newPossibleConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomain,
SentenceFormModel model) {
SentenceForm form = model.getSentenceForm(conjunct);
List<GdlTerm> tuple = GdlUtils.getTupleFromSentence(conjunct);
List<Set<GdlConstant>> domains = Lists.newArrayList();
for (int i = 0; i < tuple.size(); i++) {
if (tuple.get(i) == var) {
domains.add(newPossibleConstantsByForm.get(form).get(i));
domains.add(curDomain.get(form).get(i));
}
}
return getIntersection(domains);
}
private static Set<GdlConstant> getIntersection(
List<Set<GdlConstant>> domains) {
if (domains.isEmpty()) {
throw new IllegalArgumentException("Unsafe rule has no positive conjuncts");
}
Set<GdlConstant> intersection = Sets.newHashSet(domains.get(0));
for (int i = 1; i < domains.size(); i++) {
Set<GdlConstant> curDomain = domains.get(i);
intersection.retainAll(curDomain);
}
return intersection;
}
private static boolean removeUnneededConstants(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
SentenceFormModel model) throws InterruptedException {
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newNeededConstantsByForm = Maps.newHashMap();
for (SentenceForm form : curDomains.keySet()) {
newNeededConstantsByForm.put(form, HashMultimap.<Integer, GdlConstant>create());
}
populateInitialNeededConstants(newNeededConstantsByForm, curDomains, model);
boolean somethingChanged = true;
while (somethingChanged) {
somethingChanged = propagateNeededConstants(newNeededConstantsByForm, curDomains, model);
}
return retainNewDomains(curDomains, newNeededConstantsByForm);
}
private static boolean retainNewDomains(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newDomains) {
boolean somethingChanged = false;
for (SentenceForm form : curDomains.keySet()) {
SetMultimap<Integer, GdlConstant> newDomain = newDomains.get(form);
somethingChanged |= curDomains.get(form).entries().retainAll(newDomain.entries());
}
return somethingChanged;
}
private static boolean propagateNeededConstants(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> neededConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
SentenceFormModel model) throws InterruptedException {
boolean somethingChanged = false;
somethingChanged |= applyRuleHeadPropagation(neededConstantsByForm, curDomains, model);
somethingChanged |= applyRuleBodyOnlyPropagation(neededConstantsByForm, curDomains, model);
return somethingChanged;
}
private static boolean applyRuleBodyOnlyPropagation(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> neededConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
SentenceFormModel model) throws InterruptedException {
boolean somethingChanged = false;
//If a variable does not appear in the head of a variable,
//then all the values that are in the intersections of all the
//domains from the positive conjuncts containing the variable
//become needed.
for (GdlRule rule : getRules(model.getDescription())) {
GdlSentence head = rule.getHead();
Set<GdlVariable> varsInHead = ImmutableSet.copyOf(GdlUtils.getVariables(head));
Map<GdlVariable, Set<GdlConstant>> varDomains = getVarDomains(rule, curDomains, model);
for (GdlVariable var : ImmutableSet.copyOf(GdlUtils.getVariables(rule))) {
if (!varsInHead.contains(var)) {
Set<GdlConstant> neededConstants = varDomains.get(var);
if (neededConstants == null) {
throw new IllegalStateException("var is " + var + ";\nvarDomains key set is " + varDomains.keySet() + ";\nvarsInHead is " + varsInHead +
";\nrule is " + rule);
}
for (GdlLiteral conjunct : rule.getBody()) {
somethingChanged |=
addPossibleValuesToConjunct(neededConstants, conjunct, var, neededConstantsByForm, model);
}
}
}
}
return somethingChanged;
}
private static Map<GdlVariable, Set<GdlConstant>> getVarDomains(
GdlRule rule,
final Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
final SentenceFormModel model) {
return SentenceDomainModels.getVarDomains(rule, new AbstractSentenceDomainModel(model) {
@Override
public SentenceFormDomain getDomain(final SentenceForm form) {
return new SentenceFormDomain() {
@Override
public SentenceForm getForm() {
return form;
}
@Override
public Iterator<GdlSentence> iterator() {
throw new UnsupportedOperationException();
}
@Override
public Set<GdlConstant> getDomainForSlot(int slotIndex) {
if (!curDomains.containsKey(form)) {
return ImmutableSet.of();
}
return curDomains.get(form).get(slotIndex);
}
};
}}, VarDomainOpts.INCLUDE_HEAD);
}
private static boolean applyRuleHeadPropagation(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> neededConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
SentenceFormModel model) throws InterruptedException {
boolean somethingChanged = false;
//If a term that is a variable in the head of a rule needs a
//particular value, AND that variable is possible (i.e. in the
//current domain) in every appearance of the variable in
//positive conjuncts in the rule's body, then the value is
//needed in every appearance of the variable in the rule
//(positive or negative).
for (GdlRule rule : getRules(model.getDescription())) {
GdlSentence head = rule.getHead();
SentenceForm headForm = model.getSentenceForm(head);
List<GdlTerm> headTuple = GdlUtils.getTupleFromSentence(head);
Map<GdlVariable, Set<GdlConstant>> varDomains = getVarDomains(rule, curDomains, model);
for (int i = 0; i < headTuple.size(); i++) {
ConcurrencyUtils.checkForInterruption();
if (headTuple.get(i) instanceof GdlVariable) {
GdlVariable curVar = (GdlVariable) headTuple.get(i);
Set<GdlConstant> neededConstants = neededConstantsByForm.get(headForm).get(i);
//Whittle these down based on what's possible throughout the rule
Set<GdlConstant> neededAndPossibleConstants = Sets.newHashSet(neededConstants);
neededAndPossibleConstants.retainAll(varDomains.get(curVar));
//Relay those values back to the conjuncts in the rule body
for (GdlLiteral conjunct : rule.getBody()) {
somethingChanged |= addPossibleValuesToConjunct(neededAndPossibleConstants, conjunct, curVar, neededConstantsByForm, model);
}
}
}
}
return somethingChanged;
}
private static boolean addPossibleValuesToConjunct(
Set<GdlConstant> neededAndPossibleConstants,
GdlLiteral conjunct,
GdlVariable curVar,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> neededConstantsByForm,
SentenceFormModel model) throws InterruptedException {
if (conjunct instanceof GdlSentence) {
return addPossibleValuesToSentence(neededAndPossibleConstants, (GdlSentence) conjunct, curVar, neededConstantsByForm, model);
} else if (conjunct instanceof GdlNot) {
GdlSentence innerSentence = (GdlSentence) ((GdlNot) conjunct).getBody();
return addPossibleValuesToSentence(neededAndPossibleConstants, innerSentence, curVar, neededConstantsByForm, model);
} else if (conjunct instanceof GdlOr) {
throw new IllegalArgumentException("The SentenceDomainModelOptimizer is not designed for game descriptions with OR. Use the DeORer.");
} else if (conjunct instanceof GdlDistinct) {
return false;
} else {
throw new IllegalArgumentException("Unexpected literal type " + conjunct.getClass() + " for literal " + conjunct);
}
}
private static boolean addPossibleValuesToSentence(
Set<GdlConstant> neededAndPossibleConstants,
GdlSentence sentence,
GdlVariable curVar,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> neededConstantsByForm,
SentenceFormModel model) throws InterruptedException {
ConcurrencyUtils.checkForInterruption();
boolean somethingChanged = false;
SentenceForm form = model.getSentenceForm(sentence);
List<GdlTerm> tuple = GdlUtils.getTupleFromSentence(sentence);
Preconditions.checkArgument(form.getTupleSize() == tuple.size());
for (int i = 0; i < tuple.size(); i++) {
if (tuple.get(i) == curVar) {
Preconditions.checkNotNull(neededConstantsByForm.get(form));
Preconditions.checkNotNull(neededAndPossibleConstants);
somethingChanged |= neededConstantsByForm.get(form).putAll(i, neededAndPossibleConstants);
}
}
return somethingChanged;
}
private static Iterable<GdlSentence> getPositiveConjuncts(List<GdlLiteral> body) {
return Iterables.transform(Iterables.filter(body, new Predicate<GdlLiteral>() {
@Override
public boolean apply(GdlLiteral input) {
return input instanceof GdlSentence;
}
}), new Function<GdlLiteral, GdlSentence>() {
@Override
public GdlSentence apply(GdlLiteral input) {
return (GdlSentence) input;
}
});
}
// Unlike getPositiveConjuncts, this also returns sentences inside NOT literals.
private static List<GdlSentence> getAllSentencesInBody(List<GdlLiteral> body) {
final List<GdlSentence> sentences = Lists.newArrayList();
GdlVisitors.visitAll(body, new GdlVisitor() {
@Override
public void visitSentence(GdlSentence sentence) {
sentences.add(sentence);
}
});
return sentences;
}
private static Iterable<GdlRule> getRules(List<Gdl> description) {
return Iterables.transform(Iterables.filter(description, new Predicate<Gdl>() {
@Override
public boolean apply(Gdl input) {
return input instanceof GdlRule;
}
}), new Function<Gdl, GdlRule>() {
@Override
public GdlRule apply(Gdl input) {
return (GdlRule) input;
}
});
}
private static final ImmutableSet<GdlConstant> ALWAYS_NEEDED_SENTENCE_NAMES = ImmutableSet.of(
GdlPool.NEXT,
GdlPool.GOAL,
GdlPool.LEGAL,
GdlPool.INIT,
GdlPool.ROLE,
GdlPool.BASE,
GdlPool.INPUT,
GdlPool.TRUE,
GdlPool.DOES);
private static void populateInitialNeededConstants(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newNeededConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> curDomains,
SentenceFormModel model) throws InterruptedException {
// If the term model is part of a keyword-named sentence,
// then it is needed. This includes base and init.
for (SentenceForm form : model.getSentenceForms()) {
ConcurrencyUtils.checkForInterruption();
GdlConstant name = form.getName();
if (ALWAYS_NEEDED_SENTENCE_NAMES.contains(name)) {
newNeededConstantsByForm.get(form).putAll(curDomains.get(form));
}
}
// If the term has a constant value in some sentence in the
// BODY of a rule, then it is needed.
for (GdlRule rule : getRules(model.getDescription())) {
for (GdlSentence sentence : getAllSentencesInBody(rule.getBody())) {
addConstantsFromSentenceIfInOldDomain(newNeededConstantsByForm, curDomains, model, sentence);
}
}
}
private static void addConstantsFromSentenceIfInOldDomain(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> newConstantsByForm,
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> oldDomain,
SentenceFormModel model, GdlSentence sentence) throws InterruptedException {
SentenceForm form = model.getSentenceForm(sentence);
List<GdlTerm> tuple = GdlUtils.getTupleFromSentence(sentence);
if (tuple.size() != form.getTupleSize()) {
throw new IllegalStateException();
}
for (int i = 0; i < form.getTupleSize(); i++) {
ConcurrencyUtils.checkForInterruption();
GdlTerm term = tuple.get(i);
if (term instanceof GdlConstant) {
Set<GdlConstant> oldDomainForTerm = oldDomain.get(form).get(i);
if (oldDomainForTerm.contains(term)) {
newConstantsByForm.get(form).put(i, (GdlConstant) term);
}
}
}
}
private static ImmutableSentenceDomainModel toSentenceDomainModel(
Map<SentenceForm, SetMultimap<Integer, GdlConstant>> neededAndPossibleConstantsByForm,
SentenceFormModel formModel) throws InterruptedException {
Map<SentenceForm, SentenceFormDomain> domains = Maps.newHashMap();
for (SentenceForm form : formModel.getSentenceForms()) {
ConcurrencyUtils.checkForInterruption();
domains.put(form, CartesianSentenceFormDomain.create(form,
neededAndPossibleConstantsByForm.get(form)));
}
return ImmutableSentenceDomainModel.create(formModel, domains);
}
}