diff --git a/common/assertions/test_apartment.yaml b/common/assertions/test_apartment.yaml new file mode 100644 index 0000000..003d586 --- /dev/null +++ b/common/assertions/test_apartment.yaml @@ -0,0 +1,23 @@ +class_assertions: + Male: [Alex, Sushant] + Female: [Argentina] + Room: [RopodLab, atWorkLab] + Chair: [AlexChair, ArgentinaChair, SushantChair] + HighTable: [RopodWorkDesk, atWorkDesk] +property_assertions: + defaultLocation: + AlexChair: RopodLab + ArgentinaChair: RopodLab + SushantChair: RopodLab + RopodWorkDesk: RopodLab + atWorkDesk: atWorkLab + locatedAt: + AlexChair: RopodLab + ArgentinaChair: RopodLab + SushantChair: atWorkLab + RopodWorkDesk: RopodLab + atWorkDesk: atWorkLab + nextTo: + AlexChair: RopodWorkDesk + ArgentinaChair: RopodWorkDesk + SushantChair: atWorkDesk diff --git a/common/mas_knowledge_utils/abox_yaml_loader.py b/common/mas_knowledge_utils/abox_yaml_loader.py new file mode 100644 index 0000000..931a5bd --- /dev/null +++ b/common/mas_knowledge_utils/abox_yaml_loader.py @@ -0,0 +1,133 @@ +import yaml +import argparse +import os +from ontology_query_interface import OntologyQueryInterface + +class ABoxYAMLLoader: + '''Updates an ontology with class and property assertions + that are specified in a yaml file. + + Constructor arguments: + @param ontology_file -- full URL of an ontology file (of the form file://) + @param ontology_class_prefix -- class prefix of the items in the given ontology + @param assertions_file -- absolute-path to the yaml file containing assertions + + @author Sushant Chavan + @contact sushant.chavan@smail.inf.h-brs.de + + ''' + def __init__(self, ontology_file, ontology_class_prefix, assertions_file): + self.ontology_if = OntologyQueryInterface(ontology_file=ontology_file, + class_prefix=ontology_class_prefix) + self.class_assertions = None + self.property_assertions = None + + self.__load_assertions(assertions_file) + + def update_ontology(self, export_path): + '''Inserts the class and property assertions and updates/exports the ontology. + + Keyword arguments: + export_path -- string representing the filepath (of the form file://) + if the ontology has to be exported to a new file. + If it is 'None', the loaded ontology will be updated with the assertions + + ''' + self.process_class_assertions() + self.process_property_assertions() + self.export_ontology(export_path) + + def process_class_assertions(self): + '''Inserts the class assertions into the loaded ontology. + + ''' + if self.class_assertions is None: + return + + for class_name in self.class_assertions.keys(): + instance_names = self.class_assertions[class_name] + for instance_name in instance_names: + self.ontology_if.insert_class_assertion(class_name, instance_name) + #print("Inserting class assertion: {0}, {1}".format(class_name, instance_name)) + + def process_property_assertions(self): + '''Inserts the property assertions into the loaded ontology. + + ''' + if self.property_assertions is None: + return + + for property_name in self.property_assertions.keys(): + for subj, obj in self.property_assertions[property_name].items(): + self.ontology_if.insert_property_assertion(property_name, (subj, obj)) + #print("Inserting property assertion: {0}, {1}, {2}".format(subj, property_name, obj)) + + def export_ontology(self, export_path): + '''Updates/exports the ontology. + + Keyword arguments: + export_path -- string representing the filepath (of the form file://) + if the ontology has to be exported to a new file. + If it is 'None', the loaded ontology will be updated with the assertions + + ''' + if export_path is None: + # Overwrite the existing ontology file with the updated ontology + print("No export filepath specified. Updating the loaded ontology.") + self.ontology_if.update() + else: + # Save the updated ontology at the given file location + print("Exporting the ontology to", export_path) + self.ontology_if.export(export_path) + + def __load_assertions(self, assertions_file): + '''Loads the class and property assertions as two separate + dictionaries from the yaml file + + Keyword arguments: + assertions_file -- absolute-path to the yaml file containing assertions + + ''' + assertions = None + with open(assertions_file, 'r') as f: + assertions = yaml.load(f, Loader=yaml.FullLoader) + + if assertions is None: + raise Exception("Could not load the assertion file!") + else: + self.class_assertions = assertions['class_assertions'] + self.property_assertions = assertions['property_assertions'] + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('asserts', type=str, + help="Name of the yaml file containing the assertions \ + (eg.: apartment_asserts)") + parser.add_argument('-o', '--ontology', type=str, action='store', + help="Filename of the ontology (default: apartment_go_2019)", + default='apartment_go_2019') + parser.add_argument('-c', '--class-prefix', type=str, action='store', + help="class prefix (or namespace) of the items in the \ + given ontology (default: apartment)", + default='apartment') + parser.add_argument('-e', '--export-file', type=str, action='store', + help="Filename for exporting the updated ontology \ + (eg.: apartment_go_2019_updated). If not \ + specified, the loaded ontology will be updated.", + default=None) + + args = parser.parse_args() + + script_dir = os.path.abspath(os.path.dirname(__file__)) + ontology_dir = os.path.join(os.path.dirname(script_dir), "ontology") + assertions_dir = os.path.join(os.path.dirname(script_dir), "assertions") + + ontology_file_path = "file://" + os.path.join(ontology_dir, args.ontology + ".owl") + assertions_file_path = os.path.join(assertions_dir, args.asserts + ".yaml") + + export_file_path = None + if args.export_file is not None: + export_file_path = "file://" + os.path.join(ontology_dir, args.export_file + ".owl") + + loader = ABoxYAMLLoader(ontology_file_path, args.class_prefix, assertions_file_path) + loader.update_ontology(export_file_path) diff --git a/common/mas_knowledge_utils/ontology_query_interface.py b/common/mas_knowledge_utils/ontology_query_interface.py index 9987ea6..eb9862e 100644 --- a/common/mas_knowledge_utils/ontology_query_interface.py +++ b/common/mas_knowledge_utils/ontology_query_interface.py @@ -28,6 +28,8 @@ def __init__(self, ontology_file, class_prefix): self.knowledge_graph.load(ontology_file) self.class_prefix = class_prefix self.ontology_url = ontology_file[0:ontology_file.rfind('/')] + self.ontology_file = ontology_file + self.__instance_names = None def get_classes(self): '''Returns a list with the names of all classes in the ontology. @@ -43,6 +45,19 @@ def get_object_properties(self): if triple[1] == URIRefConstants.RDF_TYPE and \ triple[2] == URIRefConstants.OWL_OBJECT_PROPERTY] + def get_instances(self): + ''' Returns a list of all the instances defined in the ontology + + ''' + if self.__instance_names is not None: + return self.__instance_names + + self.__instance_names = [] + class_list = self.get_classes() + for c in class_list: + self.__instance_names.extend(self.get_instances_of(c)) + return list(set(self.__instance_names)) + def is_instance_of(self, obj_name, class_name): '''Checks whether 'obj_name' is an instance of 'class_name'. @@ -189,6 +204,125 @@ def get_property_domain_range(self, prop): break return (prop_domain, prop_range) + def is_class(self, class_name): + '''Checks whether 'class_name' is defined as a class in the ontology. + + Keyword arguments: + class_name -- string representing the name of the class + + ''' + return class_name in self.get_classes() + + def is_instance(self, instance_name): + '''Checks whether 'instance_name' is defined as a class instance in the ontology. + + Keyword arguments: + instance_name -- string representing the name of the class instance + + ''' + return instance_name in self.get_instances() + + def is_property(self, property_name): + '''Checks whether 'property_name' is defined as a property in the ontology. + + Keyword arguments: + property_name -- string representing the name of the property + + ''' + return property_name in self.get_object_properties() + + def insert_class_assertion(self, class_name, instance_name): + ''' Adds a new instance of a class to the ontology + + Keyword arguments: + class_name -- string representing the name of the class + instance_name -- string representing the name of the instance + + ''' + if self.is_class(class_name): + ns = rdflib.Namespace("http://{0}#".format(self.class_prefix)) + self.knowledge_graph.add((rdflib.URIRef(self.__get_entity_url(instance_name)), + rdflib.RDF.type, + rdflib.URIRef(ns[class_name]))) + # Reset instance names list to ensure that the newly added + # instance is included in the next query to the instance_list + self.__instance_names = None + else: + raise ValueError("The \"{0}\" class does not exist in the ontology!".format(class_name)) + + def insert_property_assertion(self, property_name, instance): + ''' Adds a new predicate between a subject and an object to the ontology + + Keyword arguments: + property_name -- string representing the name of the predicate + instance -- tuple(string, string) representing the subject and the object respectively + + ''' + if not self.is_instance(instance[0]): + raise ValueError("The subject \"{0}\" does not exist in the ontology as an instance of a class!".format(instance[0])) + elif not self.is_instance(instance[1]): + raise ValueError("The object \"{0}\" does not exist in the ontology as an instance of a class!".format(instance[1])) + elif not self.is_property(property_name): + raise ValueError("The property \"{0}\" is not defined in the ontology!".format(property_name)) + else: + ns = rdflib.Namespace("http://{0}#".format(self.class_prefix)) + self.knowledge_graph.add((rdflib.URIRef(self.__get_entity_url(instance[0])), + rdflib.URIRef(ns[property_name]), + rdflib.URIRef(self.__get_entity_url(instance[1])))) + + def remove_class_assertion(self, class_name, instance_name): + ''' Removes an existing instance of a class from the ontology + + Keyword arguments: + class_name -- string representing the name of the class + instance_name -- string representing the name of the instance + + ''' + if not self.is_class(class_name): + raise ValueError("The \"{0}\" class does not exist in the ontology!".format(class_name)) + elif not self.is_instance(instance_name): + raise ValueError("The \"{0}\" instance does not exist in the ontology!".format(instance_name)) + else: + ns = rdflib.Namespace("http://{0}#".format(self.class_prefix)) + self.knowledge_graph.remove((rdflib.URIRef(self.__get_entity_url(instance_name)), + rdflib.RDF.type, + rdflib.URIRef(ns[class_name]))) + + def remove_property_assertion(self, property_name, instance): + ''' Removes an existing predicate between a subject and an object from the ontology + + Keyword arguments: + property_name -- string representing the name of the predicate + instance -- tuple(string, string) representing the subject and the object respectively + + ''' + if not self.is_instance(instance[0]): + raise ValueError("The subject \"{0}\" does not exist in the ontology as an instance of a class!".format(instance[0])) + elif not self.is_instance(instance[1]): + raise ValueError("The object \"{0}\" does not exist in the ontology as an instance of a class!".format(instance[1])) + elif not self.is_property(property_name): + raise ValueError("The property \"{0}\" is not defined in the ontology!".format(property_name)) + else: + ns = rdflib.Namespace("http://{0}#".format(self.class_prefix)) + self.knowledge_graph.remove((rdflib.URIRef(self.__get_entity_url(instance[0])), + rdflib.URIRef(ns[property_name]), + rdflib.URIRef(self.__get_entity_url(instance[1])))) + + def export(self, ontology_file, format='xml'): + '''Exports the ontology as an xml document. + + Keyword arguments: + @param ontology_file -- full URL of the ontology file (of the form file://) + + ''' + self.knowledge_graph.serialize(ontology_file, format=format) + + def update(self, format='xml'): + '''Overwrites the loaded ontology file with the current version of the ontology. + + ''' + self.export(self.ontology_file, format=format) + def __format_class_name(self, class_name): '''Returns a string of the format "self.class_prefix:class_name". diff --git a/common/ontology/apartment_go_2019.owl b/common/ontology/apartment_go_2019.owl index 8368a1e..aa0a51b 100644 --- a/common/ontology/apartment_go_2019.owl +++ b/common/ontology/apartment_go_2019.owl @@ -99,10 +99,6 @@ - - - - @@ -160,7 +156,7 @@ - + @@ -188,7 +184,7 @@ - +