Skip to content

Commit

Permalink
Merge pull request #198 from duc89/onto_sqlite3_error_catch
Browse files Browse the repository at this point in the history
[OntologyManager] Use in-memory SQL backend by default
  • Loading branch information
Tigul authored Sep 9, 2024
2 parents 19cd6b2 + 49d1099 commit 94ba08a
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 28 deletions.
77 changes: 55 additions & 22 deletions src/pycram/ontology/ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import itertools
import logging
import os.path
import sqlite3

from pathlib import Path
from typing import Callable, Dict, List, Optional, Type, Tuple, Union
Expand All @@ -22,7 +23,8 @@
from ..designator import DesignatorDescription, ObjectDesignatorDescription

from ..ontology.ontology_common import (OntologyConceptHolderStore, OntologyConceptHolder,
ONTOLOGY_SQL_BACKEND_FILE_EXTENSION)
ONTOLOGY_SQL_BACKEND_FILE_EXTENSION,
ONTOLOGY_SQL_IN_MEMORY_BACKEND)

SOMA_HOME_ONTOLOGY_IRI = "http://www.ease-crc.org/ont/SOMA-HOME.owl"
SOMA_ONTOLOGY_IRI = "http://www.ease-crc.org/ont/SOMA.owl"
Expand All @@ -35,12 +37,14 @@ class OntologyManager(object, metaclass=Singleton):
Singleton class as the adapter accessing data of an OWL ontology, largely based on owlready2.
"""

def __init__(self, main_ontology_iri: Optional[str] = None, ontology_search_path: Optional[str] = None,
def __init__(self, main_ontology_iri: Optional[str] = None, main_sql_backend_filename: Optional[str] = None,
ontology_search_path: Optional[str] = None,
use_global_default_world: bool = True):
"""
Create the singleton object of OntologyManager class
:param main_ontology_iri: Ontology IRI (Internationalized Resource Identifier), either a URL to a remote OWL file or the full name path of a local one
:param main_sql_backend_filename: a full file path (no need to already exist) being used as SQL backend for the ontology world. If None, in-memory is used instead
:param ontology_search_path: directory path from which a possibly existing ontology is searched. This is appended to `owlready2.onto_path`, a global variable containing a list of directories for searching local copies of ontologies (similarly to python `sys.path` for modules/packages). If not specified, the path is "$HOME/ontologies"
:param use_global_default_world: whether or not using the owlready2-provided global default persistent world
"""
Expand Down Expand Up @@ -74,6 +78,9 @@ def __init__(self, main_ontology_iri: Optional[str] = None, ontology_search_path
#: Namespace of the main ontology
self.main_ontology_namespace: Optional[Namespace] = None

#: SQL backend for :attr:`main_ontology_world`, being either "memory" or a full file path (no need to already exist)
self.main_ontology_sql_backend = main_sql_backend_filename if main_sql_backend_filename else ONTOLOGY_SQL_IN_MEMORY_BACKEND

# Create the main ontology world holding triples
self.create_main_ontology_world(use_global_default_world=use_global_default_world)

Expand Down Expand Up @@ -149,46 +156,70 @@ def get_main_ontology_dir(self) -> Optional[str]:
return os.path.dirname(self.main_ontology_iri) if os.path.isabs(
self.main_ontology_iri) else self.get_default_ontology_search_path()

def is_main_ontology_sql_backend_in_memory(self) -> bool:
"""
Whether the main ontology's SQL backend is in-memory
:return: true if the main ontology's SQL backend is in-memory
"""
return self.main_ontology_sql_backend == ONTOLOGY_SQL_IN_MEMORY_BACKEND

def create_main_ontology_world(self, use_global_default_world: bool = True) -> None:
"""
Create the main ontology world, either reusing the owlready2-provided global default ontology world or create a new one
A backend sqlite3 file of same name with `main_ontology` is also created at the same folder with :attr:`main_ontology_iri`
(if it is a local absolute path). The file is automatically registered as cache for the main ontology world.
:param use_global_default_world: whether or not using the owlready2-provided global default persistent world
:param sql_backend_filename: a full file path (no need to already exist) being used as SQL backend for the ontology world. If None, memory is used instead
"""
self.main_ontology_world = self.create_ontology_world(
sql_backend_filename=os.path.join(self.get_main_ontology_dir(),
f"{Path(self.main_ontology_iri).stem}{ONTOLOGY_SQL_BACKEND_FILE_EXTENSION}"),
sql_backend_filename=self.main_ontology_sql_backend,
use_global_default_world=use_global_default_world)

@staticmethod
def create_ontology_world(use_global_default_world: bool = False,
sql_backend_filename: Optional[str] = None) -> OntologyWorld:
"""
Either reuse the owlready2-provided global default ontology world or create a new one
Either reuse the owlready2-provided global default ontology world or create a new one.
:param use_global_default_world: whether or not using the owlready2-provided global default persistent world
:param sql_backend_filename: a full file path (no need to already exist) being used as SQL backend for the ontology world. If None, memory is used instead
:param sql_backend_filename: an absolute file path (no need to already exist) being used as SQL backend for the ontology world. If it is None or non-absolute path, in-memory is used instead
:return: owlready2-provided global default ontology world or a newly created ontology world
"""
world = default_world
sql_backend_path_valid = sql_backend_filename and os.path.isabs(sql_backend_filename)
sql_backend_name = sql_backend_filename if sql_backend_path_valid else "memory"
if use_global_default_world:
# Reuse default world
if sql_backend_path_valid:
world.set_backend(filename=sql_backend_filename, exclusive=False, enable_thread_parallelism=True)
else:
world.set_backend(exclusive=False, enable_thread_parallelism=True)
rospy.loginfo(f"Using global default ontology world with SQL backend: {sql_backend_name}")
else:
# Create a new world with parallelized file parsing enabled
if sql_backend_path_valid:
world = OntologyWorld(filename=sql_backend_filename, exclusive=False, enable_thread_parallelism=True)
sql_backend_path_absolute = (sql_backend_filename and os.path.isabs(sql_backend_filename))
if sql_backend_filename and (sql_backend_filename != ONTOLOGY_SQL_IN_MEMORY_BACKEND):
if not sql_backend_path_absolute:
rospy.logerr(f"For ontology world accessing, either f{ONTOLOGY_SQL_IN_MEMORY_BACKEND}"
f"or an absolute path to its SQL file backend is expected: {sql_backend_filename}")
return default_world
elif not sql_backend_filename.endswith(ONTOLOGY_SQL_BACKEND_FILE_EXTENSION):
rospy.logerr(
f"Ontology world SQL backend file path, {sql_backend_filename},"
f"is expected to be of extension {ONTOLOGY_SQL_BACKEND_FILE_EXTENSION}!")
return default_world

sql_backend_path_valid = sql_backend_path_absolute
sql_backend_name = sql_backend_filename if sql_backend_path_valid else ONTOLOGY_SQL_IN_MEMORY_BACKEND
try:
if use_global_default_world:
# Reuse default world
if sql_backend_path_valid:
world.set_backend(filename=sql_backend_filename, exclusive=False, enable_thread_parallelism=True)
else:
world.set_backend(exclusive=False, enable_thread_parallelism=True)
rospy.loginfo(f"Using global default ontology world with SQL backend: {sql_backend_name}")
else:
world = OntologyWorld(exclusive=False, enable_thread_parallelism=True)
rospy.loginfo(f"Created a new ontology world with SQL backend: {sql_backend_name}")
# Create a new world with parallelized file parsing enabled
if sql_backend_path_valid:
world = OntologyWorld(filename=sql_backend_filename, exclusive=False, enable_thread_parallelism=True)
else:
world = OntologyWorld(exclusive=False, enable_thread_parallelism=True)
rospy.loginfo(f"Created a new ontology world with SQL backend: {sql_backend_name}")
except sqlite3.Error as e:
rospy.logerr(f"Failed accessing the SQL backend of ontology world: {sql_backend_name}",
e.sqlite_errorcode, e.sqlite_errorname)
return world

def create_main_ontology(self) -> bool:
Expand Down Expand Up @@ -223,7 +254,9 @@ def load_ontology(self, ontology_iri: str) -> Optional[Tuple[Ontology, Namespace
# If `ontology_iri` is a local path
if is_local_ontology_iri and not Path(ontology_iri).exists():
# -> Create an empty ontology file if not existing
with open(ontology_iri, 'w'):
ontology_path = ontology_iri if os.path.isabs(ontology_iri) else (
os.path.join(self.get_main_ontology_dir(), ontology_iri))
with open(ontology_path, 'w'):
pass

# Load ontology from `ontology_iri`
Expand Down
1 change: 1 addition & 0 deletions src/pycram/ontology/ontology_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from owlready2 import issubclass, Thing

ONTOLOGY_SQL_BACKEND_FILE_EXTENSION = ".sqlite3"
ONTOLOGY_SQL_IN_MEMORY_BACKEND = "memory"
ONTOLOGY_OWL_FILE_EXTENSION = ".owl"


Expand Down
16 changes: 10 additions & 6 deletions test/test_ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@

from pycram.ontology.ontology import OntologyManager, SOMA_HOME_ONTOLOGY_IRI, SOMA_ONTOLOGY_IRI
from pycram.ontology.ontology_common import (OntologyConceptHolderStore, OntologyConceptHolder,
ONTOLOGY_SQL_BACKEND_FILE_EXTENSION, ONTOLOGY_OWL_FILE_EXTENSION)
ONTOLOGY_SQL_BACKEND_FILE_EXTENSION, ONTOLOGY_OWL_FILE_EXTENSION,
ONTOLOGY_SQL_IN_MEMORY_BACKEND)

DEFAULT_LOCAL_ONTOLOGY_IRI = "default.owl"
class TestOntologyManager(unittest.TestCase):
Expand All @@ -40,7 +41,9 @@ class TestOntologyManager(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Try loading from remote `SOMA_ONTOLOGY_IRI`, which will fail given no internet access
cls.ontology_manager = OntologyManager(SOMA_ONTOLOGY_IRI)
cls.ontology_manager = OntologyManager(main_ontology_iri=SOMA_ONTOLOGY_IRI,
main_sql_backend_filename=os.path.join(Path.home(),
f"{Path(SOMA_ONTOLOGY_IRI).stem}{ONTOLOGY_SQL_BACKEND_FILE_EXTENSION}"))
if cls.ontology_manager.initialized():
cls.soma = cls.ontology_manager.soma
cls.dul = cls.ontology_manager.dul
Expand All @@ -49,6 +52,7 @@ def setUpClass(cls):
cls.soma = None
cls.dul = None
cls.ontology_manager.main_ontology_iri = DEFAULT_LOCAL_ONTOLOGY_IRI
cls.ontology_manager.main_ontology_sql_backend = ONTOLOGY_SQL_IN_MEMORY_BACKEND
cls.ontology_manager.create_main_ontology_world()
cls.ontology_manager.create_main_ontology()
cls.main_ontology = cls.ontology_manager.main_ontology
Expand All @@ -57,9 +61,8 @@ def setUpClass(cls):
def tearDownClass(cls):
save_dir = cls.ontology_manager.get_main_ontology_dir()
owl_filepath = f"{save_dir}/{Path(cls.ontology_manager.main_ontology_iri).stem}{ONTOLOGY_OWL_FILE_EXTENSION}"
sql_filepath = f"{save_dir}/{Path(owl_filepath).stem}{ONTOLOGY_SQL_BACKEND_FILE_EXTENSION}"
os.remove(owl_filepath)
cls.remove_sql_file(sql_filepath)
cls.remove_sql_file(cls.ontology_manager.main_ontology_sql_backend)

@classmethod
def remove_sql_file(cls, sql_filepath: str):
Expand Down Expand Up @@ -299,10 +302,11 @@ def coresidents(a: reasoning_ontology.Entity, b: reasoning_ontology.Entity) -> b
def test_ontology_save(self):
save_dir = self.ontology_manager.get_main_ontology_dir()
owl_filepath = f"{save_dir}/{Path(self.ontology_manager.main_ontology_iri).stem}{ONTOLOGY_OWL_FILE_EXTENSION}"
sql_filepath = f"{save_dir}/{Path(owl_filepath).stem}{ONTOLOGY_SQL_BACKEND_FILE_EXTENSION}"
self.assertTrue(self.ontology_manager.save(owl_filepath))
self.assertTrue(Path(owl_filepath).is_file())
self.assertTrue(Path(sql_filepath).is_file())
sql_backend = self.ontology_manager.main_ontology_sql_backend
if sql_backend != ONTOLOGY_SQL_IN_MEMORY_BACKEND:
self.assertTrue(Path(sql_backend).is_file())

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

0 comments on commit 94ba08a

Please sign in to comment.