Skip to content
This repository has been archived by the owner on Oct 9, 2023. It is now read-only.

Commit

Permalink
Implement explanations (#213)
Browse files Browse the repository at this point in the history
## What is the goal of this PR?
Following typedb/typedb#6271 and the corresponding protocol change in typedb/typedb-protocol#131 we implement Explanations, Explainable concept maps, and the explain() query API, which allows users to stream Explanations on demand **note: explain query or transaction option must be set to `true`**

## What are the changes implemented in this PR?
* Implement `Explanation` objects, and extend `ConceptMap` to contain `Explainables`
* Add the `QueryManager.explain(Explainable)` API to retrieve all direct explanations (1-rule layer)
  • Loading branch information
Alex Walker authored Mar 31, 2021
1 parent f3111a3 commit eae5454
Show file tree
Hide file tree
Showing 18 changed files with 403 additions and 95 deletions.
4 changes: 2 additions & 2 deletions dependencies/graknlabs/artifacts.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def graknlabs_grakn_core_artifacts():
artifact_name = "grakn-core-server-{platform}-{version}.{ext}",
tag_source = deployment["artifact.release"],
commit_source = deployment["artifact.snapshot"],
commit = "136d9e134a59ab4207d9d45de241997918e0f798",
commit = "a36868f1e8a34188eb371dee329ed499399cb40f",
)

def graknlabs_grakn_cluster_artifacts():
Expand All @@ -37,5 +37,5 @@ def graknlabs_grakn_cluster_artifacts():
artifact_name = "grakn-cluster-all-{platform}-{version}.{ext}",
tag_source = deployment_private["artifact.release"],
commit_source = deployment_private["artifact.snapshot"],
commit = "c361e8e4b3f1e14aa8fde4c284ebf2cb2113b99f"
commit = "f08e4d9e194ee7e1806995377c8b0a7bd56903ec"
)
2 changes: 1 addition & 1 deletion dependencies/graknlabs/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def graknlabs_common():
git_repository(
name = "graknlabs_common",
remote = "https://github.com/graknlabs/common",
tag = "2.0.0-alpha-9" # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_common
tag = "2.0.0" # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_common
)

def graknlabs_behaviour():
Expand Down
42 changes: 41 additions & 1 deletion grakn/api/answer/concept_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# under the License.
#
from abc import ABC, abstractmethod
from typing import Mapping, Iterable
from typing import Mapping, Iterable, Tuple

from grakn.api.concept.concept import Concept

Expand All @@ -35,3 +35,43 @@ def concepts(self) -> Iterable[Concept]:
@abstractmethod
def get(self, variable: str) -> Concept:
pass

@abstractmethod
def explainables(self) -> "ConceptMap.Explainables":
pass

class Explainables(ABC):

@abstractmethod
def relation(self, variable: str) -> "ConceptMap.Explainable":
pass

@abstractmethod
def attribute(self, variable: str) -> "ConceptMap.Explainable":
pass

@abstractmethod
def ownership(self, owner: str, attribute: str) -> "ConceptMap.Explainable":
pass

@abstractmethod
def relations(self) -> Mapping[str, "ConceptMap.Explainable"]:
pass

@abstractmethod
def attributes(self) -> Mapping[str, "ConceptMap.Explainable"]:
pass

@abstractmethod
def ownerships(self) -> Mapping[Tuple[str, str], "ConceptMap.Explainable"]:
pass

class Explainable(ABC):

@abstractmethod
def conjunction(self) -> str:
pass

@abstractmethod
def explainable_id(self) -> int:
pass
42 changes: 42 additions & 0 deletions grakn/api/logic/explanation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
from abc import ABC, abstractmethod
from typing import Mapping, Set

from grakn.api.answer.concept_map import ConceptMap
from grakn.api.logic.rule import Rule


class Explanation(ABC):

@abstractmethod
def rule(self) -> Rule:
pass

@abstractmethod
def conclusion(self) -> ConceptMap:
pass

@abstractmethod
def condition(self) -> ConceptMap:
pass

@abstractmethod
def variable_mapping(self) -> Mapping[str, Set[str]]:
pass
5 changes: 5 additions & 0 deletions grakn/api/query/query_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from grakn.api.answer.concept_map_group import ConceptMapGroup
from grakn.api.answer.numeric import Numeric
from grakn.api.answer.numeric_group import NumericGroup
from grakn.api.logic.explanation import Explanation
from grakn.api.options import GraknOptions
from grakn.api.query.future import QueryFuture

Expand Down Expand Up @@ -57,6 +58,10 @@ def delete(self, query: str, options: GraknOptions = None) -> QueryFuture:
def update(self, query: str, options: GraknOptions = None) -> Iterator[ConceptMap]:
pass

@abstractmethod
def explain(self, explainable: ConceptMap.Explainable, options: GraknOptions = None) -> Iterator[Explanation]:
pass

@abstractmethod
def define(self, query: str, options: GraknOptions = None) -> QueryFuture:
pass
Expand Down
4 changes: 3 additions & 1 deletion grakn/common/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ def __init__(self, code: int, message: str):
BAD_ENCODING = ConceptErrorMessage(5, "The encoding '%s' was not recognised.")
BAD_VALUE_TYPE = ConceptErrorMessage(6, "The value type '%s' was not recognised.")
BAD_ATTRIBUTE_VALUE = ConceptErrorMessage(7, "The attribute value '%s' was not recognised.")
GET_HAS_WITH_MULTIPLE_FILTERS = ConceptErrorMessage(8, "Only one filter can be applied at a time to get_has. The possible filters are: [attribute_type, attribute_types, only_key]")
NONEXISTENT_EXPLAINABLE_CONCEPT = ConceptErrorMessage(8, "The concept identified by '%s' is not explainable.")
NONEXISTENT_EXPLAINABLE_OWNERSHIP = ConceptErrorMessage(9, "The ownership by owner '%s' of attribute '%s' is not explainable.")
GET_HAS_WITH_MULTIPLE_FILTERS = ConceptErrorMessage(10, "Only one filter can be applied at a time to get_has. The possible filters are: [attribute_type, attribute_types, only_key]")


class QueryErrorMessage(ErrorMessage):
Expand Down
8 changes: 8 additions & 0 deletions grakn/common/rpc/request_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ def query_manager_update_req(query: str, options: options_proto.Options):
return query_manager_req(query_mgr_req, options)


def query_manager_explain_req(explainable_id: int, options: options_proto.Options):
query_mgr_req = query_proto.QueryManager.Req()
explain_req = query_proto.QueryManager.Explain.Req()
explain_req.explainable_id = explainable_id
query_mgr_req.explain_req.CopyFrom(explain_req)
return query_manager_req(query_mgr_req, options)


# ConceptManager

def concept_manager_req(concept_mgr_req: concept_proto.ConceptManager.Req):
Expand Down
103 changes: 96 additions & 7 deletions grakn/concept/answer/concept_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,29 @@
# under the License.
#

from typing import Mapping
from typing import Mapping, Dict, Tuple

import grakn_protocol.common.answer_pb2 as answer_proto

from grakn.api.answer.concept_map import ConceptMap
from grakn.api.concept.concept import Concept
from grakn.common.exception import GraknClientException, VARIABLE_DOES_NOT_EXIST
from grakn.common.exception import GraknClientException, VARIABLE_DOES_NOT_EXIST, NONEXISTENT_EXPLAINABLE_CONCEPT, \
NONEXISTENT_EXPLAINABLE_OWNERSHIP
from grakn.concept.proto import concept_proto_reader


class _ConceptMap(ConceptMap):

def __init__(self, mapping: Mapping[str, Concept]):
def __init__(self, mapping: Mapping[str, Concept], explainables: ConceptMap.Explainables = None):
self._map = mapping
self._explainables = explainables

@staticmethod
def of(concept_map_proto: answer_proto.ConceptMap) -> "_ConceptMap":
def of(res: answer_proto.ConceptMap) -> "_ConceptMap":
variable_map = {}
for res_var in concept_map_proto.map:
variable_map[res_var] = concept_proto_reader.concept(concept_map_proto.map[res_var])
return _ConceptMap(variable_map)
for res_var in res.map:
variable_map[res_var] = concept_proto_reader.concept(res.map[res_var])
return _ConceptMap(variable_map, _ConceptMap.Explainables.of(res.explainables))

def map(self):
return self._map
Expand All @@ -51,6 +53,9 @@ def get(self, variable: str):
raise GraknClientException.of(VARIABLE_DOES_NOT_EXIST, variable)
return concept

def explainables(self) -> ConceptMap.Explainables:
return self._explainables

def __str__(self):
return "".join(map(lambda var: "[" + var + "/" + str(self._map[var]) + "]", sorted(self._map.keys())))

Expand All @@ -63,3 +68,87 @@ def __eq__(self, other):

def __hash__(self):
return hash(self._map)

class Explainables(ConceptMap.Explainables):

def __init__(self, relations: Mapping[str, ConceptMap.Explainable] = None, attributes: Mapping[str, ConceptMap.Explainable] = None, ownerships: Mapping[Tuple[str, str], ConceptMap.Explainable] = None):
self._relations = relations
self._attributes = attributes
self._ownerships = ownerships

@staticmethod
def of(explainables: answer_proto.Explainables):
relations: Dict[str, ConceptMap.Explainable] = {}
for [var, explainable] in explainables.relations.items():
relations[var] = _ConceptMap.Explainable.of(explainable)
attributes: Dict[str, ConceptMap.Explainable] = {}
for [var, explainable] in explainables.attributes.items():
attributes[var] = _ConceptMap.Explainable.of(explainable)
ownerships: Dict[Tuple[str, str], ConceptMap.Explainable] = {}
for [var, owned_map] in explainables.ownerships.items():
for [owned, explainable] in owned_map.owned.items():
ownerships[(var, owned)] = _ConceptMap.Explainable.of(explainable)
return _ConceptMap.Explainables(relations, attributes, ownerships)

def relation(self, variable: str) -> "ConceptMap.Explainable":
explainable = self._relations.get(variable)
if not explainable:
raise GraknClientException.of(NONEXISTENT_EXPLAINABLE_CONCEPT, variable)
return explainable

def attribute(self, variable: str) -> "ConceptMap.Explainable":
explainable = self._attributes.get(variable)
if not explainable:
raise GraknClientException.of(NONEXISTENT_EXPLAINABLE_CONCEPT, variable)
return explainable

def ownership(self, owner: str, attribute: str) -> "ConceptMap.Explainable":
explainable = self._ownerships.get((owner, attribute))
if not explainable:
raise GraknClientException.of(NONEXISTENT_EXPLAINABLE_OWNERSHIP, (owner, attribute))
return explainable

def relations(self) -> Mapping[str, "ConceptMap.Explainable"]:
return self._relations

def attributes(self) -> Mapping[str, "ConceptMap.Explainable"]:
return self._attributes

def ownerships(self) -> Mapping[Tuple[str, str], "ConceptMap.Explainable"]:
return self._ownerships

def __eq__(self, other):
if other is self:
return True
if not other or type(other) != type(self):
return False
return self._relations == other._relations and self._attributes == other._attributes and self._ownerships == other._ownerships

def __hash__(self):
return hash((self._relations, self._attributes, self._ownerships))

class Explainable(ConceptMap.Explainable):

def __init__(self, conjunction: str, explainable_id: int):
self._conjunction = conjunction
self._explainable_id = explainable_id

@staticmethod
def of(explainable: answer_proto.Explainable):
return _ConceptMap.Explainable(explainable.conjunction, explainable.id)

def conjunction(self) -> str:
return self._conjunction

def explainable_id(self) -> int:
return self._explainable_id

def __eq__(self, other):
if other is self:
return True
if not other or type(other) != type(self):
return False
return self._explainable_id

def __hash__(self):
return hash(self._explainable_id)
2 changes: 1 addition & 1 deletion grakn/concept/thing/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def get_iid(self):
return self._iid

def __str__(self):
return type(self).__name__ + "[iid:" + self.get_iid() + "]"
return "%s[%s:%s]" % (type(self).__name__, self.get_type().get_label(), self.get_iid())

def __eq__(self, other):
if other is self:
Expand Down
74 changes: 74 additions & 0 deletions grakn/logic/explanation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
from typing import Mapping, Set

import grakn_protocol.common.logic_pb2 as logic_proto

from grakn.api.answer.concept_map import ConceptMap
from grakn.api.logic.explanation import Explanation
from grakn.api.logic.rule import Rule
from grakn.concept.answer.concept_map import _ConceptMap
from grakn.logic.rule import _Rule


def _var_mapping_of(var_mapping: Mapping[str, logic_proto.Explanation.VarList]):
mapping = {}
for from_ in var_mapping:
tos = var_mapping[from_]
mapping[from_] = set(tos.vars)
return mapping


class _Explanation(Explanation):

def __init__(self, rule: Rule, variable_mapping: Mapping[str, Set[str]], conclusion: ConceptMap, condition: ConceptMap):
self._rule = rule
self._variable_mapping = variable_mapping
self._conclusion = conclusion
self._condition = condition

@staticmethod
def of(explanation: logic_proto.Explanation):
return _Explanation(_Rule.of(explanation.rule), _var_mapping_of(explanation.var_mapping),
_ConceptMap.of(explanation.conclusion), _ConceptMap.of(explanation.condition))

def rule(self) -> Rule:
return self._rule

def variable_mapping(self) -> Mapping[str, Set[str]]:
return self._variable_mapping

def conclusion(self) -> ConceptMap:
return self._conclusion

def condition(self) -> ConceptMap:
return self._condition

def __str__(self):
return "Explanation[rule: %s, variable_mapping: %s, then_answer: %s, when_answer: %s]" % (self._rule, self._variable_mapping, self._conclusion, self._condition)

def __eq__(self, other):
if other is self:
return True
if not other or type(self) != type(other):
return False
return self._rule == other._rule and self._variable_mapping == other._variable_mapping and self._conclusion == other._conclusion and self._condition == other._condition

def __hash__(self):
return hash((self._rule, self._variable_mapping, self._conclusion, self._condition))
Loading

0 comments on commit eae5454

Please sign in to comment.