grakn.core.graql.reasoner.atom.Atom.java Source code

Java tutorial

Introduction

Here is the source code for grakn.core.graql.reasoner.atom.Atom.java

Source

/*
 * 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.atom;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import grakn.core.common.exception.ErrorMessage;
import grakn.core.concept.ConceptId;
import grakn.core.concept.answer.ConceptMap;
import grakn.core.concept.type.Rule;
import grakn.core.concept.type.SchemaConcept;
import grakn.core.concept.type.Type;
import grakn.core.graql.exception.GraqlQueryException;
import grakn.core.graql.reasoner.atom.binary.AttributeAtom;
import grakn.core.graql.reasoner.atom.binary.IsaAtom;
import grakn.core.graql.reasoner.atom.binary.OntologicalAtom;
import grakn.core.graql.reasoner.atom.binary.RelationAtom;
import grakn.core.graql.reasoner.atom.binary.TypeAtom;
import grakn.core.graql.reasoner.atom.predicate.IdPredicate;
import grakn.core.graql.reasoner.atom.predicate.NeqPredicate;
import grakn.core.graql.reasoner.atom.predicate.Predicate;
import grakn.core.graql.reasoner.atom.predicate.ValuePredicate;
import grakn.core.graql.reasoner.cache.SemanticDifference;
import grakn.core.graql.reasoner.cache.VariableDefinition;
import grakn.core.graql.reasoner.rule.InferenceRule;
import grakn.core.graql.reasoner.unifier.MultiUnifier;
import grakn.core.graql.reasoner.unifier.MultiUnifierImpl;
import grakn.core.graql.reasoner.unifier.Unifier;
import grakn.core.graql.reasoner.unifier.UnifierType;
import graql.lang.property.IsaProperty;
import graql.lang.property.VarProperty;
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 javax.annotation.Nullable;

import static java.util.stream.Collectors.toSet;

/**
 * {@link AtomicBase} extension defining specialised functionalities.
 */
public abstract class Atom extends AtomicBase {

    private Set<InferenceRule> applicableRules = null;

    public RelationAtom toRelationAtom() {
        throw GraqlQueryException.illegalAtomConversion(this, RelationAtom.class);
    }

    public IsaAtom toIsaAtom() {
        throw GraqlQueryException.illegalAtomConversion(this, IsaAtom.class);
    }

    /**
     * Determines whether the subsumption relation between this (A) and provided atom (B) holds,
     * i. e. determines if:
     *
     * A >= B,
     *
     * is true meaning that B is more general than A and the respective answer sets meet:
     *
     * answers(B) subsetOf answers(A)
     *
     * i. e. the set of answers of A is a subset of the set of answers of B
     *
     * @param atomic to compare with
     * @return true if this atom subsumes the provided atom
     */
    @Override
    public boolean subsumes(Atomic atomic) {
        if (!atomic.isAtom())
            return false;
        Atom parent = (Atom) atomic;
        MultiUnifier multiUnifier = this.getMultiUnifier(parent, UnifierType.SUBSUMPTIVE);
        if (multiUnifier.isEmpty())
            return false;

        MultiUnifier inverse = multiUnifier.inverse();
        return //check whether propagated answers would be complete
        !inverse.isEmpty() && inverse.stream().allMatch(u -> u.values().containsAll(this.getVarNames()))
                && !parent.getPredicates(NeqPredicate.class).findFirst().isPresent()
                && !this.getPredicates(NeqPredicate.class).findFirst().isPresent();
    }

    /**
     * @param atom to unify with
     * @return true if this unifies with atom via rule unifier
     */
    public boolean isUnifiableWith(Atom atom) {
        return !this.getMultiUnifier(atom, UnifierType.RULE).equals(MultiUnifierImpl.nonExistent());
    }

    @Override
    public boolean isAtom() {
        return true;
    }

    public boolean isRuleResolvable() {
        return getApplicableRules().findFirst().isPresent();
    }

    /**
     * @return true if the atom is ground (all variables are bound)
     */
    public boolean isGround() {
        Set<Variable> mappedVars = Stream.concat(getPredicates(), getInnerPredicates()).map(AtomicBase::getVarName)
                .collect(toSet());
        return mappedVars.containsAll(getVarNames());
    }

    /**
     * @return true if this atom is bounded - via substitution/specific resource or schema
     */
    public boolean isBounded() {
        return isResource() && ((AttributeAtom) this).isValueEquality() || this instanceof OntologicalAtom
                || isGround();
    }

    /**
     * @return true if this atom is disconnected (doesn't have neighbours)
     */
    public boolean isDisconnected() {
        return isSelectable()
                && getParentQuery().getAtoms(Atom.class).filter(Atomic::isSelectable).filter(at -> !at.equals(this))
                        .allMatch(at -> Sets.intersection(at.getVarNames(), this.getVarNames()).isEmpty());
    }

    /**
     * @return true if this atom requires direct schema lookups
     */
    public boolean requiresSchema() {
        return getSchemaConcept() == null || this instanceof OntologicalAtom;
    }

    public abstract Class<? extends VarProperty> getVarPropertyClass();

    @Override
    public Set<String> validateAsRuleHead(Rule rule) {
        Set<String> errors = new HashSet<>();
        Set<Atomic> parentAtoms = getParentQuery().getAtoms(Atomic.class).filter(at -> !at.equals(this))
                .collect(toSet());
        Set<Variable> varNames = Sets.difference(getVarNames(),
                this.getInnerPredicates().map(Atomic::getVarName).collect(toSet()));
        boolean unboundVariables = varNames.stream()
                .anyMatch(var -> parentAtoms.stream().noneMatch(at -> at.getVarNames().contains(var)));
        if (unboundVariables) {
            errors.add(ErrorMessage.VALIDATION_RULE_ILLEGAL_HEAD_ATOM_WITH_UNBOUND_VARIABLE.getMessage(rule.then(),
                    rule.label()));
        }

        SchemaConcept schemaConcept = getSchemaConcept();
        if (schemaConcept == null) {
            errors.add(ErrorMessage.VALIDATION_RULE_ILLEGAL_HEAD_ATOM_WITH_AMBIGUOUS_SCHEMA_CONCEPT
                    .getMessage(rule.then(), rule.label()));
        } else if (schemaConcept.isImplicit()) {
            errors.add(ErrorMessage.VALIDATION_RULE_ILLEGAL_HEAD_ATOM_WITH_IMPLICIT_SCHEMA_CONCEPT
                    .getMessage(rule.then(), rule.label()));
        }
        return errors;
    }

    /**
     * @return var properties this atom (its pattern) contains
     */
    public Stream<VarProperty> getVarProperties() {
        return getCombinedPattern().statements().stream()
                .flatMap(statement -> statement.getProperties(getVarPropertyClass()));
    }

    @Override
    public boolean isDirect() {
        return getPattern().properties().stream().anyMatch(VarProperty::isExplicit);
    }

    /**
     * @return set of variables that need to be have their roles expanded
     */
    public Set<Variable> getRoleExpansionVariables() {
        return new HashSet<>();
    }

    private boolean isRuleApplicable(InferenceRule child) {
        return (getIdPredicate(getVarName()) == null || child.redefinesType() || child.appendsRolePlayers())
                && isRuleApplicableViaAtom(child.getRuleConclusionAtom());
    }

    protected abstract boolean isRuleApplicableViaAtom(Atom headAtom);

    /**
     * @return set of potentially applicable rules - does shallow (fast) check for applicability
     */
    public Stream<Rule> getPotentialRules() {
        boolean isDirect = getPattern().getProperties(IsaProperty.class).findFirst().map(IsaProperty::isExplicit)
                .orElse(false);

        return getPossibleTypes().stream().flatMap(type -> tx().ruleCache().getRulesWithType(type, isDirect))
                .distinct();
    }

    /**
     * @return set of applicable rules - does detailed (slow) check for applicability
     */
    public Stream<InferenceRule> getApplicableRules() {
        if (applicableRules == null) {
            applicableRules = new HashSet<>();
            getPotentialRules().map(rule -> tx().ruleCache().getRule(rule, () -> new InferenceRule(rule, tx())))
                    .filter(this::isRuleApplicable).map(r -> r.rewrite(this)).forEach(applicableRules::add);
        }
        return applicableRules.stream();
    }

    /**
     * @return true if the atom requires materialisation in order to be referenced
     */
    public boolean requiresMaterialisation() {
        return false;
    }

    /**
     * @return true if the atom requires role expansion
     */
    public boolean requiresRoleExpansion() {
        return false;
    }

    /**
     * @return if this atom requires decomposition into a set of atoms
     */
    public boolean requiresDecomposition() {
        return this.getPotentialRules().map(r -> tx().ruleCache().getRule(r, () -> new InferenceRule(r, tx())))
                .anyMatch(InferenceRule::appendsRolePlayers);
    }

    /**
     * @return corresponding type if any
     */
    public abstract SchemaConcept getSchemaConcept();

    /**
     * @return type id of the corresponding type if any
     */
    public abstract ConceptId getTypeId();

    /**
     * @return value variable name
     */
    public abstract Variable getPredicateVariable();

    public abstract Stream<Predicate> getInnerPredicates();

    /**
     * @param type the class of {@link Predicate} to return
     * @param <T>  the type of {@link Predicate} to return
     * @return stream of predicates relevant to this atom
     */
    public <T extends Predicate> Stream<T> getInnerPredicates(Class<T> type) {
        return getInnerPredicates().filter(type::isInstance).map(type::cast);
    }

    /**
     *
     * @param var variable of interest
     * @param type the class of {@link Predicate} to return
     * @param <T>  the type of {@link Predicate} to return
     * @return stream of all predicates (public and inner) relevant to this atom and variable
     */
    public <T extends Predicate> Stream<T> getAllPredicates(Variable var, Class<T> type) {
        return Stream.concat(getPredicates(type), getInnerPredicates(type)).filter(p -> p.getVarName().equals(var));
    }

    /**
     * @return set of types relevant to this atom
     */
    public Stream<TypeAtom> getTypeConstraints() {
        return getParentQuery().getAtoms(TypeAtom.class).filter(atom -> atom != this)
                .filter(atom -> containsVar(atom.getVarName()));
    }

    /**
     * @param type the class of {@link Predicate} to return
     * @param <T>  the type of neighbour {@link Atomic} to return
     * @return neighbours of this atoms, i.e. atoms connected to this atom via shared variable
     */
    protected <T extends Atomic> Stream<T> getNeighbours(Class<T> type) {
        return getParentQuery().getAtoms(type).filter(atom -> atom != this)
                .filter(atom -> !Sets.intersection(this.getVarNames(), atom.getVarNames()).isEmpty());
    }

    /**
     * @return set of constraints of this atom (predicates + types) that are not selectable
     */
    public Stream<Atomic> getNonSelectableConstraints() {
        return Stream.concat(Stream.concat(getPredicates(), getPredicates().flatMap(AtomicBase::getPredicates)),
                getTypeConstraints().filter(at -> !at.isSelectable()));
    }

    @Override
    public Atom inferTypes() {
        return inferTypes(new ConceptMap());
    }

    @Override
    public Atom inferTypes(ConceptMap sub) {
        return this;
    }

    /**
     * @return list of types this atom can take
     */
    public ImmutableList<Type> getPossibleTypes() {
        return (getSchemaConcept() != null && getSchemaConcept().isType())
                ? ImmutableList.of(getSchemaConcept().asType())
                : ImmutableList.of();
    }

    /**
     * @param sub partial substitution
     * @return list of possible atoms obtained by applying type inference
     */
    public List<Atom> atomOptions(ConceptMap sub) {
        return Lists.newArrayList(inferTypes(sub));
    }

    /**
     * @param type to be added to this {@link Atom}
     * @return new {@link Atom} with specified type
     */
    public Atom addType(SchemaConcept type) {
        return this;
    }

    public Stream<ConceptMap> materialise() {
        return Stream.empty();
    }

    /**
     * @return set of atoms this atom can be decomposed to
     */
    public Set<Atom> rewriteToAtoms() {
        return Sets.newHashSet(this);
    }

    /**
     * rewrites the atom to user-defined type variable
     *
     * @param parentAtom parent atom that triggers rewrite
     * @return rewritten atom
     */
    public Atom rewriteWithTypeVariable(Atom parentAtom) {
        if (parentAtom.getPredicateVariable().isReturned() && !this.getPredicateVariable().isReturned()
                && this.getClass() == parentAtom.getClass()) {
            return rewriteWithTypeVariable();
        }
        return this;
    }

    /**
     * rewrites the atom to one with suitably user-defined names depending on provided parent
     *
     * @param parentAtom parent atom that triggers rewrite
     * @return rewritten atom
     */
    public abstract Atom rewriteToUserDefined(Atom parentAtom);

    public abstract Atom rewriteWithTypeVariable();

    /**
     * rewrites the atom to one with user defined relation variable
     *
     * @return rewritten atom
     */
    public Atom rewriteWithRelationVariable() {
        return this;
    }

    /**
     * attempt to find a UNIQUE unifier with the parent atom
     *
     * @param parentAtom  atom to be unified with
     * @param unifierType type of unifier to be computed
     * @return corresponding unifier
     */
    @Nullable
    public abstract Unifier getUnifier(Atom parentAtom, UnifierType unifierType);

    /**
     *
     * @param parentAtom atom wrt which we check the compatibility
     * @param unifier mappings between this (child) and parent variables
     * @param unifierType unifier type in question
     * @return true if predicates between this (child) and parent are compatible based on the mappings provided by unifier
     */
    protected boolean isPredicateCompatible(Atom parentAtom, Unifier unifier, UnifierType unifierType) {
        //check value predicates compatibility
        return unifier.mappings().stream().allMatch(mapping -> {
            Variable childVar = mapping.getKey();
            Variable parentVar = mapping.getValue();
            Set<Atomic> parentIdPredicates = parentAtom.getPredicates(parentVar, IdPredicate.class)
                    .collect(Collectors.toSet());
            Set<Atomic> childIdPredicates = this.getPredicates(childVar, IdPredicate.class)
                    .collect(Collectors.toSet());
            Set<Atomic> parentValuePredicates = parentAtom.getAllPredicates(parentVar, ValuePredicate.class)
                    .collect(Collectors.toSet());
            Set<Atomic> childValuePredicates = this.getAllPredicates(childVar, ValuePredicate.class)
                    .collect(Collectors.toSet());

            if (unifierType.inferValues()) {
                parentAtom.getParentQuery().getAtoms(IdPredicate.class)
                        .filter(id -> id.getVarName().equals(parentVar)).map(IdPredicate::toValuePredicate)
                        .filter(Objects::nonNull).forEach(parentValuePredicates::add);
                this.getParentQuery().getAtoms(IdPredicate.class).filter(id -> id.getVarName().equals(childVar))
                        .map(IdPredicate::toValuePredicate).filter(Objects::nonNull)
                        .forEach(childValuePredicates::add);
            }

            return unifierType.idCompatibility(parentIdPredicates, childIdPredicates)
                    && unifierType.valueCompatibility(parentValuePredicates, childValuePredicates);
        });
    }

    /**
     * find the (multi) unifier with parent atom
     *
     * @param parentAtom  atom to be unified with
     * @param unifierType type of unifier to be computed
     * @return multiunifier
     */
    public MultiUnifier getMultiUnifier(Atom parentAtom, UnifierType unifierType) {
        //NB only for relations we can have non-unique unifiers
        Unifier unifier = this.getUnifier(parentAtom, unifierType);
        return unifier != null ? new MultiUnifierImpl(unifier) : MultiUnifierImpl.nonExistent();
    }

    /**
     * Calculates the semantic difference between the parent and this (child) atom,
     * that needs to be applied on A(P) to find the subset belonging to A(C).
     *
     * @param parentAtom parent atom
     * @param unifier    child->parent unifier
     * @return semantic difference between child and parent
     */
    public SemanticDifference semanticDifference(Atom parentAtom, Unifier unifier) {
        Set<VariableDefinition> diff = new HashSet<>();
        Unifier unifierInverse = unifier.inverse();

        unifier.mappings().forEach(m -> {
            Variable childVar = m.getKey();
            Variable parentVar = m.getValue();

            Type childType = this.getParentQuery().getUnambiguousType(childVar, false);
            Type parentType = parentAtom.getParentQuery().getUnambiguousType(parentVar, false);
            Type type = childType != null
                    ? parentType != null ? (!parentType.equals(childType) ? childType : null) : childType
                    : null;

            Set<ValuePredicate> predicates = this.getPredicates(childVar, ValuePredicate.class).collect(toSet());
            parentAtom.getPredicates(parentVar, ValuePredicate.class)
                    .flatMap(vp -> vp.unify(unifierInverse).stream()).forEach(predicates::remove);

            diff.add(new VariableDefinition(childVar, type, null, new HashSet<>(), predicates));
        });
        return new SemanticDifference(diff);
    }
}