Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ date) and use that section as the basis for the GitHub release notes.
## [Unreleased]

### Fixed
- `TripleStoreReasoner.instances(ce, direct=False)` only expanded subclasses for the root
variable's type triple, so class expressions with fillers (e.g.
`Grandfather ⊓ ∃hasChild.Grandparent`) missed individuals typed only by a subclass of the
filler. Every type triple is now expanded, each with its own variable, and `instances_count` and
`is_subset_of_individuals` share the same helper (`with_subclass_closure`), which also removes
the shared-variable scoping bug of #575 from those two. (#571)
- `ontolearn.semantic_caching.retrieve` called `NeuralOntology(path_of_kb=...)`,
an argument removed from owlapy, so the neural-reasoner path raised
`TypeError` whenever no KGE model path was given. It now trains/loads via
Expand Down
39 changes: 22 additions & 17 deletions ontolearn/triple_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ def suf(direct: bool):
return " " if direct else "* "


# A type triple `?var a <Class>` as emitted by the Owl2SparqlConverter, for the root variable as well as
# for filler variables (`?s_1 a <Class>`). owl:NamedIndividual has no subclasses, so it is left alone.
_TYPE_TRIPLE = re.compile(r"(\?\w+) a (?=<(?!http://www\.w3\.org/2002/07/owl#NamedIndividual>))")


def with_subclass_closure(sparql: str) -> str:
"""Make every type triple `?v a <C>` of a converted class expression match instances of subclasses of
`<C>` too, i.e. `?v a ?some_cls_i. ?some_cls_i rdfs:subClassOf* <C>`, so that indirect instances are found
without materialized type assertions. Each occurrence gets its own variable so that uses inside/outside
constructs like FILTER NOT EXISTS don't accidentally share a binding."""
counter = count()

def _expand(match: re.Match) -> str:
var = f"?some_cls_{next(counter)}"
return f"{match.group(1)} a {var}. \n {var} <http://www.w3.org/2000/01/rdf-schema#subClassOf>* "

return _TYPE_TRIPLE.sub(_expand, sparql)


class TripleStoreOntology(AbstractOWLOntology):

def __init__(self, triplestore_address: str, use_cache: bool = False):
Expand Down Expand Up @@ -484,15 +503,7 @@ def instances(self, ce: OWLClassExpression, direct: bool = False, seen_set: Set
seen_set.add(ce)
ce_to_sparql = self._owl2sparql_converter.as_query("?x", ce)
if not direct:
counter = count()

def _replace_with_subclass_filter(_match: re.Match) -> str:
# Each occurrence gets its own variable so that uses inside/outside
# constructs like FILTER NOT EXISTS don't accidentally share a binding.
var = f"?some_cls_{next(counter)}"
return f"?x a {var}. \n {var} <http://www.w3.org/2000/01/rdf-schema#subClassOf>* "

ce_to_sparql = re.sub(r"\?x a ", _replace_with_subclass_filter, ce_to_sparql)
ce_to_sparql = with_subclass_closure(ce_to_sparql)
yield from send_http_request_to_ts_and_fetch_results(self.url, ce_to_sparql, OWLNamedIndividual)
if not direct:
for cls in self.equivalent_classes(ce):
Expand All @@ -506,10 +517,7 @@ def instances_count(self, ce: OWLClassExpression, direct: bool = False) -> int:
through an owl:equivalentClass axiom on `ce`."""
ce_to_sparql = self._owl2sparql_converter.as_query("?x", ce, count=True)
if not direct:
ce_to_sparql = ce_to_sparql.replace(
"?x a ",
"?x a ?some_cls. \n ?some_cls <http://www.w3.org/2000/01/rdf-schema#subClassOf>* ",
)
ce_to_sparql = with_subclass_closure(ce_to_sparql)
bindings = self.query(ce_to_sparql).json()["results"]["bindings"]
return int(bindings[0]["cnt"]["value"]) if bindings else 0

Expand All @@ -522,10 +530,7 @@ def is_subset_of_individuals(self, individuals: Iterable[OWLNamedIndividual], ce
return True
ce_pattern = "\n".join(self._owl2sparql_converter.convert("?x", ce))
if not direct:
ce_pattern = ce_pattern.replace(
"?x a ",
"?x a ?some_cls. \n ?some_cls <http://www.w3.org/2000/01/rdf-schema#subClassOf>* ",
)
ce_pattern = with_subclass_closure(ce_pattern)
values = " ".join(f"<{i.str}>" for i in individuals)
ask_query = f"ASK {{ VALUES ?x {{ {values} }} FILTER NOT EXISTS {{ {ce_pattern} }} }}"
return not self.query(ask_query).json()["boolean"]
Expand Down
62 changes: 60 additions & 2 deletions tests/test_triplestore_indirect_instances_sparql.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
import unittest
from unittest.mock import MagicMock, patch

from ontolearn.triple_store import TripleStoreOntology, TripleStoreReasoner
from owlapy.class_expression import OWLClass, OWLObjectComplementOf, OWLObjectIntersectionOf
from ontolearn.triple_store import TripleStoreOntology, TripleStoreReasoner, with_subclass_closure
from owlapy.class_expression import (OWLClass, OWLObjectComplementOf, OWLObjectIntersectionOf,
OWLObjectSomeValuesFrom)
from owlapy.iri import IRI
from owlapy.owl_individual import OWLNamedIndividual
from owlapy.owl_property import OWLObjectProperty

NS = "http://example.org/family#"
URL = "http://localhost:3030/fake/sparql"
Expand Down Expand Up @@ -81,3 +84,58 @@ def test_direct_true_does_not_rewrite_the_query(self):

if __name__ == "__main__":
unittest.main()


class TestSubclassClosureOfFillers(unittest.TestCase):
"""https://github.com/dice-group/Ontolearn/issues/571: the subclass closure must cover every type
triple of the converted query, not only the one on the root variable."""

def setUp(self):
self.grandfather = OWLClass(IRI.create(NS, "Grandfather"))
self.grandparent = OWLClass(IRI.create(NS, "Grandparent"))
self.has_child = OWLObjectProperty(IRI.create(NS, "hasChild"))
self.ce = OWLObjectIntersectionOf(
[self.grandfather, OWLObjectSomeValuesFrom(self.has_child, self.grandparent)])
self.reasoner = TripleStoreReasoner(TripleStoreOntology(URL))

def test_filler_type_triple_is_expanded(self):
query = with_subclass_closure(self.reasoner._owl2sparql_converter.as_query("?x", self.ce))
bindings = SUBCLASS_BINDING_RE.findall(query)
self.assertEqual({iri for _, iri in bindings}, {self.grandfather.str, self.grandparent.str})
self.assertEqual(len({var for var, _ in bindings}), 2)
self.assertIn("?s_1 a ?some_cls_", query)

def test_owl_named_individual_is_not_expanded(self):
sparql = "SELECT ?x WHERE { ?x a <http://www.w3.org/2002/07/owl#NamedIndividual> . ?x a <http://e.org/A> . }"
self.assertEqual(SUBCLASS_BINDING_RE.findall(with_subclass_closure(sparql)), [("?some_cls_0", "http://e.org/A")])

def test_count_and_subset_queries_scope_each_occurrence(self):
ce = OWLObjectIntersectionOf([OWLObjectComplementOf(OWLClass(IRI.create(NS, "Daughter"))),
OWLClass(IRI.create(NS, "Female"))])
resp = MagicMock()
resp.json.return_value = {"head": {"vars": ["cnt"]}, "boolean": False,
"results": {"bindings": [{"cnt": {"value": "0"}}]}}
with patch.object(TripleStoreReasoner, "query", return_value=resp) as mocked_query:
self.reasoner.instances_count(ce)
self.reasoner.is_subset_of_individuals([OWLNamedIndividual(IRI.create(NS, "a"))], ce)
self.assertEqual(mocked_query.call_count, 2)
for call in mocked_query.call_args_list:
variables = [var for var, _ in SUBCLASS_BINDING_RE.findall(call.args[0])]
self.assertEqual(len(variables), 2)
self.assertEqual(len(variables), len(set(variables)))

def test_indirect_instances_found_through_subclass_fillers(self):
"""Run the rewritten query on a tiny graph where the only type assertions are for subclasses."""
from rdflib import Graph
g = Graph()
g.parse(format="turtle", data=f"""
@prefix : <{NS}> . @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
:Grandfather rdfs:subClassOf :Father . :Grandparent rdfs:subClassOf :Parent .
:GrandfatherOfTwo rdfs:subClassOf :Grandfather . :GreatGrandparent rdfs:subClassOf :Grandparent .
:a a :GrandfatherOfTwo ; :hasChild :b . :b a :GreatGrandparent .
:c a :GrandfatherOfTwo ; :hasChild :d . :d a :Parent .
""")
base = self.reasoner._owl2sparql_converter.as_query("?x", self.ce)
names = lambda q: {str(r[0]).split("#")[1] for r in g.query(q)}
self.assertEqual(names(base), set()) # no asserted Grandfather/Grandparent types
self.assertEqual(names(with_subclass_closure(base)), {"a"}) # c's child is only a Parent, not a Grandparent
Loading