diff --git a/owlapy/owl_reasoner.py b/owlapy/owl_reasoner.py index 1db4101b..b144223e 100644 --- a/owlapy/owl_reasoner.py +++ b/owlapy/owl_reasoner.py @@ -2,6 +2,7 @@ import os import operator import logging +from time import time import owlready2 import json import subprocess @@ -1059,7 +1060,10 @@ def __init__(self, ontology: Union[SyncOntology, str], reasoner="HermiT"): self._owlapi_ontology = self.ontology.get_owlapi_ontology() self.mapper = self.ontology.mapper self.inference_types_mapping = import_and_include_axioms_generators() - self._owlapi_reasoner = initialize_reasoner(reasoner, self._owlapi_ontology) + self._owlapi_reasoner, self._reasoner_factory = initialize_reasoner(reasoner, self._owlapi_ontology) + + def dispose(self): + self._owlapi_reasoner.dispose() def _instances(self, ce: OWLClassExpression, direct=False) -> Set[OWLNamedIndividual]: """ @@ -1450,14 +1454,50 @@ def types(self, individual: OWLNamedIndividual, direct: bool = False): yield from [self.mapper.map_(ind) for ind in self._owlapi_reasoner.getTypes(self.mapper.map_(individual), direct).getFlattened()] - def has_consistent_ontology(self) -> bool: + def has_consistent_ontology(self, timeout: Optional[int] = None) -> bool: """ Check if the used ontology is consistent. + Args: + timeout: Optional timeout in seconds for the consistency check. + Returns: bool: True if the ontology used by this reasoner is consistent, False otherwise. """ - return self._owlapi_reasoner.isConsistent() + if timeout is None: + return self._owlapi_reasoner.isConsistent() + # We run the consistency check in a separate thread to allow for a timeout + # Since reasoning can be destructive (i.e., it can modify the ontology), + # we need to make sure that the reasoner operates on a deep copy of the ontology + # to prevent any unintended side effects on the original ontology. + # If timeout is specified, we need a Java-based timeout logic + if not isinstance(timeout, int) or timeout < 0: + raise ValueError(f"Timeout value must be a non-negative integer. Provided value: {timeout}") + from java.util.concurrent import CompletableFuture, TimeUnit, TimeoutException + from org.semanticweb.owlapi.apibinding import OWLManager + from org.semanticweb.owlapi.model.parameters import OntologyCopy + j_manager = OWLManager.createOWLOntologyManager() + # Load the same ontology into the new manager by deep copy + j_ontology = j_manager.copyOntology(self._owlapi_ontology, OntologyCopy.DEEP) + # Create a new reasoner for the new ontology + j_reasoner = self._reasoner_factory.createReasoner(j_ontology) + future = CompletableFuture.supplyAsync(lambda: j_reasoner.isConsistent()) + try: + j_is_consistent = future.get(timeout, TimeUnit.SECONDS) + except TimeoutException: + future.cancel(True) + # Dispose of the reasoner + j_reasoner.dispose() + # Dispose of the ontology and the manager + j_manager.clearOntologies() + del j_manager + raise TimeoutError(f"Consistency check took longer than {timeout} seconds and was cancelled.") + # Dispose of the reasoner + j_reasoner.dispose() + # Dispose of the ontology and the manager + j_manager.clearOntologies() + del j_manager + return bool(j_is_consistent) def infer_axioms(self, inference_types: list[str]) -> Iterable[OWLAxiom]: """ @@ -1576,16 +1616,45 @@ def generate_and_save_inferred_class_assertion_axioms(self, output="temp.ttl", o """ self.infer_axioms_and_save(output, output_format, ["InferredClassAssertionAxiomGenerator"]) - def is_entailed(self, axiom: OWLAxiom) -> bool: + def is_entailed(self, axiom: OWLAxiom, timeout: Optional[int] = None) -> bool: """A convenience method that determines if the specified axiom is entailed by the set of reasoner axioms. Args: axiom: The axiom to check for entailment. + timeout: The maximum time in seconds to wait for the entailment check. If None, no timeout is applied. Return: True if the axiom is entailed by the reasoner axioms and False otherwise. """ - return bool(self._owlapi_reasoner.isEntailed(self.mapper.map_(axiom))) + if timeout is None: + return bool(self._owlapi_reasoner.isEntailed(self.mapper.map_(axiom))) + # If timeout is specified, we need a Java-based timeout logic + if not isinstance(timeout, int) or timeout < 0: + raise ValueError(f"Timeout value must be a non-negative integer. Provided value: {timeout}") + from java.util.concurrent import CompletableFuture, TimeUnit, TimeoutException + from org.semanticweb.owlapi.apibinding import OWLManager + from org.semanticweb.owlapi.model.parameters import OntologyCopy + j_manager = OWLManager.createOWLOntologyManager() + # Load the same ontology into the new manager by deep copy + j_ontology = j_manager.copyOntology(self._owlapi_ontology, OntologyCopy.DEEP) + # Create a new reasoner for the new ontology + j_reasoner = self._reasoner_factory.createReasoner(j_ontology) + j_axiom = self.mapper.map_(axiom) + future = CompletableFuture.supplyAsync(lambda: j_reasoner.isEntailed(j_axiom)) + try: + j_is_entailed = future.get(timeout, TimeUnit.SECONDS) + except TimeoutException: + future.cancel(True) + # Dispose of the reasoner + j_reasoner.dispose() + # Dispose of the ontology and the manager + j_manager.clearOntologies() + del j_manager + raise TimeoutError(f"Entailment check took longer than {timeout} seconds and was cancelled.") + j_reasoner.dispose() + j_manager.clearOntologies() + del j_manager + return bool(j_is_entailed) def is_satisfiable(self, ce: OWLClassExpression) -> bool: """A convenience method that determines if the specified class expression is satisfiable with respect @@ -1600,14 +1669,45 @@ def is_satisfiable(self, ce: OWLClassExpression) -> bool: return bool(self._owlapi_reasoner.isSatisfiable(self.mapper.map_(ce))) - def unsatisfiable_classes(self): + def unsatisfiable_classes(self, timeout: Optional[int] = None) -> Set[OWLClass]: """A convenience method that obtains the classes in the signature of the root ontology that are unsatisfiable.""" - return self.mapper.map_(self._owlapi_reasoner.unsatisfiableClasses()) + if timeout is None: + return self.mapper.map_(self._owlapi_reasoner.unsatisfiableClasses()) + # If timeout is specified, we need a Java-based timeout logic + # Check that time timeout is indeed a non-negative integer + if not isinstance(timeout, int) or timeout < 0: + raise ValueError(f"Timeout value must be a non-negative integer. Provided value: {timeout}") + from java.util.concurrent import CompletableFuture, TimeUnit, TimeoutException + from org.semanticweb.owlapi.apibinding import OWLManager + from org.semanticweb.owlapi.model.parameters import OntologyCopy + j_manager = OWLManager.createOWLOntologyManager() + # Load the same ontology into the new manager by deep copy + j_ontology = j_manager.copyOntology(self._owlapi_ontology, OntologyCopy.DEEP) + # Create a new reasoner for the new ontology + j_reasoner = self._reasoner_factory.createReasoner(j_ontology) + future = CompletableFuture.supplyAsync(lambda: j_reasoner.unsatisfiableClasses()) + try: + j_unsatisfiable_classes = future.get(timeout, TimeUnit.SECONDS) + except TimeoutException: + future.cancel(True) + # Dispose of the reasoner + j_reasoner.dispose() + # Dispose of the ontology and the manager + j_manager.clearOntologies() + del j_manager + raise TimeoutError(f"Obtaining unsatisfiable classes took longer than {timeout} seconds and was cancelled.") + + j_reasoner.dispose() + j_manager.clearOntologies() + del j_manager + + return self.mapper.map_(j_unsatisfiable_classes) def create_axiom_justifications(self, axiom_to_explain: OWLAxiom, n_max_justifications: Optional[int] = 10, + timeout: Optional[int] = 1000, save: bool = False, ) -> List[Set[OWLAxiom]]: """Generate multiple justifications for why an axiom is entailed by the ontology. @@ -1636,6 +1736,7 @@ def create_axiom_justifications(self, Args: axiom_to_explain (OWLAxiom): The axiom to create justifications for. n_max_justifications (Optional[int], optional): The maximum number of justifications to generate. Defaults to 10. + timeout (Optional[int], optional): The maximum time (in seconds) to allow for justification generation before raising a TimeoutError. Defaults to 1000 seconds. save (bool, optional): If True, saves all justifications in a new ontology as axioms. Defaults to False. Raises: @@ -1655,32 +1756,27 @@ def create_axiom_justifications(self, HSTExplanationGenerator, SatisfiabilityConverter ) - - j_axiom = self.mapper.map_(axiom_to_explain) - j_ontology = self._owlapi_ontology - j_reasoner = self._owlapi_reasoner - j_data_factory = self._owlapi_manager.getOWLDataFactory() - - if self.reasoner_name == "Pellet": - from openllet.owlapi import PelletReasonerFactory - reasoner_factory = PelletReasonerFactory.getInstance() - elif self.reasoner_name == "HermiT": - from org.semanticweb.HermiT import ReasonerFactory - reasoner_factory = ReasonerFactory() - elif self.reasoner_name == "ELK": - from org.semanticweb.elk.owlapi import ElkReasonerFactory - reasoner_factory = ElkReasonerFactory() - elif self.reasoner_name == "JFact": - from uk.ac.manchester.cs.jfact import JFactFactory - reasoner_factory = JFactFactory() - elif self.reasoner_name == "Openllet": - from openllet.owlapi import PelletReasonerFactory - reasoner_factory = PelletReasonerFactory.getInstance() - elif self.reasoner_name == "Structural": - from org.semanticweb.owlapi.reasoner.structural import StructuralReasonerFactory - reasoner_factory = StructuralReasonerFactory() + from java.util.concurrent import CompletableFuture, TimeUnit, TimeoutException + if timeout: + from org.semanticweb.owlapi.apibinding import OWLManager + from org.semanticweb.owlapi.model.parameters import OntologyCopy + j_manager = OWLManager.createOWLOntologyManager() + # Make ontology manager + # Load the same ontology into the new manager by deep copy + j_ontology = j_manager.copyOntology(self._owlapi_ontology, OntologyCopy.DEEP) + # Create a new reasoner for the new ontology + j_reasoner = self._reasoner_factory.createReasoner(j_ontology) + j_reasoner_factory = self._reasoner_factory else: - raise NotImplementedError(f"Reasoner '{self.reasoner_name}' is not supported for axiom justification.") + j_ontology = self._owlapi_ontology + j_reasoner = self._owlapi_reasoner + j_data_factory = self._owlapi_manager.getOWLDataFactory() + j_reasoner_factory = self._reasoner_factory + j_reasoner = self._owlapi_reasoner + j_manager = self._owlapi_manager + + j_axiom = self.mapper.map_(axiom_to_explain) + j_data_factory = j_manager.getOWLDataFactory() # Following the internal implementation of DefaultExplanationGenerator to circumvent the need for a progress monitor @@ -1694,7 +1790,7 @@ def create_axiom_justifications(self, f"This most likely means that the axiom type {type(axiom_to_explain)} is not supported for justification generation.\n{str(e)}" ) raise e - blackbox_exp = BlackBoxExplanation(j_ontology, reasoner_factory, j_reasoner) + blackbox_exp = BlackBoxExplanation(j_ontology, j_reasoner_factory, j_reasoner) explanation_gen = HSTExplanationGenerator(blackbox_exp) justifications = [] @@ -1704,22 +1800,71 @@ def create_axiom_justifications(self, raise ValueError( f"n_max_justifications must be an integer or None, but got {n_max_justifications}" ) + if n_max_justifications is not None and n_max_justifications > 0: + time_start = time() try: - j_explanations = explanation_gen.getExplanations( - j_axiom_ce, n_max_justifications - ) + if timeout is None: + j_explanations = explanation_gen.getExplanations( + j_axiom_ce, n_max_justifications + ) + else: + future = CompletableFuture.supplyAsync( + lambda: explanation_gen.getExplanations(j_axiom_ce, n_max_justifications) + ) + j_explanations = future.get(timeout, TimeUnit.SECONDS) except Exception as e: - raise ValueError( - f"Justification failed most likely because the axiom type {type(axiom_to_explain)} is not supported for justification generation.\n{str(e)}" - ) + time_end = time() + if isinstance(e, TimeoutException) and (timeout is not None): + future.cancel(True) # Attempt to cancel the task if it's still running + # Reset the reasoner and the reasoner factory + j_reasoner.dispose() + # Wipe the ontology from the manager to free up memory + j_manager.removeOntology(j_ontology) + # Wipe the manager too + j_manager.clearOntologies() + del j_manager + raise TimeoutError( + f"Justification generation exceeded the timeout of {timeout} seconds. (Elapsed time: {time_end - time_start:.2f} seconds). " + f"Consider increasing the timeout or reducing the number of justifications to generate." + ) + if "not implemented" in str(e).lower(): + raise ValueError( + f"Justification failed most likely because the axiom type {type(axiom_to_explain)} is not supported for justification generation.\n{str(e)}" + ) + raise e else: + time_start = time() try: - j_explanations = explanation_gen.getExplanations(j_axiom_ce) + if timeout is None: + j_explanations = explanation_gen.getExplanations(j_axiom_ce) + else: + future = CompletableFuture.supplyAsync( + lambda: explanation_gen.getExplanations(j_axiom_ce) + ) + j_explanations = future.get(timeout, TimeUnit.SECONDS) + except Exception as e: - raise ValueError( - f"Justification failed most likely because the axiom type {type(axiom_to_explain)} is not supported for justification generation.\n{str(e)}" - ) + time_end = time() + if isinstance(e, TimeoutException) and (timeout is not None): + future.cancel(True) # Attempt to cancel the task if it's still running + # Reset the reasoner and the reasoner factory + j_reasoner.dispose() + # Wipe the ontology from the manager to free up memory + j_manager.removeOntology(j_ontology) + # Wipe the manager too + j_manager.clearOntologies() + del j_manager + raise TimeoutError( + f"Justification generation exceeded the timeout of {timeout} seconds (elapsed: {time_end - time_start:.2f} seconds). " + f"Consider increasing the timeout or reducing the number of justifications to generate." + ) + if "not implemented" in str(e).lower(): + raise ValueError( + f"Justification failed most likely because the axiom type {type(axiom_to_explain)} is not supported for justification generation.\n{str(e)}" + ) + raise e + for j_expl in j_explanations: py_axioms = {self.mapper.map_(ax) for ax in j_expl} @@ -1748,7 +1893,7 @@ def create_axiom_justifications(self, def create_laconic_axiom_justifications(self, axiom_to_explain: OWLAxiom, n_max_justifications: Optional[int] = 10, - timeout: int = 10_000, + timeout: Optional[int] = 1000, save: bool = False) -> List[Set[OWLAxiom]]: """Generate multiple laconic justifications for why an axiom is entailed by the ontology. Laconic justifications are a subset of the full justifications, where all irrelevant axioms have been removed. @@ -1759,7 +1904,7 @@ def create_laconic_axiom_justifications(self, Args: axiom_to_explain (OWLAxiom): The axiom to create laconic justifications for. Must be of a type that can be converted into a class expression (e.g., OWLSubClassOfAxiom, OWLClassAssertionAxiom, etc.). See n_max_justifications (Optional[int], optional): The maximum number of laconic justifications to generate. If None or a non-positive integer is provided, all justifications will be generated. Defaults to 10. - timeout (int, optional): The maximum time (in milliseconds) to wait for each justification. Defaults to 10_000. + timeout (Optional[int], optional): The maximum time (in seconds) to wait for justifications. Defaults to 1000. save (bool, optional): Whether to save the generated justifications to a file. Defaults to False. Raises: @@ -1769,34 +1914,23 @@ def create_laconic_axiom_justifications(self, Returns: List[Set[OWLAxiom]]: A list of sets of OWLAxioms, where each set represents a laconic justification for why the axiom is entailed by the ontology. """ - from org.semanticweb.owl.explanation.api import ExplanationManager from org.semanticweb.owl.explanation.impl.laconic import LaconicExplanationGeneratorFactory + from java.util.concurrent import CompletableFuture, TimeUnit, TimeoutException # Get a hold of the reasoner factory and the ontology - if self.reasoner_name == "Pellet": - from openllet.owlapi import PelletReasonerFactory - j_reasoner_factory = PelletReasonerFactory.getInstance() - elif self.reasoner_name == "HermiT": - from org.semanticweb.HermiT import ReasonerFactory - j_reasoner_factory = ReasonerFactory() - elif self.reasoner_name == "ELK": - from org.semanticweb.elk.owlapi import ElkReasonerFactory - j_reasoner_factory = ElkReasonerFactory() - elif self.reasoner_name == "JFact": - from uk.ac.manchester.cs.jfact import JFactFactory - j_reasoner_factory = JFactFactory() - elif self.reasoner_name == "Openllet": - from openllet.owlapi import PelletReasonerFactory - j_reasoner_factory = PelletReasonerFactory.getInstance() - elif self.reasoner_name == "Structural": - from org.semanticweb.owlapi.reasoner.structural import StructuralReasonerFactory - j_reasoner_factory = StructuralReasonerFactory() + if timeout: + # Import org.semanticweb.owlapi.model.OWLOntologyManager and org.semanticweb.owlapi.model.parameters.OntologyCopy + from org.semanticweb.owlapi.apibinding import OWLManager + from org.semanticweb.owlapi.model.parameters import OntologyCopy + # Because ontology reasoning can be a destructive operation (axiom addition/removal) + # we have to operate on a deep copy of the ontology. + j_manager = OWLManager.createOWLOntologyManager() + j_ontology = j_manager.copyOntology(self._owlapi_ontology, OntologyCopy.DEEP) + j_reasoner_factory = initialize_reasoner_factory(self.reasoner_name) else: - raise NotImplementedError( - f"Reasoner '{self.reasoner_name}' is not supported for laconic axiom justification.") - - j_ontology = self._owlapi_ontology + j_ontology = self._owlapi_ontology + j_reasoner_factory = self._reasoner_factory j_gen_fac = ExplanationManager.createExplanationGeneratorFactory(j_reasoner_factory) j_laconic_gen_fac = LaconicExplanationGeneratorFactory(j_gen_fac) @@ -1810,10 +1944,42 @@ def create_laconic_axiom_justifications(self, raise ValueError( f"n_max_justifications must be an integer or None, but got {n_max_justifications}" ) - if n_max_justifications is not None and n_max_justifications > 0: - j_explanations = j_gen.getExplanations(j_axiom, n_max_justifications) + if timeout is None: + try: + if n_max_justifications is not None and n_max_justifications > 0: + j_explanations = j_gen.getExplanations(j_axiom, n_max_justifications) + else: + j_explanations = j_gen.getExplanations(j_axiom) + except Exception as e: + raise ValueError( + f"Failed to get laconic justifications: {str(e)}" + ) else: - j_explanations = j_gen.getExplanations(j_axiom) + try: + if n_max_justifications is not None and n_max_justifications > 0: + future = CompletableFuture.supplyAsync( + lambda: j_gen.getExplanations(j_axiom, n_max_justifications) + ) + else: + future = CompletableFuture.supplyAsync( + lambda: j_gen.getExplanations(j_axiom) + ) + j_explanations = future.get(timeout, TimeUnit.SECONDS) + except Exception as e: + if isinstance(e, TimeoutException) and (timeout is not None): + future.cancel(True) # Attempt to cancel the task if it's still running + # Wipe the ontology + j_manager.removeOntology(j_ontology) + # Wipe the manager + j_manager.clearOntologies() + del j_manager + raise TimeoutError( + f"Laconic justification generation exceeded the timeout of {timeout} seconds. Consider increasing the timeout or reducing the number of justifications to generate." + ) + raise ValueError( + f"Failed to get laconic justifications: {str(e)}" + ) + justifications = [] for j_expl in j_explanations: py_axioms = {self.mapper.map_(ax) for ax in j_expl.getAxioms()} @@ -2073,35 +2239,37 @@ def get_root_ontology(self) -> AbstractOWLOntology: return self.ontology -def initialize_reasoner(reasoner: str, owlapi_ontology): - # () Create a reasoner using the ontology +def initialize_reasoner_factory(reasoner: str): if reasoner == "HermiT": # noinspection PyUnresolvedReferences from org.semanticweb.HermiT import ReasonerFactory - owlapi_reasoner = ReasonerFactory().createReasoner(owlapi_ontology) - assert owlapi_reasoner.getReasonerName() == "HermiT" + return ReasonerFactory() elif reasoner == "ELK": from org.semanticweb.elk.owlapi import ElkReasonerFactory - owlapi_reasoner = ElkReasonerFactory().createReasoner(owlapi_ontology) + return ElkReasonerFactory() elif reasoner == "JFact": # noinspection PyUnresolvedReferences from uk.ac.manchester.cs.jfact import JFactFactory - owlapi_reasoner = JFactFactory().createReasoner(owlapi_ontology) + return JFactFactory() elif reasoner == "Pellet": # noinspection PyUnresolvedReferences from openllet.owlapi import PelletReasonerFactory - owlapi_reasoner = PelletReasonerFactory().createReasoner(owlapi_ontology) + return PelletReasonerFactory.getInstance() elif reasoner == "Openllet": # noinspection PyUnresolvedReferences from openllet.owlapi import OpenlletReasonerFactory - owlapi_reasoner = OpenlletReasonerFactory().getInstance().createReasoner(owlapi_ontology) + return OpenlletReasonerFactory().getInstance() elif reasoner == "Structural": # noinspection PyUnresolvedReferences from org.semanticweb.owlapi.reasoner.structural import StructuralReasonerFactory - owlapi_reasoner = StructuralReasonerFactory().createReasoner(owlapi_ontology) + return StructuralReasonerFactory() else: raise NotImplementedError("Not implemented") - return owlapi_reasoner + +def initialize_reasoner(reasoner: str, owlapi_ontology): + reasoner_factory = initialize_reasoner_factory(reasoner) + owlapi_reasoner = reasoner_factory.createReasoner(owlapi_ontology) + return owlapi_reasoner, reasoner_factory def import_and_include_axioms_generators(): diff --git a/tests/test_ontology_justification.py b/tests/test_ontology_justification.py index e43d0a66..b98121dc 100644 --- a/tests/test_ontology_justification.py +++ b/tests/test_ontology_justification.py @@ -314,5 +314,195 @@ def test_inconsistency_check_2(self): self.assertFalse(test_reasoner.has_consistent_ontology(), "Justification should lead to an inconsistent ontology.") +class TestJustificationTimeout(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.ontology_path = None + for root, dirs, files in os.walk("."): + for file in files: + # Very large ontology with many justifications, so we can test timeout behavior. + if file == "carcinogenesis.owl": + cls.ontology_path = os.path.abspath(os.path.join(root, file)) + print(f"Found ontology at: {cls.ontology_path}") + break + if cls.ontology_path: + break + + if cls.ontology_path is None: + raise FileNotFoundError("Could not locate 'carcinogenesis.owl' within project structure.") + + cls.namespace = adjust_namespace("http://dl-learner.org/carcinogenesis#") + + try: + print("Loading ontology and initializing reasoner...") + cls.ontology = SyncOntology(cls.ontology_path) + cls.reasoner = SyncReasoner(cls.ontology, reasoner="HermiT") + print("Ontology loaded and reasoner initialized successfully.") + except Exception as e: + raise RuntimeError(f"Failed to load ontology or initialize reasoner: {e}") + + def test_justification_timeout(self): + # "Compound and hasAtom some Carbon" + # In description logic: Compound ⊓ ∃ hasAtom.Carbon + # Due to ontology size, we could expect some timeout here + manchester_expr = "Compound and hasAtom some Carbon" + owl_expr = manchester_to_owl_expression( + manchester_expr, namespace="http://dl-learner.org/carcinogenesis#" + ) + d100 = OWLNamedIndividual( + IRI.create("http://dl-learner.org/carcinogenesis#d100") + ) + + class_assertion = OWLClassAssertionAxiom(d100, owl_expr) + print("Axiom to prove: ", class_assertion) + + + # Check if class assertion is entailed + if not self.reasoner.is_entailed(class_assertion): + print("Class assertion is not entailed, so justifications cannot be generated.") + return + + timeout = 1 # Very short timeout to force timeout behavior since the ontology is huge + + with self.assertRaises(TimeoutError) as cm1: + self.reasoner.create_axiom_justifications( + class_assertion, timeout=timeout, save=False + ) + # Get the exception message and check that it contains the expected timeout information + print(f"Caught exception message: {str(cm1.exception)}") + # Due to internal Java-side logging, this actually floods the stdout. + # Without disabling the logging (e.g., with JAVA_TOOL_OPTIONS="-Dorg.slf4j.simpleLogger.defaultLogLevel=off"), + # the following part of the test is likely to crash the CI due to too much logging output + # even though the timeout behavior is working as expected. + # with self.assertRaises(TimeoutError) as cm2: + # self.reasoner.create_laconic_axiom_justifications( + # class_assertion, timeout=timeout, save=False + # ) + # print(f"Caught exception message for laconic justifications: {str(cm2.exception)}") + + # For some reason, the following does not work, and returns an empty list. + # The same block of code would work if isolated into a separate test case. + + +# Create a separate test for no timeout +# Otherwise, for some reason we get ConcurrentModificationException from java +class TestJustificationNoTimeout(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.ontology_path = None + for root, dirs, files in os.walk("."): + for file in files: + # Very large ontology with many justifications, so we can test timeout behavior. + if file == "carcinogenesis.owl": + cls.ontology_path = os.path.abspath(os.path.join(root, file)) + print(f"Found ontology at: {cls.ontology_path}") + break + if cls.ontology_path: + break + + if cls.ontology_path is None: + raise FileNotFoundError("Could not locate 'carcinogenesis.owl' within project structure.") + + cls.namespace = adjust_namespace("http://dl-learner.org/carcinogenesis#") + + try: + print("Loading ontology and initializing reasoner...") + cls.ontology = SyncOntology(cls.ontology_path) + cls.reasoner = SyncReasoner(cls.ontology, reasoner="HermiT") + print("Ontology loaded and reasoner initialized successfully.") + except Exception as e: + raise RuntimeError(f"Failed to load ontology or initialize reasoner: {e}") + + def test_justifications_no_timeout(self): + owl_expr = manchester_to_owl_expression( + "Compound and hasAtom some Carbon", namespace="http://dl-learner.org/carcinogenesis#" + ) + d100 = OWLNamedIndividual( + IRI.create("http://dl-learner.org/carcinogenesis#d100") + ) + class_assertion = OWLClassAssertionAxiom(d100, owl_expr) + # reasoner = SyncReasoner(self.ontology) + # Now, just give it an indefinite amount of time + justifications = self.reasoner.create_axiom_justifications( + class_assertion, + n_max_justifications=1, + timeout=None + ) + + self.assertIsInstance(justifications, list, "Justifications should be a list.") + # Check that the list is nonempty + self.assertGreater(len(justifications), 0, "Justifications list should not be empty.") + print(f"Successfully generated justifications without timeout: {justifications}") + for justification in justifications: + print("Justification:") + for axiom in justification: + print(f" {axiom}") + + # This is a bit long, but does the same. + # justifications_laconic = self.reasoner.create_laconic_axiom_justifications( + # class_assertion, + # n_max_justifications=3, + # timeout=None, + # save=False + # ) + # self.assertIsInstance(justifications_laconic, list, "Laconic justifications should be a list.") + # print(f"Successfully generated laconic justifications without timeout: {justifications_laconic}") + # for justification in justifications_laconic: + # print("Laconic Justification:") + # for axiom in justification: + # print(f" {axiom}") + + def test_consecutive_calls_with_and_without_timeout(self): + owl_expr = manchester_to_owl_expression( + "Compound and hasAtom some Carbon", namespace="http://dl-learner.org/carcinogenesis#" + ) + d100 = OWLNamedIndividual( + IRI.create("http://dl-learner.org/carcinogenesis#d100") + ) + class_assertion = OWLClassAssertionAxiom(d100, owl_expr) + n_tbox_axioms = len(self.ontology.get_tbox_axioms()) + n_abox_axioms = len(self.ontology.get_abox_axioms()) + print(f"Ontology loaded with {n_tbox_axioms} TBox axioms and {n_abox_axioms} ABox axioms.") + + # Assert that axiom is entailed + self.assertTrue( + self.reasoner.is_entailed(class_assertion), + "Axiom should be entailed by the ontology, but was not. Check that the ontology is loaded correctly and that the axiom is correctly formulated." + f"Number of tbox axioms: {len(self.ontology.get_tbox_axioms())}, number of abox axioms: {len(self.ontology.get_abox_axioms())}" + ) + + # First, call with a short timeout to trigger the timeout behavior + # Do not use pytest assertRaise + try: + self.reasoner.create_axiom_justifications( + class_assertion, + n_max_justifications=5, + timeout=1 + ) + except TimeoutError as e: + print(f"Timeout occurred as expected: {e}") + + # Assert that axiom is still entailed + self.assertTrue( + self.reasoner.is_entailed(class_assertion), + "Axiom should be entailed by the ontology (after timeout), but was not. Check that the ontology is loaded correctly and that the axiom is correctly formulated." + f"Number of tbox axioms: {len(self.ontology.get_tbox_axioms())}, number of abox axioms: {len(self.ontology.get_abox_axioms())}" + ) + + # Then, call again with no timeout and check that justifications are generated successfully + justifications = self.reasoner.create_axiom_justifications( + class_assertion, + n_max_justifications=1, + timeout=None + ) + self.assertIsInstance(justifications, list, "Justifications should be a list.") + self.assertGreater(len(justifications), 0, "Justifications list should not be empty.") + print(f"Successfully generated justifications after previous timeout: {justifications}") + for justification in justifications: + print("Justification:") + for axiom in justification: + print(f" {axiom}") + + if __name__ == "__main__": unittest.main()