Skip to content

Commit

Permalink
Merge pull request cram2#188 from duc89/onto_iri_error_catch
Browse files Browse the repository at this point in the history
Catch remote ontology loading error by owlready2
  • Loading branch information
tomsch420 authored Aug 21, 2024
2 parents fdb41ef + bdbde8f commit f39de3c
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 45 deletions.
81 changes: 56 additions & 25 deletions src/pycram/ontology/ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
from pathlib import Path
from typing import Callable, Dict, List, Optional, Type, Tuple, Union

import owlready2
import rospy

from owlready2 import (Namespace, Ontology, World as OntologyWorld, Thing, EntityClass, Imp,
Property, ObjectProperty, OwlReadyError, types,
onto_path, default_world, get_namespace, get_ontology, destroy_entity,
sync_reasoner_pellet, sync_reasoner_hermit)
sync_reasoner_pellet, sync_reasoner_hermit,
OwlReadyOntologyParsingError)
from owlready2.class_construct import GeneralClassAxiom

from ..datastructures.enums import ObjectType
Expand Down Expand Up @@ -74,22 +74,11 @@ def __init__(self, main_ontology_iri: Optional[str] = None, ontology_search_path
#: Namespace of the main ontology
self.main_ontology_namespace: Optional[Namespace] = None

# Create the main ontology world holding triples, of which a sqlite3 file path, of same name with `main_ontology` &
# at the same folder with `main_ontology_iri` (if it is a local abosulte path), is automatically registered as cache of the world
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}"),
use_global_default_world=use_global_default_world)
# Create the main ontology world holding triples
self.create_main_ontology_world(use_global_default_world=use_global_default_world)

# Load ontologies from `main_ontology_iri` to `main_ontology_world`
# If `main_ontology_iri` is a remote URL, Owlready2 first searches for a local copy of the OWL file (from `onto_path`),
# if not found, tries to download it from the Internet.
ontology_info = self.load_ontology(self.main_ontology_iri)
if ontology_info:
self.main_ontology, self.main_ontology_namespace = ontology_info
if self.main_ontology and self.main_ontology.loaded:
self.soma = self.ontologies.get(SOMA_ONTOLOGY_NAMESPACE)
self.dul = self.ontologies.get(DUL_ONTOLOGY_NAMESPACE)
# Create the main ontology & its namespace, fetching :attr:`soma`, :attr:`dul` if loading from SOMA ontology
self.create_main_ontology()

@staticmethod
def print_ontology_class(ontology_class: Type[Thing]):
Expand Down Expand Up @@ -160,6 +149,19 @@ 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 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
"""
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}"),
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:
Expand Down Expand Up @@ -189,6 +191,22 @@ def create_ontology_world(use_global_default_world: bool = False,
rospy.loginfo(f"Created a new ontology world with SQL backend: {sql_backend_name}")
return world

def create_main_ontology(self) -> bool:
"""
Load ontologies from :attr:`main_ontology_iri` to :attr:`main_ontology_world`
If `main_ontology_iri` is a remote URL, Owlready2 first searches for a local copy of the OWL file (from `onto_path`),
if not found, tries to download it from the Internet.
:return: True if loading succeeds
"""
ontology_info = self.load_ontology(self.main_ontology_iri)
if ontology_info:
self.main_ontology, self.main_ontology_namespace = ontology_info
if self.main_ontology and self.main_ontology.loaded:
self.soma = self.ontologies.get(SOMA_ONTOLOGY_NAMESPACE)
self.dul = self.ontologies.get(DUL_ONTOLOGY_NAMESPACE)
return ontology_info is not None

def load_ontology(self, ontology_iri: str) -> Optional[Tuple[Ontology, Namespace]]:
"""
Load an ontology from an IRI
Expand All @@ -200,19 +218,32 @@ def load_ontology(self, ontology_iri: str) -> Optional[Tuple[Ontology, Namespace
rospy.logerr("Ontology IRI is empty")
return None

# If `ontology_iri` is a local path -> create an empty ontology file if not existing
if not (ontology_iri.startswith("http:") or ontology_iri.startswith("https:")) \
and not Path(ontology_iri).exists():
is_local_ontology_iri = not (ontology_iri.startswith("http:") or ontology_iri.startswith("https:"))

# 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'):
pass

# Load ontology from `ontology_iri`
if self.main_ontology_world:
ontology = self.main_ontology_world.get_ontology(ontology_iri).load(reload_if_newer=True)
else:
ontology = get_ontology(ontology_iri).load(reload_if_newer=True)
ontology = None
try:
if self.main_ontology_world:
ontology = self.main_ontology_world.get_ontology(ontology_iri).load(reload_if_newer=True)
else:
ontology = get_ontology(ontology_iri).load(reload_if_newer=True)
except OwlReadyOntologyParsingError as error:
rospy.logwarn(error)
if is_local_ontology_iri:
rospy.logerr(f"Main ontology failed being loaded from {ontology_iri}")
else:
rospy.logwarn(f"Main ontology failed being downloaded from the remote {ontology_iri}")
return None

# Browse loaded `ontology`, fetching sub-ontologies
ontology_namespace = get_namespace(ontology_iri)
if ontology.loaded:
if ontology and ontology.loaded:
rospy.loginfo(
f'Ontology [{ontology.base_iri}]\'s name: {ontology.name} has been loaded')
rospy.loginfo(f'- main namespace: {ontology_namespace.name}')
Expand Down
15 changes: 0 additions & 15 deletions test/test_action_designator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@ class TestActionDesignatorGrounding(BulletWorldTestCase):

def test_move_torso(self):
description = action_designator.MoveTorsoAction([0.3])
# SOMA ontology seems not provide a corresponding concept yet for MoveTorso
#self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().position, 0.3)
with simulated_robot:
description.resolve().perform()
self.assertEqual(self.world.robot.get_joint_position(RobotDescription.current_robot_description.torso_joint), 0.3)

def test_set_gripper(self):
description = action_designator.SetGripperAction([Arms.LEFT], [GripperState.OPEN, GripperState.CLOSE])
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().gripper, Arms.LEFT)
self.assertEqual(description.ground().motion, GripperState.OPEN)
self.assertEqual(len(list(iter(description))), 2)
Expand All @@ -38,21 +35,18 @@ def test_set_gripper(self):
def test_release(self):
object_description = object_designator.ObjectDesignatorDescription(names=["milk"])
description = action_designator.ReleaseAction([Arms.LEFT], object_description)
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().gripper, Arms.LEFT)
self.assertEqual(description.ground().object_designator.name, "milk")

def test_grip(self):
object_description = object_designator.ObjectDesignatorDescription(names=["milk"])
description = action_designator.GripAction([Arms.LEFT], object_description, [0.5])
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().gripper, Arms.LEFT)
self.assertEqual(description.ground().object_designator.name, "milk")

def test_park_arms(self):
description = action_designator.ParkArmsAction([Arms.BOTH])
self.assertEqual(description.ground().arm, Arms.BOTH)
self.assertTrue(description.ontology_concept_holders)
with simulated_robot:
description.resolve().perform()
for joint, pose in RobotDescription.current_robot_description.get_static_joint_chain("right", "park").items():
Expand All @@ -66,13 +60,11 @@ def test_navigate(self):
with simulated_robot:
description.resolve().perform()
self.assertEqual(description.ground().target_location, Pose([1, 0, 0], [0, 0, 0, 1]))
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(self.robot.get_pose(), Pose([1, 0, 0]))

def test_pick_up(self):
object_description = object_designator.ObjectDesignatorDescription(names=["milk"])
description = action_designator.PickUpAction(object_description, [Arms.LEFT], [Grasp.FRONT])
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().object_designator.name, "milk")
with simulated_robot:
NavigateActionPerformable(Pose([0.6, 0.4, 0], [0, 0, 0, 1])).perform()
Expand All @@ -83,7 +75,6 @@ def test_pick_up(self):
def test_place(self):
object_description = object_designator.ObjectDesignatorDescription(names=["milk"])
description = action_designator.PlaceAction(object_description, [Pose([1.3, 1, 0.9], [0, 0, 0, 1])], [Arms.LEFT])
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().object_designator.name, "milk")
with simulated_robot:
NavigateActionPerformable(Pose([0.6, 0.4, 0], [0, 0, 0, 1])).perform()
Expand All @@ -94,7 +85,6 @@ def test_place(self):

def test_look_at(self):
description = action_designator.LookAtAction([Pose([1, 0, 1])])
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().target, Pose([1, 0, 1]))
with simulated_robot:
description.resolve().perform()
Expand All @@ -105,7 +95,6 @@ def test_detect(self):
self.milk.set_pose(Pose([1.5, 0, 1.2]))
object_description = object_designator.ObjectDesignatorDescription(names=["milk"])
description = action_designator.DetectAction(object_description)
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().object_designator.name, "milk")
with simulated_robot:
detected_object = description.resolve().perform()
Expand All @@ -118,14 +107,12 @@ def test_detect(self):
def test_open(self):
object_description = object_designator.ObjectDesignatorDescription(names=["milk"])
description = action_designator.OpenAction(object_description, [Arms.LEFT])
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().object_designator.name, "milk")

@unittest.skip
def test_close(self):
object_description = object_designator.ObjectDesignatorDescription(names=["milk"])
description = action_designator.CloseAction(object_description, [Arms.LEFT])
self.assertTrue(description.ontology_concept_holders)
self.assertEqual(description.ground().object_designator.name, "milk")

def test_transport(self):
Expand All @@ -134,7 +121,6 @@ def test_transport(self):
[Arms.LEFT],
[Pose([-1.35, 0.78, 0.95],
[0.0, 0.0, 0.16439898301071468, 0.9863939245479175])])
self.assertTrue(description.ontology_concept_holders)
with simulated_robot:
action_designator.MoveTorsoAction([0.2]).resolve().perform()
description.resolve().perform()
Expand All @@ -148,7 +134,6 @@ def test_grasping(self):
self.robot.set_pose(Pose([-2.14, 1.06, 0]))
milk_desig = object_designator.ObjectDesignatorDescription(names=["milk"])
description = action_designator.GraspingAction([Arms.RIGHT], milk_desig)
self.assertTrue(description.ontology_concept_holders)
with simulated_robot:
description.resolve().perform()
dist = np.linalg.norm(
Expand Down
13 changes: 8 additions & 5 deletions test/test_ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from pycram.ontology.ontology_common import (OntologyConceptHolderStore, OntologyConceptHolder,
ONTOLOGY_SQL_BACKEND_FILE_EXTENSION, ONTOLOGY_OWL_FILE_EXTENSION)


DEFAULT_LOCAL_ONTOLOGY_IRI = "default.owl"
class TestOntologyManager(unittest.TestCase):
ontology_manager: OntologyManager
main_ontology: Optional[owlready2.Ontology]
Expand All @@ -39,15 +39,19 @@ 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)
if cls.ontology_manager.initialized():
cls.main_ontology = cls.ontology_manager.main_ontology
cls.soma = cls.ontology_manager.soma
cls.dul = cls.ontology_manager.dul
else:
cls.main_ontology = None
# Else, load from `DEFAULT_LOCAL_ONTOLOGY_IRI`
cls.soma = None
cls.dul = None
cls.ontology_manager.main_ontology_iri = DEFAULT_LOCAL_ONTOLOGY_IRI
cls.ontology_manager.create_main_ontology_world()
cls.ontology_manager.create_main_ontology()
cls.main_ontology = cls.ontology_manager.main_ontology

@classmethod
def tearDownClass(cls):
Expand Down Expand Up @@ -234,7 +238,7 @@ def test_ontology_reasoning(self):
ontology_property_parent_class=owlready2.ObjectProperty,
ontology=reasoning_ontology))

# Define rules for "bigger_than" in [reasoning_ontology]
# Define rules for `transportability` & `co-residence` in [reasoning_ontology]
with reasoning_ontology:
def can_transport_itself(a: reasoning_ontology.Entity) -> bool:
return a in a.can_transport
Expand Down Expand Up @@ -300,6 +304,5 @@ def test_ontology_save(self):
self.assertTrue(Path(owl_filepath).is_file())
self.assertTrue(Path(sql_filepath).is_file())


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

0 comments on commit f39de3c

Please sign in to comment.