Java tutorial
/* * GRAKN.AI - THE KNOWLEDGE GRAPH * Copyright (C) 2018 Grakn Labs Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package grakn.core.graql.reasoner.rule; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import grakn.core.concept.Concept; import grakn.core.concept.answer.ConceptMap; import grakn.core.concept.type.Rule; import grakn.core.concept.type.SchemaConcept; import grakn.core.graql.reasoner.atom.Atom; import grakn.core.graql.reasoner.atom.Atomic; import grakn.core.graql.reasoner.atom.binary.AttributeAtom; import grakn.core.graql.reasoner.atom.binary.RelationAtom; import grakn.core.graql.reasoner.atom.binary.TypeAtom; import grakn.core.graql.reasoner.atom.predicate.ValuePredicate; import grakn.core.graql.reasoner.query.ReasonerAtomicQuery; import grakn.core.graql.reasoner.query.ReasonerQueries; import grakn.core.graql.reasoner.query.ReasonerQueryImpl; import grakn.core.graql.reasoner.query.ResolvableQuery; import grakn.core.graql.reasoner.state.QueryStateBase; import grakn.core.graql.reasoner.state.ResolutionState; import grakn.core.graql.reasoner.state.RuleState; import grakn.core.graql.reasoner.unifier.MultiUnifier; import grakn.core.graql.reasoner.unifier.Unifier; import grakn.core.graql.reasoner.unifier.UnifierType; import grakn.core.server.kb.concept.ConceptUtils; import grakn.core.server.session.TransactionOLTP; import graql.lang.Graql; import graql.lang.pattern.Conjunction; import graql.lang.pattern.Pattern; import graql.lang.statement.Statement; import graql.lang.statement.Variable; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.stream.Collectors.toSet; /** * * <p> * Class providing resolution and higher level facilities for {@link Rule} objects. * </p> * * */ public class InferenceRule { private final TransactionOLTP tx; private final Rule rule; private final ResolvableQuery body; private final ReasonerAtomicQuery head; private long priority = Long.MAX_VALUE; private Atom conclusionAtom = null; private Boolean requiresMaterialisation = null; public InferenceRule(Rule rule, TransactionOLTP tx) { this.tx = tx; this.rule = rule; //TODO simplify once changes propagated to rule objects this.body = ReasonerQueries.resolvable(Iterables.getOnlyElement(rule.when().getNegationDNF().getPatterns()), tx); this.head = ReasonerQueries.atomic(conjunction(rule.then()), tx); } private InferenceRule(ReasonerAtomicQuery head, ResolvableQuery body, Rule rule, TransactionOLTP tx) { this.tx = tx; this.rule = rule; this.head = head; this.body = body; } @Override public String toString() { return "\n" + this.body.toString() + "->\n" + this.head.toString() + "]\n"; } @Override public boolean equals(Object obj) { if (obj == null || this.getClass() != obj.getClass()) return false; InferenceRule rule = (InferenceRule) obj; return this.getBody().equals(rule.getBody()) && this.getHead().equals(rule.getHead()); } @Override public int hashCode() { int hashCode = 1; hashCode = hashCode * 37 + getBody().hashCode(); hashCode = hashCode * 37 + getHead().hashCode(); return hashCode; } /** * @return the priority with which the rule should be fired */ public long resolutionPriority() { if (priority == Long.MAX_VALUE) { //NB: this has to be relatively lightweight as it is called on each rule //TODO come with a more useful metric boolean bodyRuleResolvable = getBody().getAtoms(Atom.class).map(Atom::getSchemaConcept) .filter(Objects::nonNull).map(Concept::asType) .anyMatch(t -> t.thenRules().findFirst().isPresent()); priority = bodyRuleResolvable ? -1 : 0; } return priority; } private Conjunction<Statement> conjunction(Pattern pattern) { Set<Statement> vars = pattern.getDisjunctiveNormalForm().getPatterns().stream() .flatMap(p -> p.getPatterns().stream()).collect(toSet()); return Graql.and(vars); } public Rule getRule() { return rule; } /** * @return true if the rule has disconnected head, i.e. head and body do not share any variables */ private boolean hasDisconnectedHead() { return Sets.intersection(body.getVarNames(), head.getVarNames()).isEmpty(); } /** * @return true if head satisfies the pattern specified in the body of the rule */ boolean headSatisfiesBody() { Set<Atomic> atoms = new HashSet<>(getHead().getAtoms()); Set<Variable> headVars = getHead().getVarNames(); getBody().getAtoms(TypeAtom.class).filter(t -> !t.isRelation()) .filter(t -> !Sets.intersection(t.getVarNames(), headVars).isEmpty()).forEach(atoms::add); return getBody().isEquivalent(ReasonerQueries.create(atoms, tx)); } /** * rule requires materialisation in the context of resolving parent atom * if parent atom requires materialisation, head atom requires materialisation or if the head contains only fresh variables * * @return true if the rule needs to be materialised */ public boolean requiresMaterialisation(Atom parentAtom) { if (requiresMaterialisation == null) { requiresMaterialisation = parentAtom.requiresMaterialisation() || getHead().getAtom().requiresMaterialisation() || hasDisconnectedHead(); } return requiresMaterialisation; } /** * @return body of the rule of the form head :- body */ public ResolvableQuery getBody() { return body; } /** * @return head of the rule of the form head :- body */ public ReasonerAtomicQuery getHead() { return head; } /** * @return reasoner query formed of combining head and body queries */ private ReasonerQueryImpl getCombinedQuery() { Set<Atomic> allAtoms = new HashSet<>(body.getAtoms()); //NB: if rule acts as a sub, do not include type overlap boolean subHead = head.getAtom().isType(); if (subHead) { body.getAtoms().stream().filter(Atomic::isType) .filter(at -> at.getVarName().equals(head.getAtom().getVarName())).forEach(allAtoms::remove); } allAtoms.add(head.getAtom()); return ReasonerQueries.create(allAtoms, tx); } /** * @return a conclusion atom which parent contains all atoms in the rule */ public Atom getRuleConclusionAtom() { if (conclusionAtom == null) { conclusionAtom = getCombinedQuery().getAtoms(Atom.class).filter(at -> at.equals(head.getAtom())) .findFirst().orElse(null); } return conclusionAtom; } /** * @param parentAtom atom containing constraints (parent) * @param unifier unifier unifying parent with the rule * @return rule with propagated constraints from parent */ private InferenceRule propagateConstraints(Atom parentAtom, Unifier unifier) { if (!parentAtom.isRelation() && !parentAtom.isResource()) return this; Atom headAtom = head.getAtom(); //we are only rewriting the conjunction atoms (not complement atoms) as //the constraints are propagated from the conjunctive part anyway and //all variables in the -ve part not referenced in the +ve part have a different scope ReasonerQueryImpl bodyConjunction = getBody().asComposite().getConjunctiveQuery(); Set<Atomic> bodyConjunctionAtoms = new HashSet<>(bodyConjunction.getAtoms()); //transfer value predicates Set<Variable> bodyVars = bodyConjunction.getVarNames(); Set<ValuePredicate> vpsToPropagate = parentAtom.getPredicates(ValuePredicate.class) .flatMap(vp -> vp.unify(unifier).stream()).filter(vp -> bodyVars.contains(vp.getVarName())) .collect(toSet()); bodyConjunctionAtoms.addAll(vpsToPropagate); //if head is a resource merge vps into head if (headAtom.isResource()) { AttributeAtom resourceHead = (AttributeAtom) headAtom; if (resourceHead.getMultiPredicate().isEmpty()) { Set<ValuePredicate> innerVps = parentAtom.getInnerPredicates(ValuePredicate.class) .flatMap(vp -> vp.unify(unifier).stream()).collect(toSet()); bodyConjunctionAtoms.addAll(innerVps); headAtom = AttributeAtom.create(resourceHead.getPattern(), resourceHead.getAttributeVariable(), resourceHead.getRelationVariable(), resourceHead.getPredicateVariable(), resourceHead.getTypeId(), innerVps, resourceHead.getParentQuery()); } } Set<TypeAtom> unifiedTypes = parentAtom.getTypeConstraints().flatMap(type -> type.unify(unifier).stream()) .collect(toSet()); //set rule body types to sub types of combined query+rule types Set<TypeAtom> ruleTypes = bodyConjunction.getAtoms(TypeAtom.class).filter(t -> !t.isRelation()) .collect(toSet()); Set<TypeAtom> allTypes = Sets.union(unifiedTypes, ruleTypes); allTypes.stream().filter(ta -> { SchemaConcept schemaConcept = ta.getSchemaConcept(); SchemaConcept subType = allTypes.stream().map(Atom::getSchemaConcept).filter(Objects::nonNull) .filter(t -> ConceptUtils.nonMetaSups(t).contains(schemaConcept)).findFirst().orElse(null); return schemaConcept == null || subType == null; }).forEach(t -> bodyConjunctionAtoms.add(t.copy(body))); ReasonerQueryImpl rewrittenBodyConj = ReasonerQueries.create(bodyConjunctionAtoms, tx); ResolvableQuery rewrittenBody = getBody().isComposite() ? ReasonerQueries.composite(rewrittenBodyConj, getBody().asComposite().getComplementQueries(), tx) : rewrittenBodyConj; return new InferenceRule(ReasonerQueries.atomic(headAtom), rewrittenBody, rule, tx); } /** * @return true if the application of the rule results in type redefinition */ public boolean redefinesType() { Variable instanceVariable = getHead().getAtom().getVarName(); return getBody().getVarNames().contains(instanceVariable); } /** * @return true if the application of the rule results in addition of roleplayers to existing relations */ public boolean appendsRolePlayers() { Atom headAtom = getHead().getAtom(); SchemaConcept headType = headAtom.getSchemaConcept(); if (headType.isRelationType() && headAtom.getVarName().isReturned()) { RelationAtom bodyAtom = getBody().getAtoms(RelationAtom.class) .filter(at -> Objects.nonNull(at.getSchemaConcept())) .filter(at -> at.getSchemaConcept().equals(headType)).filter(at -> at.getVarName().isReturned()) .findFirst().orElse(null); return bodyAtom != null; } return false; } private InferenceRule rewriteHeadToRelation(Atom parentAtom) { if (parentAtom.isRelation() && getHead().getAtom().isResource()) { return new InferenceRule(ReasonerQueries.atomic(getHead().getAtom().toRelationAtom()), getBody(), rule, tx); } return this; } private InferenceRule rewriteVariables(Atom parentAtom) { if (parentAtom.isUserDefined() || parentAtom.requiresRoleExpansion()) { ReasonerAtomicQuery rewrittenHead = ReasonerQueries .atomic(head.getAtom().rewriteToUserDefined(parentAtom)); Stream<Atom> bodyConjAtoms = getBody().isComposite() ? getBody().asComposite().getConjunctiveQuery().getAtoms(Atom.class) : getBody().getAtoms(Atom.class); //NB: only rewriting atoms from the same type hierarchy List<Atom> rewrittenBodyConjAtoms = bodyConjAtoms .map(at -> ConceptUtils.areDisjointTypes(at.getSchemaConcept(), head.getAtom().getSchemaConcept(), false) ? at : at.rewriteToUserDefined(parentAtom)) .collect(Collectors.toList()); ReasonerQueryImpl rewrittenBodyConj = ReasonerQueries.create(rewrittenBodyConjAtoms, tx); ResolvableQuery rewrittenBody = getBody().isComposite() ? ReasonerQueries.composite(rewrittenBodyConj, getBody().asComposite().getComplementQueries(), tx) : rewrittenBodyConj; //NB we don't have to rewrite complements as we don't allow recursion atm return new InferenceRule(rewrittenHead, rewrittenBody, rule, tx); } return this; } private InferenceRule rewriteBodyAtoms() { if (getBody().requiresDecomposition()) { return new InferenceRule(getHead(), getBody().rewrite(), rule, tx); } return this; } /** * rewrite the rule to a form with user defined variables * @param parentAtom reference parent atom * @return rewritten rule */ public InferenceRule rewrite(Atom parentAtom) { return this.rewriteBodyAtoms().rewriteHeadToRelation(parentAtom).rewriteVariables(parentAtom); } /** * @param parentAtom atom to unify the rule with * @return corresponding unifier */ public MultiUnifier getMultiUnifier(Atom parentAtom) { Atom childAtom = getRuleConclusionAtom(); if (parentAtom.getSchemaConcept() != null) { return childAtom.getMultiUnifier(parentAtom, UnifierType.RULE); } //case of match all atom (atom without type) else { Atom extendedParent = parentAtom.addType(childAtom.getSchemaConcept()).inferTypes(); return childAtom.getMultiUnifier(extendedParent, UnifierType.RULE); } } /** * @param parentAtom atom to which this rule is applied * @param ruleUnifier unifier with parent state * @param parent parent state * @param visitedSubGoals set of visited sub goals * @return resolution subGoal formed from this rule */ public ResolutionState subGoal(Atom parentAtom, Unifier ruleUnifier, QueryStateBase parent, Set<ReasonerAtomicQuery> visitedSubGoals) { Unifier ruleUnifierInverse = ruleUnifier.inverse(); //delta' = theta . thetaP . delta ConceptMap partialSubPrime = ruleUnifierInverse.apply(parentAtom.getParentQuery().getSubstitution()); return new RuleState(this.propagateConstraints(parentAtom, ruleUnifierInverse), partialSubPrime, ruleUnifier, parent, visitedSubGoals); } }