diff --git a/JPS_BASE_LIB/python_wrapper/docs/examples/ogm.md b/JPS_BASE_LIB/python_wrapper/docs/examples/ogm.md
index 72ceed32c71..d76aef002a7 100644
--- a/JPS_BASE_LIB/python_wrapper/docs/examples/ogm.md
+++ b/JPS_BASE_LIB/python_wrapper/docs/examples/ogm.md
@@ -51,6 +51,9 @@ For simplicity, `` (**Note the
@prefix yo: .
```
+> NOTE: if you wish to develop this in a Jupyter notebook, you might find it helpful to set the ontology to development mode using `YourOntology.set_dev_mode()`, which will allow you re-run the cell once you made changes to your classes/properties without throwing an "class already registered" error. Once you are happy with your ontology and wish to switch back to production mode, you may do this via `YourOntology.set_prod_mode()`.
+
+
### Define a property (relationship)
To define custom object and data properties, the two base classes `ObjectProperty` and `DatatypeProperty` should be used respectively. It should be noted that the user is only required to specify the cardinality of these properties at the class defination, as their `rdfs:domain` and `rdfs:range` will be automatically handled by the class that utilises the defined properties.
@@ -347,6 +350,26 @@ another_object_of_one_concept = OneConcept.pull_from_kg(
> NOTE the developer should be aware of the `recursive_depth` that one is using to pull the triples from the knowledge graph.
+#### NOTE when pulling instances with multiple `rdf:type` definitions
+
+For instances defined with multiple `rdf:type`, this pulling function instantiates the Python object using the deepest subclass found in the intersection of the subclasses of the calling class and those specified by `rdf:type`. If multiple deepest subclasses coexist (e.g., when subclasses from different branches of the inheritance tree are identified), the code raises an error. To prevent this, you can pull the object directly using the desired subclass.
+
+For a concrete example using the class hierarchy below, assume an instance is defined with `rdf:type` of both class `C` and `E`. Pulling this instance using `A.pull_from_kg()` will result in an error because both `C` and `E` are identified as potential classes for instantiation, but they belong to different branches of the inheritance tree. A workaround is to pull the instance explicitly using either class `C` or `E`. Alternatively, if a class `F` exist as subclass of both `C` and `E`, pulling the instance with `A.pull_from_kg()` would succeed, as class `F` would be identified as the new "deepest" subclass.
+
+```mermaid
+classDiagram
+ class A
+ class B
+ class C
+ class D
+ class E
+
+ A <|-- B
+ B <|-- C
+ A <|-- D
+ D <|-- E
+```
+
### Update existing objects in triple store
To make changes to the local objects and update it in the triple store:
@@ -378,3 +401,7 @@ one_concept.revert_local_changes()
- How to generate Python script given an OWL file
- Add support for many-to-many cardinality constraints?
+- Mermaid codes
+- Type hint for object/datatype properties
+- Allocate set or single instances when accessing object/datatype properties
+- Handle rdf:type when it's a class
diff --git a/JPS_BASE_LIB/python_wrapper/setup.py b/JPS_BASE_LIB/python_wrapper/setup.py
index 951b965c235..4ca602757c3 100644
--- a/JPS_BASE_LIB/python_wrapper/setup.py
+++ b/JPS_BASE_LIB/python_wrapper/setup.py
@@ -2,7 +2,7 @@
setup(
name='twa',
- version='0.0.4',
+ version='0.0.6',
author='Jiaru Bai; Daniel Nurkowski',
author_email='jb2197@cam.ac.uk; danieln@cmclinnovations.com',
license='MIT',
diff --git a/JPS_BASE_LIB/python_wrapper/tests/conftest.py b/JPS_BASE_LIB/python_wrapper/tests/conftest.py
index 4749a805a06..46cd9ac1ad4 100644
--- a/JPS_BASE_LIB/python_wrapper/tests/conftest.py
+++ b/JPS_BASE_LIB/python_wrapper/tests/conftest.py
@@ -186,7 +186,7 @@ def _get_service_url(service_name, url_route):
time.sleep(3)
if not service_available:
- raise RuntimeError("Blazegraph service did not become available within the timeout period")
+ raise RuntimeError(f"Blazegraph service did not become available within the timeout period: {timeout} seconds")
return service_url
return _get_service_url
diff --git a/JPS_BASE_LIB/python_wrapper/tests/test_base_ontology.py b/JPS_BASE_LIB/python_wrapper/tests/test_base_ontology.py
index 766324feda3..231bb7a1450 100644
--- a/JPS_BASE_LIB/python_wrapper/tests/test_base_ontology.py
+++ b/JPS_BASE_LIB/python_wrapper/tests/test_base_ontology.py
@@ -77,6 +77,10 @@ class E(D):
pass
+class For_Dev_Mode_Test(BaseClass):
+ rdfs_isDefinedBy = ExampleOntology
+
+
HasPart = TransitiveProperty.create_from_base('HasPart', ExampleOntology)
@@ -104,6 +108,29 @@ def init():
return a1, a2, a3, b, c, d
+def test_dev_mode():
+ assert not ExampleOntology._dev_mode
+ assert not ExampleOntology.is_dev_mode()
+ ExampleOntology.set_dev_mode()
+ assert ExampleOntology._dev_mode
+ assert ExampleOntology.is_dev_mode()
+ ExampleOntology.set_prod_mode()
+ assert not ExampleOntology._dev_mode
+ assert not ExampleOntology.is_dev_mode()
+ with pytest.raises(ValueError) as e_info:
+ class For_Dev_Mode_Test(BaseClass):
+ rdfs_isDefinedBy = ExampleOntology
+ assert e_info.match('https://example.org/example/For_Dev_Mode_Test')
+ assert e_info.match('already exists in')
+ """
+ E ValueError: Class with rdf_type https://example.org/example/For_Dev_Mode_Test already exists in
+ : .
+ """
+ ExampleOntology.set_dev_mode()
+ class For_Dev_Mode_Test(BaseClass):
+ rdfs_isDefinedBy = ExampleOntology
+
+
def test_retrieve_cardinality():
assert DataProperty_A.retrieve_cardinality() == (0, 1)
assert DataProperty_B.retrieve_cardinality() == (1, 1)
@@ -151,8 +178,8 @@ def test_basics():
a = A(data_property_a={'a'}, rdfs_comment='my comment', rdfs_label='my label')
assert a.data_property_a == {'a'}
assert a.rdfs_isDefinedBy.base_url in a.instance_iri
- assert a.rdfs_comment == 'my comment'
- assert a.rdfs_label == 'my label'
+ assert a.rdfs_comment == {'my comment'}
+ assert a.rdfs_label == {'my label'}
# test one can instantiate with a custom instance_iri
my_random_iri = f'https://{str(uuid.uuid4())}'
a_with_random_iri = A(data_property_a={'a'}, instance_iri=my_random_iri)
@@ -915,3 +942,172 @@ def test_all_triples_of_nodes():
assert (URIRef(d.instance_iri), URIRef(ObjectProperty_D_A.predicate_iri), URIRef(a1.instance_iri)) in g
# in total 6 triples
assert sum(1 for _ in g.triples((None, None, None))) == 6
+
+
+def test_cls_rdfs_comment_label():
+ comments = ['comment1', 'comment2', 'comment3']
+ labels = ['label1', 'label2']
+
+ class TestRdfsCommentLabel(E):
+ rdfs_comment_clz = comments
+ rdfs_label_clz = labels
+
+ class TestRdfsCommentLabelDataProperty(DatatypeProperty):
+ rdfs_isDefinedBy = ExampleOntology
+ rdfs_comment_clz = comments
+ rdfs_label_clz = labels
+
+ class TestRdfsCommentLabelObjectProperty(ObjectProperty):
+ rdfs_isDefinedBy = ExampleOntology
+ rdfs_comment_clz = comments
+ rdfs_label_clz = labels
+
+ g = Graph()
+ g = TestRdfsCommentLabel._export_to_owl(g)
+ g = TestRdfsCommentLabelDataProperty._export_to_owl(g, set(), set())
+ g = TestRdfsCommentLabelObjectProperty._export_to_owl(g, set(), set())
+ # rdfs:comment triple
+ for comment in comments:
+ assert (URIRef(TestRdfsCommentLabel.rdf_type), URIRef(RDFS.comment), Literal(comment)) in g
+ assert (URIRef(TestRdfsCommentLabelDataProperty.predicate_iri), URIRef(RDFS.comment), Literal(comment)) in g
+ assert (URIRef(TestRdfsCommentLabelObjectProperty.predicate_iri), URIRef(RDFS.comment), Literal(comment)) in g
+ # rdfs:label triple
+ for label in labels:
+ assert (URIRef(TestRdfsCommentLabel.rdf_type), URIRef(RDFS.label), Literal(label)) in g
+ assert (URIRef(TestRdfsCommentLabelDataProperty.predicate_iri), URIRef(RDFS.label), Literal(label)) in g
+ assert (URIRef(TestRdfsCommentLabelObjectProperty.predicate_iri), URIRef(RDFS.label), Literal(label)) in g
+
+
+def test_instances_with_multiple_rdf_type(initialise_sparql_client, recwarn):
+ a1, a2, a3, b, c, d = init()
+ # create data property and classes for this test
+ Data_Property_E_Sub = DatatypeProperty.create_from_base('Data_Property_E_Sub', ExampleOntology, 0, 2)
+ Data_Property_E_Para = DatatypeProperty.create_from_base('Data_Property_Parallel_To_E', ExampleOntology, 0, 2)
+ class E_Sub(E):
+ # this class is used to test the case that the object is pulled from the KG with the correct level of subclass
+ # i.e. if the object is instance of E but pulled using class D, it should NOT be pulled as E_Sub even E_Sub is a subclass of E
+ data_property_e_sub: Data_Property_E_Sub[str]
+ class E_Para(D):
+ data_property_e_para: Data_Property_E_Para[str]
+
+ # create an object e and e_sub and push it to the KG
+ INFO_NOT_LOST_FOR_E_SUB = 'this is to test information not lost for e_sub'
+ INFO_NOT_LOST_FOR_E_PARA = 'this is to test information not lost for parallel_to_e'
+ NEW_INFO_FOR_E_SUB = 'this is to test new information for e_sub'
+ NEW_INFO_FOR_E_PARA = 'this is to test new information for parallel_to_e'
+ e = E(object_property_d_a=[a1], object_property_d_c=[c])
+ e_sub = E_Sub(object_property_d_a=[a1], object_property_d_c=[c], data_property_e_sub=INFO_NOT_LOST_FOR_E_SUB)
+ e_para = E_Para(object_property_d_a=[a1], object_property_d_c=[c], data_property_e_para=INFO_NOT_LOST_FOR_E_PARA)
+ sparql_client = initialise_sparql_client
+ e.push_to_kg(sparql_client, -1)
+ e_sub.push_to_kg(sparql_client, -1)
+ e_para.push_to_kg(sparql_client, -1)
+
+ # now also insert the triples for additional rdf:type of e and e_sub due to subclassing
+ sparql_client.perform_update(f'insert data {{ <{e.instance_iri}> <{RDF.type.toPython()}> <{D.rdf_type}> }}')
+ sparql_client.perform_update(f'insert data {{ <{e_sub.instance_iri}> <{RDF.type.toPython()}> <{D.rdf_type}> }}')
+ sparql_client.perform_update(f'insert data {{ <{e_sub.instance_iri}> <{RDF.type.toPython()}> <{E.rdf_type}> }}')
+ # create addtional triples to make e_para also rdf:type of E_Sub, as well as data property for E_Sub
+ sparql_client.perform_update(f'insert data {{ <{e_para.instance_iri}> <{RDF.type.toPython()}> <{E_Sub.rdf_type}> }}')
+ sparql_client.perform_update(f'insert data {{ <{e_para.instance_iri}> <{Data_Property_E_Sub.predicate_iri}> "{INFO_NOT_LOST_FOR_E_SUB}" }}')
+
+ # after clearing the ontology object lookup, the object should be pulled from the KG again as a fresh object
+ KnowledgeGraph.clear_object_lookup()
+
+ # test 1: pull the object e using D class but it should return as E object
+ e_pulled = D.pull_from_kg([e.instance_iri], sparql_client, -1)[0]
+ # the id of the object should be different, meaning it's a different object
+ assert id(e) != id(e_pulled)
+ # the pulled object should also be instance of E, but not D
+ assert type(e_pulled) is E
+ assert type(e_pulled) is not D
+ assert type(e_pulled) is not E_Sub
+
+ # test 2: pull the object e using E_Sub class which should raise error
+ with pytest.raises(ValueError) as e_info:
+ E_Sub.pull_from_kg([e.instance_iri], sparql_client, -1)
+ assert e_info.match(f"""The instance {e.instance_iri} is of type """)
+ assert e_info.match(f"""{E.rdf_type}""")
+ assert e_info.match(f"""{D.rdf_type}""")
+ assert e_info.match(f"""it doesn't match the rdf:type of class {E_Sub.__name__} \({E_Sub.rdf_type}\)""")
+
+ # test 3: pull the object e_sub using D class which should return as E_Sub object
+ e_sub_pulled = D.pull_from_kg([e_sub.instance_iri], sparql_client, -1)[0]
+ # the id of the object should be different, meaning it's a different object
+ assert id(e_sub) != id(e_sub_pulled)
+ # the pulled object should also be instance of E_Sub, but not D
+ assert type(e_sub_pulled) is E_Sub
+ assert type(e_sub_pulled) is not D
+ assert type(e_sub_pulled) is not E
+ # the information should be preserved
+ assert e_sub_pulled.data_property_e_sub == {INFO_NOT_LOST_FOR_E_SUB}
+
+ # test 4: pull the object e_para using D class should throw an error as there's no subclass relation between E_Sub and E_Para
+ with pytest.raises(ValueError) as e_info:
+ D.pull_from_kg([e_para.instance_iri], sparql_client, -1)
+ assert e_info.match(f"""The instance {e_para.instance_iri} is of type """)
+ assert e_info.match(f"""Amongst the pulling class {D.__name__} \({D.rdf_type}\)""")
+ assert e_info.match(f"""and its subclasses \({D.construct_subclass_dictionary()}\)""")
+ assert e_info.match(f"""{E_Sub.rdf_type}""")
+ assert e_info.match(f"""{E_Para.rdf_type}""")
+ assert e_info.match(f"""there exist classes that are not in the same branch of the inheritance tree""")
+ assert e_info.match(f"""please check the inheritance tree is correctly defined in Python""")
+
+ # test 5: pull the object e_para using E_Sub class should return as E_Sub object
+ e_para_pulled_as_e_sub = E_Sub.pull_from_kg([e_para.instance_iri], sparql_client, -1)[0]
+ # the id of the object should be different, meaning it's a different object
+ assert id(e_para) != id(e_para_pulled_as_e_sub)
+ # the pulled object should also be instance of E_Sub, but not E_Para
+ assert type(e_para_pulled_as_e_sub) is E_Sub
+ assert type(e_para_pulled_as_e_sub) is not E_Para
+ assert type(e_para_pulled_as_e_sub) is not D
+ # the information should be preserved
+ assert e_para_pulled_as_e_sub.data_property_e_sub == {INFO_NOT_LOST_FOR_E_SUB}
+ # if I now change the data property of E_Sub, it should not affect the data property of E_Para which is not pulled as part of e_para_pulled_as_e_sub
+ e_para_pulled_as_e_sub.data_property_e_sub.add(NEW_INFO_FOR_E_SUB)
+ e_para_pulled_as_e_sub.push_to_kg(sparql_client, -1)
+ assert sparql_client.check_if_triple_exist(e_para.instance_iri, Data_Property_E_Sub.predicate_iri, NEW_INFO_FOR_E_SUB, XSD.string.toPython())
+ assert sparql_client.check_if_triple_exist(e_para.instance_iri, Data_Property_E_Sub.predicate_iri, INFO_NOT_LOST_FOR_E_SUB, XSD.string.toPython())
+ assert sparql_client.check_if_triple_exist(e_para.instance_iri, Data_Property_E_Para.predicate_iri, INFO_NOT_LOST_FOR_E_PARA, XSD.string.toPython())
+
+ # test 6: pull the object e_para using E_Para class should return as E_Para object
+ e_para_pulled_as_e_para = E_Para.pull_from_kg([e_para.instance_iri], sparql_client, -1)[0]
+ # the id of the object should be different, meaning it's a different object
+ assert id(e_para) != id(e_para_pulled_as_e_para)
+ # the pulled object should also be instance of E_Para, but not E_Sub
+ assert type(e_para_pulled_as_e_para) is E_Para
+ assert type(e_para_pulled_as_e_para) is not E_Sub
+ assert type(e_para_pulled_as_e_para) is not D
+ # the information should be preserved
+ assert e_para_pulled_as_e_para.data_property_e_para == {INFO_NOT_LOST_FOR_E_PARA}
+ # if I now change the data property of E_Para, it should not affect the data property of E_Sub which is not pulled as part of e_para_pulled_as_e_para
+ e_para_pulled_as_e_para.data_property_e_para.add(NEW_INFO_FOR_E_PARA)
+ e_para_pulled_as_e_para.push_to_kg(sparql_client, -1)
+ assert sparql_client.check_if_triple_exist(e_para.instance_iri, Data_Property_E_Para.predicate_iri, NEW_INFO_FOR_E_PARA, XSD.string.toPython())
+ assert sparql_client.check_if_triple_exist(e_para.instance_iri, Data_Property_E_Para.predicate_iri, INFO_NOT_LOST_FOR_E_PARA, XSD.string.toPython())
+ assert sparql_client.check_if_triple_exist(e_para.instance_iri, Data_Property_E_Sub.predicate_iri, INFO_NOT_LOST_FOR_E_SUB, XSD.string.toPython())
+ assert sparql_client.check_if_triple_exist(e_para.instance_iri, Data_Property_E_Sub.predicate_iri, NEW_INFO_FOR_E_SUB, XSD.string.toPython())
+
+ # test 7: create a new class and make it subclass of E_Sub and E_Para, then pulling it with D class should return as the new class object
+ class New_E_Super_Sub(E_Para, E_Sub):
+ pass
+ # make the object e_para also rdf:type of New_E_Super_Sub
+ sparql_client.perform_update(f'insert data {{ <{e_para.instance_iri}> <{RDF.type.toPython()}> <{New_E_Super_Sub.rdf_type}> }}')
+ e_super_sub = D.pull_from_kg([e_para.instance_iri], sparql_client, -1)[0]
+ # the id of the object should be different, meaning it's a different object
+ assert id(e_para) != id(e_super_sub)
+ # the pulled object should also be instance of New_E_Super_Sub, but not E_Sub nor E_Para
+ assert type(e_super_sub) is New_E_Super_Sub
+ assert type(e_super_sub) is not E_Sub
+ assert type(e_super_sub) is not E_Para
+ assert type(e_super_sub) is not D
+ # the information should be preserved
+ assert e_super_sub.data_property_e_para == {INFO_NOT_LOST_FOR_E_PARA, NEW_INFO_FOR_E_PARA}
+ assert e_super_sub.data_property_e_sub == {INFO_NOT_LOST_FOR_E_SUB, NEW_INFO_FOR_E_SUB}
+
+ # final check: all the warning messages when overiwritting the pulled object in the registry
+ assert len(recwarn) == 2
+ warning_message_1 = str(recwarn[0].message)
+ assert f"""An object with the same IRI {e_para.instance_iri} has already been instantiated and registered with type {E_Sub}. Replacing its regiatration now with type {E_Para}.""" in warning_message_1
+ warning_message_2 = str(recwarn[1].message)
+ assert f"""An object with the same IRI {e_para.instance_iri} has already been instantiated and registered with type {E_Para}. Replacing its regiatration now with type {New_E_Super_Sub}.""" in warning_message_2
diff --git a/JPS_BASE_LIB/python_wrapper/twa/__init__.py b/JPS_BASE_LIB/python_wrapper/twa/__init__.py
index c9395dd36b4..e253d541ad9 100644
--- a/JPS_BASE_LIB/python_wrapper/twa/__init__.py
+++ b/JPS_BASE_LIB/python_wrapper/twa/__init__.py
@@ -1,3 +1,3 @@
from twa.JPSGateway import JPSGateway
-__version__ = "0.0.4"
+__version__ = "0.0.6"
diff --git a/JPS_BASE_LIB/python_wrapper/twa/data_model/base_ontology.py b/JPS_BASE_LIB/python_wrapper/twa/data_model/base_ontology.py
index 2c4b9584a62..9ef4e927bc5 100644
--- a/JPS_BASE_LIB/python_wrapper/twa/data_model/base_ontology.py
+++ b/JPS_BASE_LIB/python_wrapper/twa/data_model/base_ontology.py
@@ -170,7 +170,7 @@ def _register_ontology(cls, ontology: BaseOntology):
cls.ontology_lookup[ontology.namespace_iri] = ontology
@classmethod
- def _register_class(cls, ontology_class: BaseClass):
+ def _register_class(cls, ontology_class: BaseClass, dev_mode: bool = False):
"""
This method registers a BaseClass (the Pydantic class itself) to the knowledge graph in Python memory.
@@ -180,12 +180,12 @@ def _register_class(cls, ontology_class: BaseClass):
if cls.class_lookup is None:
cls.class_lookup = {}
- if ontology_class.rdf_type in cls.class_lookup:
+ if ontology_class.rdf_type in cls.class_lookup and not dev_mode:
raise ValueError(f'Class with rdf_type {ontology_class.rdf_type} already exists in the knowledge graph: {cls.class_lookup[ontology_class.rdf_type]}.')
cls.class_lookup[ontology_class.rdf_type] = ontology_class
@classmethod
- def _register_property(cls, prop: BaseProperty):
+ def _register_property(cls, prop: BaseProperty, dev_mode: bool = False):
"""
This method registers a BaseProperty (the Pydantic class itself) to the knowledge graph in Python memory.
@@ -195,7 +195,7 @@ def _register_property(cls, prop: BaseProperty):
if cls.property_lookup is None:
cls.property_lookup = {}
- if prop.predicate_iri in cls.property_lookup:
+ if prop.predicate_iri in cls.property_lookup and not dev_mode:
raise ValueError(f'Property with predicate IRI {prop.predicate_iri} already exists in the knowledge graph: {cls.property_lookup[prop.predicate_iri]}.')
cls.property_lookup[prop.predicate_iri] = prop
@@ -260,8 +260,9 @@ class BaseOntology(BaseModel):
class_lookup: ClassVar[Dict[str, BaseClass]] = None
object_property_lookup: ClassVar[Dict[str, ObjectProperty]] = None
data_property_lookup: ClassVar[Dict[str, DatatypeProperty]] = None
- rdfs_comment: ClassVar[str] = None
+ rdfs_comment: ClassVar[Set[str]] = None
owl_versionInfo: ClassVar[str] = None
+ _dev_mode: ClassVar[bool] = False
@classmethod
def __pydantic_init_subclass__(cls, **kwargs):
@@ -276,6 +277,21 @@ def __pydantic_init_subclass__(cls, **kwargs):
# register the ontology to the knowledge graph
KnowledgeGraph._register_ontology(cls)
+ @classmethod
+ def is_dev_mode(cls):
+ """This method returns whether the KnowledgeGraph is in development mode."""
+ return cls._dev_mode
+
+ @classmethod
+ def set_dev_mode(cls):
+ """This method sets the KnowledgeGraph to development mode, where duplicate class or property registration will be allowed that the existing ones will be overwritten."""
+ cls._dev_mode = True
+
+ @classmethod
+ def set_prod_mode(cls):
+ """This method sets the KnowledgeGraph to production mode, where duplicate class or property registration will raise an error."""
+ cls._dev_mode = False
+
@classmethod
def _register_class(cls, ontolgy_class: BaseClass):
"""
@@ -289,12 +305,12 @@ def _register_class(cls, ontolgy_class: BaseClass):
if cls.class_lookup is None:
cls.class_lookup = {}
- if ontolgy_class.rdf_type in cls.class_lookup:
+ if ontolgy_class.rdf_type in cls.class_lookup and not cls.is_dev_mode():
raise ValueError(f'Class with rdf_type {ontolgy_class.rdf_type} already exists in {cls}: {cls.class_lookup[ontolgy_class.rdf_type]}.')
cls.class_lookup[ontolgy_class.rdf_type] = ontolgy_class
# also register with knowledge graph
- KnowledgeGraph._register_class(ontolgy_class)
+ KnowledgeGraph._register_class(ontolgy_class, cls.is_dev_mode())
@classmethod
def _register_object_property(cls, prop: ObjectProperty):
@@ -309,12 +325,12 @@ def _register_object_property(cls, prop: ObjectProperty):
if cls.object_property_lookup is None:
cls.object_property_lookup = {}
- if prop.predicate_iri in cls.object_property_lookup:
+ if prop.predicate_iri in cls.object_property_lookup and not cls.is_dev_mode():
raise ValueError(f'Object property with predicate IRI {prop.predicate_iri} already exists in {cls}: {cls.object_property_lookup[prop.predicate_iri]}.')
cls.object_property_lookup[prop.predicate_iri] = prop
# also register with knowledge graph
- KnowledgeGraph._register_property(prop)
+ KnowledgeGraph._register_property(prop, cls.is_dev_mode())
@classmethod
def _register_data_property(cls, prop: DatatypeProperty):
@@ -329,12 +345,12 @@ def _register_data_property(cls, prop: DatatypeProperty):
if cls.data_property_lookup is None:
cls.data_property_lookup = {}
- if prop.predicate_iri in cls.data_property_lookup:
+ if prop.predicate_iri in cls.data_property_lookup and not cls.is_dev_mode():
raise ValueError(f'Data property with predicate IRI {prop.predicate_iri} already exists in {cls}: {cls.data_property_lookup[prop.predicate_iri]}.')
cls.data_property_lookup[prop.predicate_iri] = prop
# also register with knowledge graph
- KnowledgeGraph._register_property(prop)
+ KnowledgeGraph._register_property(prop, cls.is_dev_mode())
@classmethod
def export_to_graph(cls, g: Graph = None) -> Graph:
@@ -351,7 +367,11 @@ def export_to_graph(cls, g: Graph = None) -> Graph:
g.add((URIRef(cls.namespace_iri), RDF.type, OWL.Ontology))
g.add((URIRef(cls.namespace_iri), DC.date, Literal(datetime.now().isoformat())))
if bool(cls.rdfs_comment):
- g.add((URIRef(cls.namespace_iri), RDFS.comment, Literal(cls.rdfs_comment)))
+ if isinstance(cls.rdfs_comment, str):
+ g.add((URIRef(cls.namespace_iri), RDFS.comment, Literal(cls.rdfs_comment)))
+ elif isinstance(cls.rdfs_comment, set):
+ for comment in cls.rdfs_comment:
+ g.add((URIRef(cls.namespace_iri), RDFS.comment, Literal(comment)))
if bool(cls.owl_versionInfo):
g.add((URIRef(cls.namespace_iri), OWL.versionInfo, Literal(cls.owl_versionInfo)))
# handle all classes
@@ -426,6 +446,8 @@ class BaseProperty(set, Generic[T]):
"""
rdfs_isDefinedBy: ClassVar[Type[BaseOntology]] = None
predicate_iri: ClassVar[str] = None
+ rdfs_comment_clz: ClassVar[Set[str]] = None
+ rdfs_label_clz: ClassVar[Set[str]] = None
owl_minQualifiedCardinality: ClassVar[int] = 0
owl_maxQualifiedCardinality: ClassVar[int] = None
@@ -513,6 +535,13 @@ def _export_to_owl(
idx = cls.__mro__.index(DatatypeProperty)
for i in range(1, idx):
g.add((URIRef(property_iri), RDFS.subPropertyOf, URIRef(cls.__mro__[i].predicate_iri)))
+ # add rdfs_comment_clz and rdfs_label_clz for class
+ if bool(cls.rdfs_comment_clz):
+ for comment in cls.rdfs_comment_clz:
+ g.add((URIRef(property_iri), RDFS.comment, Literal(comment)))
+ if bool(cls.rdfs_label_clz):
+ for label in cls.rdfs_label_clz:
+ g.add((URIRef(property_iri), RDFS.label, Literal(label)))
# add domain
if len(rdfs_domain) == 0:
# it is possible that a property is defined without specifying its domain, so we only print a warning
@@ -704,8 +733,10 @@ class MyClass(BaseClass):
rdf_type: ClassVar[str] = OWL_BASE_URL + 'Class'
""" > NOTE rdf_type is the automatically generated IRI of the class which can also be accessed at the instance level. """
object_lookup: ClassVar[Dict[str, BaseClass]] = None
- rdfs_comment: Optional[str] = Field(default=None)
- rdfs_label: Optional[str] = Field(default=None)
+ rdfs_comment_clz: ClassVar[Set[str]] = None
+ rdfs_label_clz: ClassVar[Set[str]] = None
+ rdfs_comment: Optional[Set[str]] = Field(default_factory=set)
+ rdfs_label: Optional[Set[str]] = Field(default_factory=set)
instance_iri: str = Field(default='')
# format of the cache for all properties: {property_name: property_object}
_latest_cache: Dict[str, Any] = PrivateAttr(default_factory=dict)
@@ -732,6 +763,24 @@ def __pydantic_init_subclass__(cls, **kwargs):
# register the class to the ontology
cls.rdfs_isDefinedBy._register_class(cls)
+ @classmethod
+ def init_instance_iri(cls) -> str:
+ return init_instance_iri(cls.rdfs_isDefinedBy.namespace_iri, cls.__name__)
+
+ def __init__(self, **data):
+ # handle the case when rdfs_comment and rdfs_label are provided as a non-set value
+ if 'rdfs_comment' in data and not isinstance(data['rdfs_comment'], set):
+ if isinstance(data['rdfs_comment'], list):
+ data['rdfs_comment'] = set(data['rdfs_comment'])
+ else:
+ data['rdfs_comment'] = {data['rdfs_comment']}
+ if 'rdfs_label' in data and not isinstance(data['rdfs_label'], set):
+ if isinstance(data['rdfs_label'], list):
+ data['rdfs_label'] = set(data['rdfs_label'])
+ else:
+ data['rdfs_label'] = {data['rdfs_label']}
+ super().__init__(**data)
+
def model_post_init(self, __context: Any) -> None:
"""
The post init process of the BaseClass.
@@ -745,10 +794,7 @@ def model_post_init(self, __context: Any) -> None:
None: It calls the super().model_post_init(__context) to finish the post init process
"""
if not bool(self.instance_iri):
- self.instance_iri = init_instance_iri(
- self.__class__.rdfs_isDefinedBy.namespace_iri,
- self.__class__.__name__
- )
+ self.instance_iri = self.__class__.init_instance_iri()
# set new instance to the global look up table, so that we can avoid creating the same instance multiple times
self._register_object()
return super().model_post_init(__context)
@@ -764,8 +810,13 @@ def _register_object(self):
if self.__class__.object_lookup is None:
self.__class__.object_lookup = {}
if self.instance_iri in self.__class__.object_lookup:
- raise ValueError(
- f"An object with the same IRI {self.instance_iri} has already been registered.")
+ if type(self.__class__.object_lookup[self.instance_iri]) == type(self):
+ # TODO and not self.__class__.rdfs_isDefinedBy.is_dev_mode()?
+ raise ValueError(
+ f"An object with the same IRI {self.instance_iri} has already been instantiated and registered with the same type {type(self)}.")
+ else:
+ warnings.warn(f"An object with the same IRI {self.instance_iri} has already been instantiated and registered with type {type(self.__class__.object_lookup[self.instance_iri])}. Replacing its regiatration now with type {type(self)}.")
+ del self.__class__.object_lookup[self.instance_iri]
self.__class__.object_lookup[self.instance_iri] = self
@classmethod
@@ -820,6 +871,7 @@ def push_all_instances_to_kg(
for obj in cls.object_lookup.values():
g_to_remove, g_to_add = obj._collect_diff_to_graph(g_to_remove, g_to_add, recursive_depth)
sparql_client.delete_and_insert_graphs(g_to_remove, g_to_add)
+ return g_to_remove, g_to_add
@classmethod
def clear_object_lookup(cls):
@@ -884,13 +936,49 @@ def pull_from_kg(
for iri, props in node_dct.items():
# TODO optimise the time complexity of the following code when the number of instances is large
# check if the rdf:type of the instance matches the calling class or any of its subclasses
- target_clz_rdf_type = list(props[RDF.type.toPython()])[0]
- if target_clz_rdf_type != cls.rdf_type and target_clz_rdf_type not in cls.construct_subclass_dictionary().keys():
+ target_clz_rdf_types = set(props.get(RDF.type.toPython(), [])) # NOTE this supports instance instantiated with multiple rdf:type
+ if not target_clz_rdf_types:
+ raise ValueError(f"The instance {iri} has no rdf:type, retrieved outgoing links and attributes: {props}.")
+ cls_subclasses = set(cls.construct_subclass_dictionary().keys())
+ cls_subclasses.add(cls.rdf_type)
+ intersection = target_clz_rdf_types & cls_subclasses
+ if intersection:
+ if len(intersection) == 1:
+ target_clz_rdf_type = next(iter(intersection))
+ else:
+ # NOTE instead of using the first element of the intersection
+ # we find the deepest subclass as target_clz_rdf_type
+ # so that the created object could inherite all the properties of its parent classes
+ # which prevents the loss of information
+ parent_classes = set()
+ for c in intersection:
+ if c in parent_classes:
+ # skip if it's already a parent class
+ continue
+ for other in intersection:
+ if other != c and issubclass(cls.retrieve_subclass(c), cls.retrieve_subclass(other)):
+ parent_classes.add(other)
+ deepest_subclasses = intersection - parent_classes
+ if len(deepest_subclasses) > 1:
+ # TODO [future] add support for allowing users to specify the target class
+ KnowledgeGraph._remove_iri_from_loading(iri)
+ raise ValueError(
+ f"""The instance {iri} is of type {target_clz_rdf_types}.
+ Amongst the pulling class {cls.__name__} ({cls.rdf_type})
+ and its subclasses ({cls.construct_subclass_dictionary()}),
+ there exist classes that are not in the same branch of the inheritance tree,
+ including {deepest_subclasses},
+ therefore it cannot be instantiated by pulling with class {cls.__name__}.
+ Please consider pulling the instance directly with one of the class in {deepest_subclasses}
+ Alternatively, please check the inheritance tree is correctly defined in Python.""")
+ else:
+ target_clz_rdf_type = next(iter(deepest_subclasses))
+ else:
# if there's any error, remove the iri from the loading status
# otherwise it will block any further pulling of the same object
KnowledgeGraph._remove_iri_from_loading(iri)
raise ValueError(
- f"""The instance {iri} is of type {props[RDF.type.toPython()]},
+ f"""The instance {iri} is of type {target_clz_rdf_types},
it doesn't match the rdf:type of class {cls.__name__} ({cls.rdf_type}),
nor any of its subclasses ({cls.construct_subclass_dictionary()}),
therefore it cannot be instantiated.""")
@@ -930,15 +1018,11 @@ def pull_from_kg(
# handle rdfs:label and rdfs:comment (also fetch of the remote KG)
rdfs_properties_dict = {}
if RDFS.label.toPython() in props:
- if len(props[RDFS.label.toPython()]) > 1:
- raise ValueError(f"The instance {iri} has multiple rdfs:label {props[RDFS.label.toPython()]}.")
- rdfs_properties_dict['rdfs_label'] = list(props[RDFS.label.toPython()])[0]
+ rdfs_properties_dict['rdfs_label'] = set(list(props[RDFS.label.toPython()]))
if RDFS.comment.toPython() in props:
- if len(props[RDFS.comment.toPython()]) > 1:
- raise ValueError(f"The instance {iri} has multiple rdfs:comment {props[RDFS.comment.toPython()]}.")
- rdfs_properties_dict['rdfs_comment'] = list(props[RDFS.comment.toPython()])[0]
+ rdfs_properties_dict['rdfs_comment'] = set(list(props[RDFS.comment.toPython()]))
# instantiate the object
- if inst is not None:
+ if inst is not None and type(inst) is target_clz:
for op_iri, op_dct in ops.items():
if flag_pull:
# below lines pull those object properties that are NOT connected in the remote KG,
@@ -1073,6 +1157,13 @@ def _export_to_owl(cls, g: Graph) -> Graph:
cls_iri = cls.rdf_type
g.add((URIRef(cls_iri), RDF.type, OWL.Class))
g.add((URIRef(cls_iri), RDFS.isDefinedBy, URIRef(cls.rdfs_isDefinedBy.namespace_iri)))
+ # add rdfs_comment_clz and rdfs_label_clz for class
+ if bool(cls.rdfs_comment_clz):
+ for comment in cls.rdfs_comment_clz:
+ g.add((URIRef(cls_iri), RDFS.comment, Literal(comment)))
+ if bool(cls.rdfs_label_clz):
+ for label in cls.rdfs_label_clz:
+ g.add((URIRef(cls_iri), RDFS.label, Literal(label)))
# add super classes
idx = cls.__mro__.index(BaseClass)
for i in range(1, idx):
@@ -1219,9 +1310,9 @@ def _update_according_to_fetch(self, fetched: dict, flag_connect_object: bool, f
# compare rdfs_comment and rdfs_label
for r in ['rdfs_comment', 'rdfs_label']:
- fetched_value = fetched.get(r, None)
- cached_value = self._latest_cache.get(r, None)
- local_value = getattr(self, r)
+ fetched_value = fetched.get(r, set())
+ cached_value = self._latest_cache.get(r, set())
+ local_value = getattr(self, r) if getattr(self, r) is not None else set()
# apply the same logic as above
if fetched_value != cached_value:
if local_value == cached_value:
@@ -1330,6 +1421,8 @@ def _collect_diff_to_graph(self, g_to_remove: Graph, g_to_add: Graph, recursive_
recursive_depth = max(recursive_depth - 1, 0) if recursive_depth > -1 else max(recursive_depth - 1, -1)
p_cache = self._latest_cache.get(f, set())
+ if p_cache is None:
+ p_cache = set() # allows set operations
p_now = getattr(self, f)
if p_now is None:
p_now = set() # allows set operations
@@ -1357,18 +1450,20 @@ def _collect_diff_to_graph(self, g_to_remove: Graph, g_to_add: Graph, recursive_
g_to_remove, g_to_add = d_py._collect_diff_to_graph(g_to_remove, g_to_add, recursive_depth, traversed_iris)
elif f == 'rdfs_comment':
- if self._latest_cache.get(f) != self.rdfs_comment:
- if self._latest_cache.get(f) is not None:
- g_to_remove.add((URIRef(self.instance_iri), RDFS.comment, Literal(self._latest_cache.get(f))))
- if self.rdfs_comment is not None:
- g_to_add.add((URIRef(self.instance_iri), RDFS.comment, Literal(self.rdfs_comment)))
+ rdfs_comment_cache = self._latest_cache.get(f, set())
+ rdfs_comment_now = self.rdfs_comment if self.rdfs_comment is not None else set()
+ for comment in rdfs_comment_cache - rdfs_comment_now:
+ g_to_remove.add((URIRef(self.instance_iri), RDFS.comment, Literal(comment)))
+ for comment in rdfs_comment_now - rdfs_comment_cache:
+ g_to_add.add((URIRef(self.instance_iri), RDFS.comment, Literal(comment)))
elif f == 'rdfs_label':
- if self._latest_cache.get(f) != self.rdfs_label:
- if self._latest_cache.get(f) is not None:
- g_to_remove.add((URIRef(self.instance_iri), RDFS.label, Literal(self._latest_cache.get(f))))
- if self.rdfs_label is not None:
- g_to_add.add((URIRef(self.instance_iri), RDFS.label, Literal(self.rdfs_label)))
+ rdfs_label_cache = self._latest_cache.get(f, set())
+ rdfs_label_now = self.rdfs_label if self.rdfs_label is not None else set()
+ for label in rdfs_label_cache - rdfs_label_now:
+ g_to_remove.add((URIRef(self.instance_iri), RDFS.label, Literal(label)))
+ for label in rdfs_label_now - rdfs_label_cache:
+ g_to_add.add((URIRef(self.instance_iri), RDFS.label, Literal(label)))
if not self._exist_in_kg:
g_to_add.add((URIRef(self.instance_iri), RDF.type, URIRef(self.rdf_type)))
@@ -1403,10 +1498,12 @@ def graph(self, g: Graph = None) -> Graph:
prop = getattr(self, f) if getattr(self, f) is not None else set()
for o in prop:
g.add((URIRef(self.instance_iri), URIRef(tp.predicate_iri), Literal(o)))
- elif f == 'rdfs_comment' and self.rdfs_comment is not None:
- g.add((URIRef(self.instance_iri), RDFS.comment, Literal(self.rdfs_comment)))
- elif f == 'rdfs_label' and self.rdfs_label is not None:
- g.add((URIRef(self.instance_iri), RDFS.label, Literal(self.rdfs_label)))
+ elif f == 'rdfs_comment' and bool(self.rdfs_comment):
+ for comment in self.rdfs_comment:
+ g.add((URIRef(self.instance_iri), RDFS.comment, Literal(comment)))
+ elif f == 'rdfs_label' and bool(self.rdfs_label):
+ for label in self.rdfs_label:
+ g.add((URIRef(self.instance_iri), RDFS.label, Literal(label)))
return g
def triples(self) -> str:
diff --git a/JPS_Ontology/ontology/ontomops/ontomops-ogm.ttl b/JPS_Ontology/ontology/ontomops/ontomops-ogm.ttl
new file mode 100644
index 00000000000..4448210912b
--- /dev/null
+++ b/JPS_Ontology/ontology/ontomops/ontomops-ogm.ttl
@@ -0,0 +1,329 @@
+@prefix dc: .
+@prefix owl: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix xsd: .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:ObjectProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:integer .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:integer .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:integer .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:double .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:double .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain [ a owl:Class ;
+ owl:unionOf ( ) ] ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:double .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:double .
+
+ a owl:ObjectProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range .
+
+ a owl:DatatypeProperty ;
+ rdfs:domain ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:string .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf .
+
+ a owl:Class ;
+ rdfs:isDefinedBy .
+
+ a owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf ,
+ .
+
+ a owl:Ontology ;
+ dc:date "2024-12-07T19:03:28.517090" ;
+ rdfs:comment "An ontology developed for representing Metal-Organic Polyhedra (MOPs). This is object graph mapper (OGM) version." ;
+ owl:versionInfo "1.1-ogm" .
+
diff --git a/MOPTools/MOP_Discovery/assembler/assembler.py b/MOPTools/MOP_Discovery/assembler/assembler.py
index 07a76cf53f5..1b736a387c9 100644
--- a/MOPTools/MOP_Discovery/assembler/assembler.py
+++ b/MOPTools/MOP_Discovery/assembler/assembler.py
@@ -165,8 +165,8 @@ def mop_string_writer(mop_building_1, mop_building_2):
def mop_data_wrapper(mopFormula, altmopFormula, mop_symmetry, mop_weight,mop_charge):
"""First queries each MOP formula string, then wraps the provenanve, molecular weight and charge."""
checked = []
- result1 = querykg(SPARQL_ENDPOINTS['ontomops'], mop_reference(mopFormula, mop_symmetry))
- result2 = querykg(SPARQL_ENDPOINTS['ontomops'], mop_reference(altmopFormula, mop_symmetry))
+ result1 = querykg(SPARQL_ENDPOINTS.ontomops, mop_reference(mopFormula, mop_symmetry))
+ result2 = querykg(SPARQL_ENDPOINTS.ontomops, mop_reference(altmopFormula, mop_symmetry))
if result1:
if 'MOPReference' in result1[0].keys():
provenance = result1[0]['MOPReference']
diff --git a/MOPTools/MOP_Discovery/kg_overview/analytics_operations.py b/MOPTools/MOP_Discovery/kg_overview/analytics_operations.py
index 704618d42f5..f3de0a9e7ac 100644
--- a/MOPTools/MOP_Discovery/kg_overview/analytics_operations.py
+++ b/MOPTools/MOP_Discovery/kg_overview/analytics_operations.py
@@ -16,7 +16,7 @@
def mopsoverview():
"""Collects all MOP IRIs found in the OntoMOP KG"""
- result = querykg(SPARQL_ENDPOINTS['ontomops'], getMOPIRIs())
+ result = querykg(SPARQL_ENDPOINTS.ontomops, getMOPIRIs())
refinedlist = [] # Each IRI is saved in the list of refined IRIs
for item in result:
refined = item['mopIRI']
@@ -42,7 +42,7 @@ def assemblyModelGroups(listofMOPs):
# When quering SPARQL we obtain two lines of information deriving from each GBU/CBU
# We do not know which CBU will be returned first, therefore this is being sorted.
for mopIRI in listofMOPs:
- MOPandGBUs = querykg(SPARQL_ENDPOINTS['ontomops'], mop_GBUs(mopIRI))
+ MOPandGBUs = querykg(SPARQL_ENDPOINTS.ontomops, mop_GBUs(mopIRI))
i = 0
assemblyModel = {}
for MOPandGBU in MOPandGBUs:
@@ -138,7 +138,7 @@ def order(cbuA, cbuB):
gbu['CBU1_Modularity'] = cbuA['Modularity']
gbu['CBU1_Planarity'] = cbuA['Planarity']
gbu['CBU1_Type'] = cbuA['CBUType']
- gbu['CBU1_SpeciesIRI'] = cbuA['speciesIRI']
+ gbu['CBU1_SpeciesIRI'] = cbuA['cbuIRI']
gbu['CBU1_OuterCoordination'] = cbuA['OuterCoordination']
gbu['CBU1_FunctionalGroup'] = cbuA['CBUFunctionalGroup']
gbu['CBU1_Direction'] = cbuA['Direction']
@@ -147,7 +147,7 @@ def order(cbuA, cbuB):
gbu['CBU2_Modularity'] = cbuB['Modularity']
gbu['CBU2_Planarity'] = cbuB['Planarity']
gbu['CBU2_Type'] = cbuB['CBUType']
- gbu['CBU2_SpeciesIRI'] = cbuB['speciesIRI']
+ gbu['CBU2_SpeciesIRI'] = cbuB['cbuIRI']
gbu['CBU2_OuterCoordination'] = cbuB['OuterCoordination']
gbu['CBU2_FunctionalGroup'] = cbuB['CBUFunctionalGroup']
gbu['CBU2_Direction'] = cbuB['Direction']
@@ -157,7 +157,7 @@ def order(cbuA, cbuB):
gbu['CBU1_Modularity'] = cbuB['Modularity']
gbu['CBU1_Planarity'] = cbuB['Planarity']
gbu['CBU1_Type'] = cbuB['CBUType']
- gbu['CBU1_SpeciesIRI'] = cbuB['speciesIRI']
+ gbu['CBU1_SpeciesIRI'] = cbuB['cbuIRI']
gbu['CBU1_OuterCoordination'] = cbuB['OuterCoordination']
gbu['CBU1_FunctionalGroup'] = cbuB['CBUFunctionalGroup']
gbu['CBU1_Direction'] = cbuB['Direction']
@@ -166,7 +166,7 @@ def order(cbuA, cbuB):
gbu['CBU2_Modularity'] = cbuA['Modularity']
gbu['CBU2_Planarity'] = cbuA['Planarity']
gbu['CBU2_Type'] = cbuA['CBUType']
- gbu['CBU2_SpeciesIRI'] = cbuA['speciesIRI']
+ gbu['CBU2_SpeciesIRI'] = cbuA['cbuIRI']
gbu['CBU2_OuterCoordination'] = cbuA['OuterCoordination']
gbu['CBU2_FunctionalGroup'] = cbuA['CBUFunctionalGroup']
gbu['CBU2_Direction'] = cbuA['Direction']
diff --git a/MOPTools/MOP_Discovery/kg_overview/analytics_output.py b/MOPTools/MOP_Discovery/kg_overview/analytics_output.py
index 57537f19a0f..e9c2790e2ec 100644
--- a/MOPTools/MOP_Discovery/kg_overview/analytics_output.py
+++ b/MOPTools/MOP_Discovery/kg_overview/analytics_output.py
@@ -28,30 +28,26 @@ def kgoverview_csv(uniques):
def r1_json(list_R1):
"""Writes JSON file for List R1."""
list_R1_jsonpath = FILE_PATHS['list_R1']
- outpreR1 = json.dumps(list_R1, indent=4)
- jsonoutput = open(list_R1_jsonpath, 'w')
- jsonoutput.write(outpreR1)
+ with open(list_R1_jsonpath, 'w') as f:
+ json.dump(list_R1, f, indent=4)
def preR2_json(list_preR2):
"""Writes JSON file for List preR2"""
list_preR2_jsonpath = FILE_PATHS['list_preR2']
- outpreR2 = json.dumps(list_preR2, indent=4)
- jsonoutput = open(list_preR2_jsonpath, 'w')
- jsonoutput.write(outpreR2)
+ with open(list_preR2_jsonpath, 'w') as f:
+ json.dump(list_preR2, f, indent=4)
def assemblyModel_json(assemblyModel, string):
"""Produces a starting and final json file with library MOPs and their respective properties."""
assemblyModel_jsonpath = FILE_PATHS['mops_am']
- outAssemblyModel = json.dumps(assemblyModel, indent=4)
- jsonoutput = open(assemblyModel_jsonpath+string+'.json', 'w')
- jsonoutput.write(outAssemblyModel)
+ with open(assemblyModel_jsonpath+string+'.json', 'w') as f:
+ json.dump(assemblyModel, f, indent=4)
def assemblyModel_json_temp(assemblyModel, string, frequency):
"""For each assemly model produces a temporary file."""
assemblyModel_jsonpath = FILE_PATHS['temp']
- outAssemblyModel = json.dumps(assemblyModel, indent=4)
- jsonoutput = open(assemblyModel_jsonpath+string+"__"+str(frequency)+'.json', 'w')
- jsonoutput.write(outAssemblyModel)
+ with open(assemblyModel_jsonpath+string+"__"+str(frequency)+'.json', 'w') as f:
+ json.dump(assemblyModel, f, indent=4)
def assemblyModel_json_update(string, frequency):
"""Merges the temporary json file to the original and final json."""
diff --git a/MOPTools/MOP_Discovery/main.py b/MOPTools/MOP_Discovery/main.py
index fd451e0879d..3dba5d2b212 100644
--- a/MOPTools/MOP_Discovery/main.py
+++ b/MOPTools/MOP_Discovery/main.py
@@ -13,7 +13,7 @@ def start():
FOLDER_NAME = 'mops_output'
mops_output()
workflow()
- shutil.rmtree(FOLDER_NAME+'\\temp')
+ shutil.rmtree(os.path.join(FOLDER_NAME, 'temp'))
def mops_output():
FOLDER_NAME = 'mops_output'
@@ -22,14 +22,14 @@ def mops_output():
" or delete it if you want to regenerate the output.\nOtherwise I am not doing anything. Exiting now.")
return
os.mkdir(FOLDER_NAME)
- os.mkdir(FOLDER_NAME+'\\mops_am')
- os.mkdir(FOLDER_NAME+'\\r1_cbus')
- os.mkdir(FOLDER_NAME+'\\r2_cbus')
- os.mkdir(FOLDER_NAME+'\\temp')
- os.mkdir(FOLDER_NAME+'\\mops_r1')
- os.mkdir(FOLDER_NAME+'\\mops_r2')
+ os.mkdir(os.path.join(FOLDER_NAME, 'mops_am'))
+ os.mkdir(os.path.join(FOLDER_NAME, 'r1_cbus'))
+ os.mkdir(os.path.join(FOLDER_NAME, 'r2_cbus'))
+ os.mkdir(os.path.join(FOLDER_NAME, 'temp'))
+ os.mkdir(os.path.join(FOLDER_NAME, 'mops_r1'))
+ os.mkdir(os.path.join(FOLDER_NAME, 'mops_r2'))
if __name__ == '__main__':
print ("Started at ", datetime.datetime.now())
start()
- print ("Finished at ", datetime.datetime.now())
\ No newline at end of file
+ print ("Finished at ", datetime.datetime.now())
diff --git a/MOPTools/MOP_Discovery/manager/file_paths.py b/MOPTools/MOP_Discovery/manager/file_paths.py
index 74690160a04..2e208a9b183 100644
--- a/MOPTools/MOP_Discovery/manager/file_paths.py
+++ b/MOPTools/MOP_Discovery/manager/file_paths.py
@@ -3,19 +3,21 @@
@author: Aleksandar Kondinski
'''
-
+
+import os
+base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FILE_PATHS = {
- 'r1_mops_csv': 'mops_output\\r1_mops.csv',
- 'r2_mops_csv': 'mops_output\\r2_mops.csv',
- 'r1andr2_csv': 'mops_output\\r1_r2.csv',
- 'kg_assembly_csv': 'mops_output\\kg_analysis_1.csv',
- 'mops_am': 'mops_output\\mops_am\\',
- 'r1_cbus': 'mops_output\\r1_cbus\\',
- 'r2_cbus': 'mops_output\\r2_cbus\\',
- 'list_R1': 'mops_output\\list_R1.json',
- 'list_preR2': 'mops_output\\list_preR2.json',
- 'list_R2': 'mops_output\\list_R2.json',
- 'temp': 'mops_output\\temp\\',
- 'mops_r1': 'mops_output\\mops_r1\\',
- 'mops_r2': 'mops_output\\mops_r2\\'
-}
\ No newline at end of file
+ 'r1_mops_csv': os.path.join(base, 'mops_output', 'r1_mops.csv'),
+ 'r2_mops_csv': os.path.join(base, 'mops_output', 'r2_mops.csv'),
+ 'r1andr2_csv': os.path.join(base, 'mops_output', 'r1_r2.csv'),
+ 'kg_assembly_csv': os.path.join(base, 'mops_output', 'kg_analysis_1.csv'),
+ 'mops_am': os.path.join(base, 'mops_output', 'mops_am', ''),
+ 'r1_cbus': os.path.join(base, 'mops_output', 'r1_cbus', ''),
+ 'r2_cbus': os.path.join(base, 'mops_output', 'r2_cbus', ''),
+ 'list_R1': os.path.join(base, 'mops_output', 'list_R1.json'),
+ 'list_preR2': os.path.join(base, 'mops_output', 'list_preR2.json'),
+ 'list_R2': os.path.join(base, 'mops_output', 'list_R2.json'),
+ 'temp': os.path.join(base, 'mops_output', 'temp', ''),
+ 'mops_r1': os.path.join(base, 'mops_output', 'mops_r1', ''),
+ 'mops_r2': os.path.join(base, 'mops_output', 'mops_r2', '')
+}
diff --git a/MOPTools/MOP_Discovery/query_operations/config.json b/MOPTools/MOP_Discovery/query_operations/config.json
new file mode 100644
index 00000000000..c1f9d487965
--- /dev/null
+++ b/MOPTools/MOP_Discovery/query_operations/config.json
@@ -0,0 +1,4 @@
+{
+ "ontospecies": "http://localhost:48082/blazegraph/namespace/ontomops/sparql",
+ "ontomops": "http://localhost:48082/blazegraph/namespace/ontomops/sparql"
+}
\ No newline at end of file
diff --git a/MOPTools/MOP_Discovery/query_operations/queryTemplates.py b/MOPTools/MOP_Discovery/query_operations/queryTemplates.py
index d6740b8815e..959fb17ec7d 100644
--- a/MOPTools/MOP_Discovery/query_operations/queryTemplates.py
+++ b/MOPTools/MOP_Discovery/query_operations/queryTemplates.py
@@ -8,26 +8,26 @@ def getMOPIRIs():
"""This function collects all MOPs from the KG."""
queryStr = """
- PREFIX OntoMOPs:
+ PREFIX OntoMOPs:
PREFIX OntoSpecies:
PREFIX Measure:
PREFIX rdf:
- SELECT ?mopIRI ?MOPFormula ?speciesIRI ?CBUFormula ?NumberValue ?Planarity ?Modularity ?Symbol
- WHERE
- {
- ?mopIRI OntoMOPs:hasMOPFormula ?MOPFormula .
- ?mopIRI OntoMOPs:hasAssemblyModel ?AssemblyModel .
- ?AssemblyModel OntoMOPs:hasPolyhedralShape ?PolhedralShape .
- ?PolhedralShape OntoMOPs:hasSymbol ?Symbol .
- ?AssemblyModel OntoMOPs:hasGenericBuildingUnitNumber ?GBUNumber .
- ?GBUNumber OntoMOPs:isNumberOf ?GBU .
- ?GBU OntoMOPs:hasPlanarity ?Planarity .
- ?GBU OntoMOPs:hasModularity ?Modularity .
- ?GBUNumber OntoSpecies:value ?NumberValue .
- ?mopIRI OntoMOPs:hasChemicalBuildingUnit ?CBU .
- ?CBU OntoMOPs:isFunctioningAs ?GBU .
- ?CBU OntoMOPs:hasCBUFormula ?CBUFormula .
- ?CBU OntoSpecies:hasUniqueSpecies ?speciesIRI .
+ SELECT ?mopIRI ?MOPFormula ?CBUFormula ?NumberValue ?Planarity ?Modularity ?Symbol ?cbuIRI
+ WHERE {
+ ?mopIRI OntoMOPs:hasMOPFormula ?MOPFormula .
+ ?mopIRI OntoMOPs:hasProvenance ?Provenance .
+ ?mopIRI OntoMOPs:hasAssemblyModel ?AssemblyModel .
+ ?AssemblyModel OntoMOPs:hasPolyhedralShape ?PolhedralShape .
+ ?PolhedralShape OntoMOPs:hasSymbol ?Symbol .
+ ?AssemblyModel OntoMOPs:hasGenericBuildingUnitNumber ?GBUNumber .
+ ?GBUNumber OntoMOPs:isNumberOf ?GBU .
+ ?GBU OntoMOPs:hasGBUType ?GBUType .
+ ?GBUType OntoMOPs:hasPlanarity ?Planarity .
+ ?GBUType OntoMOPs:hasModularity ?Modularity .
+ ?GBUNumber OntoMOPs:hasUnitNumberValue ?NumberValue .
+ ?mopIRI OntoMOPs:hasChemicalBuildingUnit ?cbuIRI .
+ ?cbuIRI OntoMOPs:isFunctioningAs ?GBU .
+ ?cbuIRI OntoMOPs:hasCBUFormula ?CBUFormula .
}"""
return queryStr
@@ -35,35 +35,34 @@ def mop_GBUs(mopIRI):
"""Queries and collects MOP data relating to the GBUs/CBUs.
As every MOP has two GBUs, returns back information on both. """
queryStr = """
- PREFIX OntoMOPs:
+ PREFIX OntoMOPs:
PREFIX OntoSpecies:
PREFIX Measure: