diff --git a/.gitignore b/.gitignore index b0a5122553a10c652ad2318a3e9e2df03e245638..ad06501b3c59dee108ac1ed0562c8a5066f91c78 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ +*.pyc +__pycache__ +*.todo + env/* diff --git a/ontoScorer/__pycache__/metrics.cpython-311.pyc b/ontoScorer/__pycache__/metrics.cpython-311.pyc index b3d7b0f5b4273d827168b62f4a05c4bc64426e93..261155446af384b3fa8a8d952545d43dda6c6dec 100644 Binary files a/ontoScorer/__pycache__/metrics.cpython-311.pyc and b/ontoScorer/__pycache__/metrics.cpython-311.pyc differ diff --git a/ontoScorer/__pycache__/ontology.cpython-311.pyc b/ontoScorer/__pycache__/ontology.cpython-311.pyc index aafe00e11d0aad998e8c002a193da51608e8d62d..b71e1d0f3cad1e8ed6574796cb86610805e54287 100644 Binary files a/ontoScorer/__pycache__/ontology.cpython-311.pyc and b/ontoScorer/__pycache__/ontology.cpython-311.pyc differ diff --git a/ontoScorer/__pycache__/scorer.cpython-311.pyc b/ontoScorer/__pycache__/scorer.cpython-311.pyc index 15863905c230a177ffba8eb4a80f491d1fd94ecb..7696fe257f510bead13598ef2ee71e027b9b4612 100644 Binary files a/ontoScorer/__pycache__/scorer.cpython-311.pyc and b/ontoScorer/__pycache__/scorer.cpython-311.pyc differ diff --git a/ontoScorer/metrics.py b/ontoScorer/metrics.py index e905353156d991bc58e573834c279ae3a8f3a470..73fd413c38cce339f00800388902305f355985e0 100644 --- a/ontoScorer/metrics.py +++ b/ontoScorer/metrics.py @@ -8,6 +8,7 @@ #============================================================================== from sklearn.metrics import precision_score, recall_score, f1_score +from ontoScorer.ontology import Ontology class Metrics: def __init__(self): @@ -15,12 +16,14 @@ class Metrics: self.recall = 0 self.f1 = 0 - def calculate(self, reference_classes, generated_classes): + def calculate(self, reference_ontology, generated_ontology): + reference_classes = set([cls.name() for cls in reference_ontology.get_classes()]) + generated_classes = set([cls.name() for cls in generated_ontology.get_classes()]) + all_classes = reference_classes.union(generated_classes) y_true = [1 if cls in reference_classes else 0 for cls in all_classes] y_pred = [1 if cls in generated_classes else 0 for cls in all_classes] self.precision = precision_score(y_true, y_pred) self.recall = recall_score(y_true, y_pred) - self.f1 = f1_score(y_true, y_pred) - + self.f1 = f1_score(y_true, y_pred) \ No newline at end of file diff --git a/ontoScorer/ontology.py b/ontoScorer/ontology.py index f9403cbbe97ad455713da15ea5d7b27795c759dc..90d432950bd7f96755e8036d5c8a71e86cd3b717 100644 --- a/ontoScorer/ontology.py +++ b/ontoScorer/ontology.py @@ -2,34 +2,149 @@ # -*-coding:Utf-8 -* #============================================================================== -# ontoScorer: [brief description of the module] +# Ontology Analyzer #------------------------------------------------------------------------------ -# Detailed module description, if needed +# Extracts various elements (Classes, Object Properties, Data Properties, +# Individuals, Annotations) from RDF/OWL ontologies. Enables foundational +# comparisons between different ontologies. #============================================================================== -from rdflib import Graph, OWL +from rdflib import Graph, RDF, RDFS, OWL +from rdflib import URIRef, BNode from rdflib.namespace import split_uri + +#============================================================================== +# Classes: Element, NamedElement and BlankElement +#============================================================================== + +class Element: + def __init__(self, reference, graph): + self.reference = reference + self.graph = graph + + def properties(self): + """Retrieve properties associated with the element.""" + return list(self.graph.predicate_objects(subject=self.reference)) + + def name(self): + raise NotImplementedError("The method name() must be implemented by subclasses.") + +class NamedElement(Element): + def __init__(self, uri, graph): + super().__init__(uri, graph) + self.uri = uri + + def __str__(self): + return str(self.uri) + + def name(self): + _, element_name = split_uri(self.uri) + return element_name + +class BlankElement(Element): + def __init__(self, bnode, graph): + super().__init__(bnode, graph) + self.id = str(bnode) + + def __str__(self): + return f"BNode: {self.id}" + + def name(self): + return self.id + + + +#============================================================================== +# Class: Ontology +#============================================================================== + class Ontology: + + #-------------------------------------------------------------------------- + # Constructor(s) + #-------------------------------------------------------------------------- + def __init__(self, ontology_path): + """Initialize the Ontology object.""" self.path = ontology_path self.graph = self.load_ontology(ontology_path) self.classes = self.get_classes() + + #-------------------------------------------------------------------------- + # Base Method(s) + #-------------------------------------------------------------------------- + def load_ontology(self, path): + """Load the ontology from the given path into an RDF graph.""" g = Graph() g.parse(path, format="ttl") return g + def _get_elements_of_type(self, rdf_type): + """Extract all elements of a specific RDF type from the ontology.""" + elements = [] + for s, _, o in self.graph.triples((None, RDF.type, rdf_type)): + if isinstance(s, BNode): + elements.append(BlankElement(s, self.graph)) + elif isinstance(s, URIRef): + elements.append(NamedElement(s, self.graph)) + return elements + + + #-------------------------------------------------------------------------- + # Extracting Method(s) + #-------------------------------------------------------------------------- + def get_classes(self): - classes = set() - triplets_count = 0 - for s, p, o in self.graph.triples((None, None, None)): - triplets_count += 1 - if o == OWL.Class: - _, class_name = split_uri(s) - classes.add(class_name) - return classes - - def compare_to(self, other_ontology): - return self.classes, other_ontology.classes \ No newline at end of file + """Extract all classes from the ontology.""" + return self._get_elements_of_type(OWL.Class) + + def get_object_properties(self): + """Extract all object properties from the ontology.""" + return self._get_elements_of_type(OWL.ObjectProperty) + + def get_data_properties(self): + """Extract all data properties from the ontology.""" + return self._get_elements_of_type(OWL.DatatypeProperty) + + def get_restrictions(self): + """Extract all restrictons from the ontology.""" + return self._get_elements_of_type(OWL.Restriction) + + def get_individuals(self) -> list: + """Extract all individuals from the ontology.""" + all_types = set(self.graph.subjects(RDF.type)) + non_individuals = {element.reference for element in + self.get_classes() + + self.get_object_properties() + + self.get_data_properties()} + + individuals = all_types - non_individuals + + return [NamedElement(i, self.graph) if isinstance(i, URIRef) else BlankElement(i, self.graph) + for i in individuals] + + def get_annotations(self): + """Extract all annotation comments from the ontology.""" + annotations = set() + for _, _, o in self.graph.triples((None, RDFS.label, None)): + annotations.add(str(o)) + return annotations + + + #-------------------------------------------------------------------------- + # Comparison Method(s) + #-------------------------------------------------------------------------- + + def compare_to(self, other_ontology) -> tuple: + """Compare classes of the current ontology with another.""" + self_classes = {c.name() for c in self.classes} + other_classes = {c.name() for c in other_ontology.classes} + + # Pour des comparaisons plus avancées, ajustez ce code + unique_to_self = self_classes - other_classes + unique_to_other = other_classes - self_classes + + return unique_to_self, unique_to_other diff --git a/ontoScorer/scorer.py b/ontoScorer/scorer.py index 2ed808558b8a67385d0897e8010149eb4086bcf2..763d9301810b6624f2f15d6e4d853b5fa9cd29c7 100644 --- a/ontoScorer/scorer.py +++ b/ontoScorer/scorer.py @@ -19,7 +19,7 @@ class OntoScorer: def compare(self): self.comparison_result = self.reference_ontology.compare_to(self.generated_ontology) - self.metrics.calculate(*self.comparison_result) + self.metrics.calculate(self.reference_ontology, self.generated_ontology) def generate_report(self): report = Report(self.reference_ontology, diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 0000000000000000000000000000000000000000..c611d6eb47e6caafe8cabd172b8a49d061a56587 --- /dev/null +++ b/tests/context.py @@ -0,0 +1,10 @@ +import os +import sys + +CURRENT_DIRPATH = os.path.dirname(os.path.abspath(__file__)) +LIB_PATH = os.path.dirname(f'{CURRENT_DIRPATH}/../..') +print(f'Test Context: {LIB_PATH}') +sys.path.insert(0, os.path.abspath(LIB_PATH)) + +import ontoScorer + diff --git a/tests/test_data/ontology_a.ttl b/tests/test_data/ontology_a.ttl new file mode 100644 index 0000000000000000000000000000000000000000..2b5ee473b9aaafcdab815b06788bd4a9aa435474 --- /dev/null +++ b/tests/test_data/ontology_a.ttl @@ -0,0 +1,104 @@ +@prefix ns1: <https://tenet.tetras-libre.fr/base-ontology#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . + +<https://tenet.tetras-libre.fr/extract-result#SolarSystem> a owl:Individual, + <https://tenet.tetras-libre.fr/extract-result#system>, + <https://tenet.tetras-libre.fr/extract-result#system-hasPart-object>, + <https://tenet.tetras-libre.fr/extract-result#system-hasPart-sun> ; + rdfs:label "SolarSystem" ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#direct> a owl:ObjectProperty ; + rdfs:label "direct" ; + rdfs:subPropertyOf ns1:Out_ObjectProperty ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#gravitation-bind-system> a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty <https://tenet.tetras-libre.fr/extract-result#bind> ; + owl:someValuesFrom <https://tenet.tetras-libre.fr/extract-result#system> ], + <https://tenet.tetras-libre.fr/extract-result#gravitation> ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#hasManner> a owl:ObjectProperty ; + rdfs:label "hasManner" ; + rdfs:subPropertyOf ns1:Out_ObjectProperty ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#not-direct> a owl:ObjectProperty ; + rdfs:subPropertyOf ns1:Out_ObjectProperty ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#object-orbit-hasManner-direct-sun> a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty <https://tenet.tetras-libre.fr/extract-result#orbit-hasManner-direct> ; + owl:someValuesFrom <https://tenet.tetras-libre.fr/extract-result#sun> ], + <https://tenet.tetras-libre.fr/extract-result#object> ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#object-orbit-hasManner-not-direct-sun> a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty <https://tenet.tetras-libre.fr/extract-result#orbit-hasManner-not-direct> ; + owl:someValuesFrom <https://tenet.tetras-libre.fr/extract-result#sun> ], + <https://tenet.tetras-libre.fr/extract-result#object> ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#bind> a owl:ObjectProperty ; + rdfs:label "bind" ; + rdfs:subPropertyOf ns1:Out_ObjectProperty ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#gravitation> a owl:Class ; + rdfs:label "gravitation" ; + rdfs:subClassOf ns1:Entity ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#orbit-hasManner-direct> a owl:ObjectProperty ; + rdfs:subPropertyOf <https://tenet.tetras-libre.fr/extract-result#orbit> ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#orbit-hasManner-not-direct> a owl:ObjectProperty ; + rdfs:subPropertyOf <https://tenet.tetras-libre.fr/extract-result#orbit> ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#system-hasPart-object> a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty <https://tenet.tetras-libre.fr/extract-result#hasPart> ; + owl:someValuesFrom <https://tenet.tetras-libre.fr/extract-result#object> ], + <https://tenet.tetras-libre.fr/extract-result#system> ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#system-hasPart-sun> a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty <https://tenet.tetras-libre.fr/extract-result#hasPart> ; + owl:someValuesFrom <https://tenet.tetras-libre.fr/extract-result#sun> ], + <https://tenet.tetras-libre.fr/extract-result#system> ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#hasPart> a owl:ObjectProperty ; + rdfs:label "hasPart" ; + rdfs:subPropertyOf ns1:Out_ObjectProperty ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#orbit> a owl:ObjectProperty ; + rdfs:label "orbit" ; + rdfs:subPropertyOf ns1:Out_ObjectProperty ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#object> a owl:Class ; + rdfs:label "object" ; + rdfs:subClassOf ns1:Entity ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#sun> a owl:Class ; + rdfs:label "sun" ; + rdfs:subClassOf ns1:Entity ; + ns1:fromStructure "unknown" . + +<https://tenet.tetras-libre.fr/extract-result#system> a owl:Class ; + rdfs:label "system" ; + rdfs:subClassOf ns1:Entity, + ns1:Undetermined_Thing ; + ns1:fromStructure "unknown" . + diff --git a/tests/test_data/ontology_b.ttl b/tests/test_data/ontology_b.ttl new file mode 100644 index 0000000000000000000000000000000000000000..2b9713297e6616b251f740fc10991dc2f265c5c4 --- /dev/null +++ b/tests/test_data/ontology_b.ttl @@ -0,0 +1,109 @@ +@prefix base: <https://reference.tetras-libre.fr/base-ontology#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix result: <https://reference.tetras-libre.fr/expected-result#> . + +result:SolarSystem a owl:Individual, + result:system, + result:system-isBindBy-gravitation, + result:system-hasPart-object-orbit-sun, + result:system-hasPart-sun ; + rdfs:label "SolarSystem" . + +result:direct a owl:ObjectProperty ; + rdfs:label "direct" ; + rdfs:subPropertyOf base:Out_ObjectProperty . + +result:gravitation-bind-system a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty result:bind ; + owl:someValuesFrom result:system ], + result:gravitation . + +result:hasManner a owl:ObjectProperty ; + rdfs:label "hasManner" ; + rdfs:subPropertyOf base:Out_ObjectProperty . + +result:not-direct a owl:ObjectProperty ; + rdfs:subPropertyOf base:Out_ObjectProperty . + +result:object-orbit-sun a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty result:orbit ; + owl:someValuesFrom result:sun ], + result:object . + +result:object-orbit-hasManner-direct-sun a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty result:orbit-hasManner-direct ; + owl:someValuesFrom result:sun ], + result:object-orbit-sun . + +result:object-orbit-hasManner-not-direct-sun a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty result:orbit-hasManner-not-direct ; + owl:someValuesFrom result:sun ], + result:object-orbit-sun . + +result:gravitation a owl:Class ; + rdfs:label "gravitation" ; + rdfs:subClassOf base:Entity . + +result:system-isBindBy-gravitation a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty result:isBindBy ; + owl:someValuesFrom result:gravitation ], + result:system . + +result:system-hasPart-object a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty result:hasPart ; + owl:someValuesFrom result:object ], + result:system . + +result:system-hasPart-object-orbit-sun a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty result:hasPart ; + owl:someValuesFrom result:object-orbit-sun ], + result:system-hasPart-object . + +result:system-hasPart-sun a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty result:hasPart ; + owl:someValuesFrom result:sun ], + result:system . + +result:bind a owl:ObjectProperty ; + rdfs:label "bind" ; + rdfs:subPropertyOf base:Out_ObjectProperty . + +result:isBindBy owl:inverseOf result:bind ; + rdfs:label "isBindBy" ; + rdfs:subPropertyOf base:Out_ObjectProperty . + +result:orbit a owl:ObjectProperty ; + rdfs:label "orbit" ; + rdfs:subPropertyOf base:Out_ObjectProperty . + +result:orbit-hasManner-direct a owl:ObjectProperty ; + rdfs:subPropertyOf result:orbit . + +result:orbit-hasManner-not-direct a owl:ObjectProperty ; + rdfs:subPropertyOf result:orbit . + +result:hasPart a owl:ObjectProperty ; + rdfs:label "hasPart" ; + rdfs:subPropertyOf base:Out_ObjectProperty . + +result:object a owl:Class ; + rdfs:label "object" ; + rdfs:subClassOf base:Entity . + +result:system a owl:Class ; + rdfs:label "system" ; + rdfs:subClassOf base:Entity. + +result:sun a owl:Class ; + rdfs:label "sun" ; + rdfs:subClassOf base:Entity . + diff --git a/tests/test_ontology.py b/tests/test_ontology.py index 4415d78eaa2ad835966f1b350fdefbdf3e9d9d61..8da3b2eea6aad01ab668e23029d6fcf0e27fb401 100644 --- a/tests/test_ontology.py +++ b/tests/test_ontology.py @@ -2,9 +2,89 @@ # -*-coding:Utf-8 -* #============================================================================== -# ontoScorer: [brief description of the module] +# test_ontology: Ontology Testing Module #------------------------------------------------------------------------------ -# Detailed module description, if needed +# Contains tests for verifying functionality of the Ontology class. #============================================================================== -# TODO +import unittest +import os +from context import ontoScorer +from ontoScorer.ontology import Ontology +from rdflib import URIRef + +class TestOntology(unittest.TestCase): + + def setUp(self): + # Test ontology paths + DATA_FOLDER_PATH = f'{os.path.dirname(os.path.abspath(__file__))}/test_data' + self.ontology1_path = f"{DATA_FOLDER_PATH}/ontology_a.ttl" + self.ontology2_path = f"{DATA_FOLDER_PATH}/ontology_b.ttl" + self.onto1 = Ontology(self.ontology1_path) + self.onto2 = Ontology(self.ontology2_path) + + def test_load_ontology(self): + self.assertIsNotNone(self.onto1.graph) + self.assertIsNotNone(self.onto2.graph) + + def test_get_classes(self): + classes1_names = {c.name() for c in self.onto1.get_classes()} + classes2_names = {c.name() for c in self.onto2.get_classes()} + + self.assertIn("gravitation-bind-system", classes1_names) + self.assertIn("gravitation-bind-system", classes1_names) + self.assertIn("object-orbit-hasManner-direct-sun", classes1_names) + self.assertIn("object-orbit-hasManner-not-direct-sun", classes1_names) + self.assertIn("gravitation", classes1_names) + self.assertIn("system-hasPart-object", classes1_names) + self.assertIn("system-hasPart-sun", classes1_names) + self.assertIn("object", classes1_names) + self.assertIn("sun", classes1_names) + self.assertIn("system", classes1_names) + self.assertNotIn("system-isBindBy-gravitation", classes1_names) + self.assertIn("system-isBindBy-gravitation", classes2_names) + + def test_get_object_properties(self): + object_properties_names = {op.name() for op in self.onto1.get_object_properties()} + + self.assertIn("direct", object_properties_names) + self.assertIn("hasManner", object_properties_names) + self.assertIn("not-direct", object_properties_names) + self.assertIn("bind", object_properties_names) + self.assertIn("orbit-hasManner-direct", object_properties_names) + self.assertIn("orbit-hasManner-not-direct", object_properties_names) + self.assertIn("hasPart", object_properties_names) + self.assertIn("orbit", object_properties_names) + + def test_get_data_properties(self): + data_properties_names = {dp.name() for dp in self.onto1.get_data_properties()} + + self.assertEqual(len(data_properties_names), 0) + + def test_get_restrictions(self): + restrictions = self.onto1.get_restrictions() + expected_restrictions_count = 5 + self.assertEqual(len(restrictions), expected_restrictions_count) + + def test_get_individuals(self): + individuals_names = {ind.name() for ind in self.onto1.get_individuals()} + + self.assertIn("SolarSystem", individuals_names) + self.assertNotIn("gravitation", individuals_names) + + def test_get_annotations(self): + annotations = self.onto1.get_annotations() + self.assertIn("SolarSystem", annotations) + self.assertIn("direct", annotations) + self.assertIn("gravitation", annotations) + self.assertIn("orbit", annotations) + self.assertIn("object", annotations) + self.assertIn("sun", annotations) + self.assertIn("system", annotations) + + def test_compare_to(self): + comparison_result = self.onto1.compare_to(self.onto2) + self.assertIsInstance(comparison_result, tuple) + +if __name__ == "__main__": + unittest.main()