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

Implement explanations #213

Merged
merged 10 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 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 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 then_answer(self) -> ConceptMap:
pass

@abstractmethod
def when_answer(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
104 changes: 97 additions & 7 deletions grakn/concept/answer/concept_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,44 @@
# 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


def _explainables_of(explainables: answer_proto.Explainables):
relations: Dict[str, ConceptMap.Explainable] = {}
for var in explainables.explainable_relations:
explainable = explainables.explainable_relations[var]
relations[var] = _ConceptMap.Explainable.of(explainable)
attributes: Dict[str, ConceptMap.Explainable] = {}
for var in explainables.explainable_attributes:
explainable = explainables.explainable_attributes[var]
attributes[var] = _ConceptMap.Explainable.of(explainable)
ownerships: Dict[Tuple[str, str], ConceptMap.Explainable] = {}
for ownership in explainables.explainable_ownerships:
ownerships[(ownership.owner, ownership.attribute)] = _ConceptMap.Explainable.of(ownership.explainable)
return _ConceptMap.Explainables(relations, attributes, ownerships)


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, _explainables_of(res.explainables))

def map(self):
return self._map
Expand All @@ -51,6 +68,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 +83,73 @@ 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

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":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this typing cracks me up

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)
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.VarsList]):
mapping = {}
for from_ in var_mapping:
tos = var_mapping[from_]
mapping[from_] = set(tos.vars_list)
return mapping


class _Explanation(Explanation):

def __init__(self, rule: Rule, variable_mapping: Mapping[str, Set[str]], then_answer: ConceptMap, when_answer: ConceptMap):
self._rule = rule
self._variable_mapping = variable_mapping
self._then_answer = then_answer
self._when_answer = when_answer

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

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

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

def then_answer(self) -> ConceptMap:
return self._then_answer

def when_answer(self) -> ConceptMap:
return self._when_answer

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

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._then_answer == other._then_answer and self._when_answer == other._when_answer

def __hash__(self):
return hash((self._rule, self._variable_mapping, self._then_answer, self._when_answer))
Loading