From 096394447e0b620865722eaab4f3dc991b271753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= <5106696+b-gehrke@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:19:46 +0200 Subject: [PATCH] Fixed hierarchy queries (#33) --- src/ontology.rs | 98 ++++++++++++++++++++++------------- test/resources/simple.ofn | 5 ++ test/resources/simple.ofn.raw | 5 ++ test/resources/simple.omn | 2 + test/resources/simple.omn.raw | 2 + test/resources/simple.owl | 8 +++ test/resources/simple.owl.raw | 8 +++ test/resources/simple.owx | 7 +++ test/resources/simple.owx.raw | 7 +++ test/test_base.py | 10 ++-- test/test_hierarchy.py | 89 +++++++++++++++++++++++++++++++ 11 files changed, 201 insertions(+), 40 deletions(-) create mode 100644 test/test_hierarchy.py diff --git a/src/ontology.rs b/src/ontology.rs index a9398c7..1ebeea5 100644 --- a/src/ontology.rs +++ b/src/ontology.rs @@ -7,9 +7,7 @@ use curie::Curie; use horned_owl::io::rdf::reader::ConcreteRDFOntology; use horned_owl::io::ResourceType; use horned_owl::model::{ - AnnotatedComponent, Annotation, AnnotationAssertion, AnnotationSubject, AnnotationValue, - ArcAnnotatedComponent, ArcStr, Build, Component, ComponentKind, HigherKinded, IRI, - Literal, MutableOntology, Ontology, OntologyID, + AnnotatedComponent, Annotation, AnnotationAssertion, AnnotationSubject, AnnotationValue, ArcAnnotatedComponent, ArcStr, Build, Class, ClassExpression, Component, ComponentKind, HigherKinded, Literal, MutableOntology, Ontology, OntologyID, SubClassOf, IRI }; use horned_owl::ontology::component_mapped::{ ArcComponentMappedOntology, ComponentMappedIndex, ComponentMappedOntology, @@ -325,8 +323,8 @@ impl PyIndexedOntology { /// get_subclasses(self, iri: str, iri_is_absolute: Optional[bool] = None) -> Set[str] /// /// Gets all subclasses of an entity. - #[pyo3[signature = (iri, iri_is_absolute = None)]] - pub fn get_subclasses( + #[pyo3[name="get_subclasses", signature = (iri, iri_is_absolute = None)]] + pub fn py_get_subclasses( &mut self, py: Python<'_>, iri: String, @@ -334,20 +332,16 @@ impl PyIndexedOntology { ) -> PyResult> { let iri: IRI = self.iri(py, iri, iri_is_absolute)?.into(); - let subclasses = self.classes_to_subclasses.get(&iri); - if let Some(subclss) = subclasses { - let subclasses: HashSet = subclss.iter().map(|sc| sc.to_string()).collect(); - Ok(subclasses) - } else { - Ok(HashSet::new()) - } + let classes: HashSet = self.get_subclasses(&iri).into_iter().map(|i| i.to_string()).collect(); + + Ok(classes) } /// get_superclasses(self, iri: str, iri_is_absolute: Optional[bool] = None) -> Set[str] /// /// Gets all superclasses of an entity. - #[pyo3[signature = (iri, iri_is_absolute = None)]] - pub fn get_superclasses( + #[pyo3[name="get_superclasses", signature = (iri, iri_is_absolute = None)]] + pub fn py_get_superclasses( &mut self, py: Python<'_>, iri: String, @@ -355,13 +349,9 @@ impl PyIndexedOntology { ) -> PyResult> { let iri: IRI = self.iri(py, iri, iri_is_absolute)?.into(); - let superclasses = self.classes_to_superclasses.get(&iri); - if let Some(superclss) = superclasses { - let superclasses: HashSet = superclss.iter().map(|sc| sc.to_string()).collect(); - Ok(superclasses) - } else { - Ok(HashSet::new()) - } + let classes: HashSet = self.get_superclasses(&iri).into_iter().map(|i| i.to_string()).collect(); + + Ok(classes) } /// get_classes(self) -> Set[str] @@ -882,10 +872,10 @@ impl PyIndexedOntology { self.recurse_descendants(&parent_iri, &mut descendants); - Ok(descendants) + Ok(descendants.into_iter().map(|c| c.to_string()).collect()) } - /// get_ancestors(self, onto: PyIndexedOntology, child: str, iri_is_absolute: Optional[bool] = None) -> Set[str] + /// get_ancestors(self, child: str, iri_is_absolute: Optional[bool] = None) -> Set[str] /// /// Gets all direct and indirect super classes of a class. #[pyo3[signature = (child_iri, *, iri_is_absolute = None)]] @@ -901,7 +891,7 @@ impl PyIndexedOntology { self.recurse_ancestors(&child_iri, &mut ancestors); - Ok(ancestors) + Ok(ancestors.into_iter().map(|c| c.to_string()).collect()) } /// build_iri_index(self) -> None @@ -963,21 +953,57 @@ impl PyIndexedOntology { } impl PyIndexedOntology { - fn recurse_descendants(&self, superclass: &IRI, descendants: &mut HashSet) { - descendants.insert(superclass.into()); - if self.classes_to_subclasses.contains_key(superclass) { - for cls2 in &mut self.classes_to_subclasses[superclass].iter() { - self.recurse_descendants(cls2, descendants); - } + pub fn get_subclasses(&self, iri: &IRI) -> HashSet> { + let subclass_axioms = if let Some(ref component_index) = &self.component_index { + Box::new(component_index.component_for_kind(ComponentKind::SubClassOf)) + as Box>> + } else { + Box::new((&self.set_index).into_iter()) + }; + + subclass_axioms + .filter_map(|aax| match &aax.component { + Component::SubClassOf(SubClassOf{ + sub: ClassExpression::Class(Class(sub)), + sup: ClassExpression::Class(Class(sup))}) if sup == iri => Some(sub.clone()), + _ => None, + }) + .collect() + } + + pub fn get_superclasses(&self, iri: &IRI) -> HashSet> { + let subclass_axioms = if let Some(ref component_index) = &self.component_index { + Box::new(component_index.component_for_kind(ComponentKind::SubClassOf)) + as Box>> + } else { + Box::new((&self.set_index).into_iter()) + }; + + subclass_axioms + .filter_map(|aax| match &aax.component { + Component::SubClassOf(SubClassOf{ + sub: ClassExpression::Class(Class(sub)), + sup: ClassExpression::Class(Class(sup))}) if sub == iri => Some(sup.clone()), + _ => None, + }) + .collect() + } + + fn recurse_descendants(&self, superclass: &IRI, descendants: &mut HashSet>) { + let subclasses = self.get_subclasses(superclass); + + for cls in subclasses.into_iter() { + self.recurse_descendants(&cls, descendants); + descendants.insert(cls); } } - fn recurse_ancestors(&self, subclass: &IRI, ancestors: &mut HashSet) { - ancestors.insert(subclass.into()); - if self.classes_to_superclasses.contains_key(subclass) { - for cls2 in &mut self.classes_to_superclasses[subclass].iter() { - self.recurse_ancestors(cls2, ancestors); - } + fn recurse_ancestors(&self, subclass: &IRI, ancestors: &mut HashSet>) { + let superclasses = self.get_superclasses(subclass); + + for cls in superclasses.into_iter() { + self.recurse_ancestors(&cls, ancestors); + ancestors.insert(cls); } } diff --git a/test/resources/simple.ofn b/test/resources/simple.ofn index 859d2ee..8b64f47 100644 --- a/test/resources/simple.ofn +++ b/test/resources/simple.ofn @@ -10,6 +10,7 @@ Ontology( Declaration(Class()) Declaration(Class()) Declaration(Class()) +Declaration(Class()) ############################ @@ -25,5 +26,9 @@ AnnotationAssertion(rdfs:label "ClassA") AnnotationAssertion(rdfs:label "ClassB") SubClassOf( ) +# Class: () + +SubClassOf( ) + ) \ No newline at end of file diff --git a/test/resources/simple.ofn.raw b/test/resources/simple.ofn.raw index 859d2ee..8b64f47 100644 --- a/test/resources/simple.ofn.raw +++ b/test/resources/simple.ofn.raw @@ -10,6 +10,7 @@ Ontology( Declaration(Class()) Declaration(Class()) Declaration(Class()) +Declaration(Class()) ############################ @@ -25,5 +26,9 @@ AnnotationAssertion(rdfs:label "ClassA") AnnotationAssertion(rdfs:label "ClassB") SubClassOf( ) +# Class: () + +SubClassOf( ) + ) \ No newline at end of file diff --git a/test/resources/simple.omn b/test/resources/simple.omn index 2cfb60b..c547442 100644 --- a/test/resources/simple.omn +++ b/test/resources/simple.omn @@ -7,3 +7,5 @@ Ontology: Annotations: rdfs:label "ClassB" SubClassOf: :A Class: :C + Class: :D + SubClassOf: :B diff --git a/test/resources/simple.omn.raw b/test/resources/simple.omn.raw index 2cfb60b..c547442 100644 --- a/test/resources/simple.omn.raw +++ b/test/resources/simple.omn.raw @@ -7,3 +7,5 @@ Ontology: Annotations: rdfs:label "ClassB" SubClassOf: :A Class: :C + Class: :D + SubClassOf: :B diff --git a/test/resources/simple.owl b/test/resources/simple.owl index 9b6d054..1fb5b99 100644 --- a/test/resources/simple.owl +++ b/test/resources/simple.owl @@ -41,6 +41,14 @@ + + + + + + + + diff --git a/test/resources/simple.owl.raw b/test/resources/simple.owl.raw index 9b6d054..1fb5b99 100644 --- a/test/resources/simple.owl.raw +++ b/test/resources/simple.owl.raw @@ -41,6 +41,14 @@ + + + + + + + + diff --git a/test/resources/simple.owx b/test/resources/simple.owx index 8103cf2..5e6e579 100644 --- a/test/resources/simple.owx +++ b/test/resources/simple.owx @@ -20,10 +20,17 @@ + + + + + + + https://example.com/A diff --git a/test/resources/simple.owx.raw b/test/resources/simple.owx.raw index 8103cf2..5e6e579 100644 --- a/test/resources/simple.owx.raw +++ b/test/resources/simple.owx.raw @@ -20,10 +20,17 @@ + + + + + + + https://example.com/A diff --git a/test/test_base.py b/test/test_base.py index 744713d..dc25e9c 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -1,7 +1,5 @@ import os -from typing import List - import pyhornedowl from pyhornedowl.model import * @@ -22,9 +20,13 @@ def simple_ontology_comps() -> List[Component]: DeclareClass(Class(IRI.parse("https://example.com/A"))), DeclareClass(Class(IRI.parse("https://example.com/B"))), DeclareClass(Class(IRI.parse("https://example.com/C"))), + DeclareClass(Class(IRI.parse("https://example.com/D"))), + SubClassOf( + sup=Class(IRI.parse("https://example.com/A")), + sub=Class(IRI.parse("https://example.com/B"))), SubClassOf( - Class(IRI.parse("https://example.com/A")), - Class(IRI.parse("https://example.com/B"))), + sup=Class(IRI.parse("https://example.com/B")), + sub=Class(IRI.parse("https://example.com/D"))), AnnotationAssertion( IRI.parse("https://example.com/A"), Annotation(AnnotationProperty(IRI.parse(RDFS_LABEL)), diff --git a/test/test_hierarchy.py b/test/test_hierarchy.py new file mode 100644 index 0000000..e6357d9 --- /dev/null +++ b/test/test_hierarchy.py @@ -0,0 +1,89 @@ +import unittest + +from test_base import simple_ontology + + +class HierarchyTestCase(unittest.TestCase): + def test_no_subclass(self): + o = simple_ontology() + + expected = set() + actual = o.get_subclasses(":C") + + self.assertSetEqual(expected, actual) + + def test_no_superclass(self): + o = simple_ontology() + + expected = set() + actual = o.get_superclasses(":C") + + self.assertSetEqual(expected, actual) + + def test_direct_subclass(self): + o = simple_ontology() + + expected = {"https://example.com/B"} + actual = o.get_subclasses(":A") + + self.assertSetEqual(expected, actual) + + def test_direct_superclass(self): + o = simple_ontology() + + expected = {"https://example.com/A"} + actual = o.get_superclasses(":B") + + self.assertSetEqual(expected, actual) + + def test_no_ancestors(self): + o = simple_ontology() + + expected = set() + actual = o.get_ancestors(":A") + + self.assertSetEqual(expected, actual) + + def test_no_descendants(self): + o = simple_ontology() + + expected = set() + actual = o.get_descendants(":C") + + self.assertSetEqual(expected, actual) + + def test_single_ancestors(self): + o = simple_ontology() + + expected = {"https://example.com/A"} + actual = o.get_ancestors(":B") + + self.assertSetEqual(expected, actual) + + def test_single_descendants(self): + o = simple_ontology() + + expected = {"https://example.com/D"} + actual = o.get_descendants(":B") + + self.assertSetEqual(expected, actual) + + def test_multiple_ancestors(self): + o = simple_ontology() + + expected = {"https://example.com/A", "https://example.com/B"} + actual = o.get_ancestors(":D") + + self.assertSetEqual(expected, actual) + + def test_multiple_descendants(self): + o = simple_ontology() + + expected = {"https://example.com/B", "https://example.com/D"} + actual = o.get_descendants(":A") + + self.assertSetEqual(expected, actual) + + +if __name__ == '__main__': + unittest.main()