Skip to content

Ontology Reasoning

OntologyReasoner(onto, reasoner_type)

Ontology reasoner class that extends from the Java library OWLAPI.

Attributes:

Name Type Description
onto Ontology

The input deeponto ontology.

owl_reasoner_factory OWLReasonerFactory

A reasoner factory for creating a reasoner.

owl_reasoner OWLReasoner

The created reasoner.

owl_data_factory OWLDataFactory

A data factory (inherited from onto) for manipulating axioms.

Parameters:

Name Type Description Default
onto Ontology

The input ontology to conduct reasoning on.

required
reasoner_type str

The type of reasoner used. Options are ["hermit", "elk", "struct"].

required
Source code in src/deeponto/onto/ontology.py
628
629
630
631
632
633
634
635
636
637
638
639
640
def __init__(self, onto: Ontology, reasoner_type: str):
    """Initialise an ontology reasoner.

    Args:
        onto (Ontology): The input ontology to conduct reasoning on.
        reasoner_type (str): The type of reasoner used. Options are `["hermit", "elk", "struct"]`.
    """
    self.onto = onto
    self.owl_reasoner_factory = None
    self.owl_reasoner = None
    self.reasoner_type = reasoner_type
    self.load_reasoner(self.reasoner_type)
    self.owl_data_factory = self.onto.owl_data_factory

load_reasoner(reasoner_type)

Load a new reaonser and dispose the old one if existed.

Source code in src/deeponto/onto/ontology.py
642
643
644
645
646
647
648
649
650
651
652
653
654
655
def load_reasoner(self, reasoner_type: str):
    """Load a new reaonser and dispose the old one if existed."""
    assert reasoner_type in REASONER_DICT.keys(), f"Unknown or unsupported reasoner type: {reasoner_type}."

    if self.owl_reasoner:
        self.owl_reasoner.dispose()

    self.reasoner_type = reasoner_type
    self.owl_reasoner_factory = REASONER_DICT[self.reasoner_type]()
    # TODO: remove ELK message
    # somehow Level.ERROR does not prevent the INFO message from ELK
    # Logger.getLogger("org.semanticweb.elk").setLevel(Level.OFF)

    self.owl_reasoner = self.owl_reasoner_factory.createReasoner(self.onto.owl_onto)

get_entity_type(entity, is_singular=False) staticmethod

A handy method to get the type of an entity (OWLObject).

NOTE: This method is inherited from the Ontology Class.

Source code in src/deeponto/onto/ontology.py
657
658
659
660
661
662
663
@staticmethod
def get_entity_type(entity: OWLObject, is_singular: bool = False):
    """A handy method to get the type of an entity (`OWLObject`).

    NOTE: This method is inherited from the Ontology Class.
    """
    return Ontology.get_entity_type(entity, is_singular)

has_iri(entity) staticmethod

Check if an entity has an IRI.

Source code in src/deeponto/onto/ontology.py
665
666
667
668
669
670
671
672
@staticmethod
def has_iri(entity: OWLObject):
    """Check if an entity has an IRI."""
    try:
        entity.getIRI()
        return True
    except:
        return False

get_inferred_super_entities(entity, direct=False)

Return the IRIs of named super-entities of a given OWLObject according to the reasoner.

A mixture of getSuperClasses, getSuperObjectProperties, getSuperDataProperties functions imported from the OWLAPI reasoner. The type of input entity will be automatically determined. The top entity such as owl:Thing is ignored.

Parameters:

Name Type Description Default
entity OWLObject

An OWLObject entity of interest.

required
direct bool

Return parents (direct=True) or ancestors (direct=False). Defaults to False.

False

Returns:

Type Description
list[str]

A list of IRIs of the super-entities of the given OWLObject entity.

Source code in src/deeponto/onto/ontology.py
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
def get_inferred_super_entities(self, entity: OWLObject, direct: bool = False):
    r"""Return the IRIs of named super-entities of a given `OWLObject` according to the reasoner.

    A mixture of `getSuperClasses`, `getSuperObjectProperties`, `getSuperDataProperties`
    functions imported from the OWLAPI reasoner. The type of input entity will be
    automatically determined. The top entity such as `owl:Thing` is ignored.


    Args:
        entity (OWLObject): An `OWLObject` entity of interest.
        direct (bool, optional): Return parents (`direct=True`) or
            ancestors (`direct=False`). Defaults to `False`.

    Returns:
        (list[str]): A list of IRIs of the super-entities of the given `OWLObject` entity.
    """
    entity_type = self.get_entity_type(entity)
    get_super = f"getSuper{entity_type}"
    TOP = TOP_BOTTOMS[entity_type].TOP  # get the corresponding TOP entity
    super_entities = getattr(self.owl_reasoner, get_super)(entity, direct).getFlattened()
    super_entity_iris = [str(s.getIRI()) for s in super_entities]
    # the root node is owl#Thing
    if TOP in super_entity_iris:
        super_entity_iris.remove(TOP)
    return super_entity_iris

get_inferred_sub_entities(entity, direct=False)

Return the IRIs of named sub-entities of a given OWLObject according to the reasoner.

A mixture of getSubClasses, getSubObjectProperties, getSubDataProperties functions imported from the OWLAPI reasoner. The type of input entity will be automatically determined. The bottom entity such as owl:Nothing is ignored.

Parameters:

Name Type Description Default
entity OWLObject

An OWLObject entity of interest.

required
direct bool

Return parents (direct=True) or ancestors (direct=False). Defaults to False.

False

Returns:

Type Description
list[str]

A list of IRIs of the sub-entities of the given OWLObject entity.

Source code in src/deeponto/onto/ontology.py
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
def get_inferred_sub_entities(self, entity: OWLObject, direct: bool = False):
    """Return the IRIs of named sub-entities of a given `OWLObject` according to the reasoner.

    A mixture of `getSubClasses`, `getSubObjectProperties`, `getSubDataProperties`
    functions imported from the OWLAPI reasoner. The type of input entity will be
    automatically determined. The bottom entity such as `owl:Nothing` is ignored.

    Args:
        entity (OWLObject): An `OWLObject` entity of interest.
        direct (bool, optional): Return parents (`direct=True`) or
            ancestors (`direct=False`). Defaults to `False`.

    Returns:
        (list[str]): A list of IRIs of the sub-entities of the given `OWLObject` entity.
    """
    entity_type = self.get_entity_type(entity)
    get_sub = f"getSub{entity_type}"
    BOTTOM = TOP_BOTTOMS[entity_type].BOTTOM
    sub_entities = getattr(self.owl_reasoner, get_sub)(entity, direct).getFlattened()
    sub_entity_iris = [str(s.getIRI()) for s in sub_entities]
    # the root node is owl#Thing
    if BOTTOM in sub_entity_iris:
        sub_entity_iris.remove(BOTTOM)
    return sub_entity_iris

check_subsumption(sub_entity, super_entity)

Check if the first entity is subsumed by the second entity according to the reasoner.

Source code in src/deeponto/onto/ontology.py
725
726
727
728
729
730
731
732
def check_subsumption(self, sub_entity: OWLObject, super_entity: OWLObject):
    """Check if the first entity is subsumed by the second entity according to the reasoner."""
    entity_type = self.get_entity_type(sub_entity, is_singular=True)
    assert entity_type == self.get_entity_type(super_entity, is_singular=True)

    sub_axiom = getattr(self.owl_data_factory, f"getOWLSub{entity_type}OfAxiom")(sub_entity, super_entity)

    return self.owl_reasoner.isEntailed(sub_axiom)

check_disjoint(entity1, entity2)

Check if two entities are disjoint according to the reasoner.

Source code in src/deeponto/onto/ontology.py
734
735
736
737
738
739
740
741
def check_disjoint(self, entity1: OWLObject, entity2: OWLObject):
    """Check if two entities are disjoint according to the reasoner."""
    entity_type = self.get_entity_type(entity1)
    assert entity_type == self.get_entity_type(entity2)

    disjoint_axiom = getattr(self.owl_data_factory, f"getOWLDisjoint{entity_type}Axiom")([entity1, entity2])

    return self.owl_reasoner.isEntailed(disjoint_axiom)

check_common_descendants(entity1, entity2)

Check if two entities have a common decendant.

Entities can be OWL class or property expressions, and can be either atomic or complex. It takes longer computation time for the complex ones. Complex entities do not have an IRI. This method is optimised in the way that if there exists an atomic entity A, we compute descendants for A and compare them against the other entity which could be complex.

Source code in src/deeponto/onto/ontology.py
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
def check_common_descendants(self, entity1: OWLObject, entity2: OWLObject):
    """Check if two entities have a common decendant.

    Entities can be **OWL class or property expressions**, and can be either **atomic
    or complex**. It takes longer computation time for the complex ones. Complex
    entities do not have an IRI. This method is optimised in the way that if
    there exists an atomic entity `A`, we compute descendants for `A` and
    compare them against the other entity which could be complex.
    """
    entity_type = self.get_entity_type(entity1)
    assert entity_type == self.get_entity_type(entity2)

    if not self.has_iri(entity1) and not self.has_iri(entity2):
        logger.warn("Computing descendants for two complex entities is not efficient.")

    # `computed` is the one we compute the descendants
    # `compared` is the one we compare `computed`'s descendant one-by-one
    # we set the atomic entity as `computed` for efficiency if there is one
    computed, compared = entity1, entity2
    if not self.has_iri(entity1) and self.has_iri(entity2):
        computed, compared = entity2, entity1

    # for every inferred child of `computed`, check if it is subsumed by `compared``
    for descendant_iri in self.get_inferred_sub_entities(computed, direct=False):
        # print("check a subsumption")
        if self.check_subsumption(self.onto.get_owl_object(descendant_iri), compared):
            return True
    return False

get_instances(owl_class, direct=False)

Return the list of named individuals that are instances of a given OWL class expression.

Parameters:

Name Type Description Default
owl_class OWLClassExpression

An ontology class of interest.

required
direct bool

Return direct instances (direct=True) or also include the sub-classes' instances (direct=False). Defaults to False.

False

Returns:

Type Description
list[OWLIndividual]

A list of named individuals that are instances of owl_class.

Source code in src/deeponto/onto/ontology.py
772
773
774
775
776
777
778
779
780
781
782
783
def get_instances(self, owl_class: OWLClassExpression, direct: bool = False):
    """Return the list of named individuals that are instances of a given OWL class expression.

    Args:
        owl_class (OWLClassExpression): An ontology class of interest.
        direct (bool, optional): Return direct instances (`direct=True`) or
            also include the sub-classes' instances (`direct=False`). Defaults to `False`.

    Returns:
        (list[OWLIndividual]): A list of named individuals that are instances of `owl_class`.
    """
    return list(self.owl_reasoner.getInstances(owl_class, direct).getFlattened())

check_instance(owl_instance, owl_class)

Check if a named individual is an instance of an OWL class.

Source code in src/deeponto/onto/ontology.py
785
786
787
788
def check_instance(self, owl_instance: OWLIndividual, owl_class: OWLClassExpression):
    """Check if a named individual is an instance of an OWL class."""
    assertion_axiom = self.owl_data_factory.getOWLClassAssertionAxiom(owl_class, owl_instance)
    return self.owl_reasoner.isEntailed(assertion_axiom)

check_common_instances(owl_class1, owl_class2)

Check if two OWL class expressions have a common instance.

Class expressions can be atomic or complex, and it takes longer computation time for the complex ones. Complex classes do not have an IRI. This method is optimised in the way that if there exists an atomic class A, we compute instances for A and compare them against the other class which could be complex.

Difference with check_common_descendants

The inputs of this function are restricted to OWL class expressions. This is because descendant is related to hierarchy and both class and property expressions have a hierarchy, but instance is restricted to classes.

Source code in src/deeponto/onto/ontology.py
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
def check_common_instances(self, owl_class1: OWLClassExpression, owl_class2: OWLClassExpression):
    """Check if two OWL class expressions have a common instance.

    Class expressions can be **atomic or complex**, and it takes longer computation time
    for the complex ones. Complex classes do not have an IRI. This method is optimised
    in the way that if there exists an atomic class `A`, we compute instances for `A` and
    compare them against the other class which could be complex.

    !!! note "Difference with [`check_common_descendants`][deeponto.onto.OntologyReasoner.check_common_descendants]"
        The inputs of this function are restricted to **OWL class expressions**. This is because
        `descendant` is related to hierarchy and both class and property expressions have a hierarchy,
        but `instance` is restricted to classes.
    """

    if not self.has_iri(owl_class1) and not self.has_iri(owl_class2):
        logger.warn("Computing instances for two complex classes is not efficient.")

    # `computed` is the one we compute the instances
    # `compared` is the one we compare `computed`'s descendant one-by-one
    # we set the atomic entity as `computed` for efficiency if there is one
    computed, compared = owl_class1, owl_class2
    if not self.has_iri(owl_class1) and self.has_iri(owl_class2):
        computed, compared = owl_class2, owl_class2

    # for every inferred instance of `computed`, check if it is subsumed by `compared``
    for instance in self.get_instances(computed, direct=False):
        if self.check_instance(instance, compared):
            return True
    return False

check_assumed_disjoint(owl_class1, owl_class2)

Check if two OWL class expressions satisfy the Assumed Disjointness.

Citation

The definition of Assumed Disjointness comes from the paper: Language Model Analysis for Ontology Subsumption Inference.

Assumed Disjointness (Definition)

Two class expressions \(C\) and \(D\) are assumed to be disjoint if they meet the followings:

  1. By adding the disjointness axiom of them into the ontology, \(C\) and \(D\) are still satisfiable.
  2. \(C\) and \(D\) do not have a common descendant (otherwise \(C\) and \(D\) can be satisfiable but their common descendants become the bottom \(\bot\).)

Note that the special case where \(C\) and \(D\) are already disjoint is covered by the first check. The paper also proposed a practical alternative to decide Assumed Disjointness. See check_assumed_disjoint_alternative.

Examples:

Suppose pre-load an ontology onto from the disease ontology file doid.owl.

>>> c1 = onto.get_owl_object("http://purl.obolibrary.org/obo/DOID_4058")
>>> c2 = onto.get_owl_object("http://purl.obolibrary.org/obo/DOID_0001816")
>>> onto.reasoner.check_assumed_disjoint(c1, c2)
[SUCCESSFULLY] Adding the axiom DisjointClasses(<http://purl.obolibrary.org/obo/DOID_0001816> <http://purl.obolibrary.org/obo/DOID_4058>) into the ontology.
[CHECK1 True] input classes are still satisfiable;
[SUCCESSFULLY] Removing the axiom from the ontology.
[CHECK2 False] input classes have NO common descendant.
[PASSED False] assumed disjointness check done.
False
Source code in src/deeponto/onto/ontology.py
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
def check_assumed_disjoint(self, owl_class1: OWLClassExpression, owl_class2: OWLClassExpression):
    r"""Check if two OWL class expressions satisfy the Assumed Disjointness.

    !!! credit "Citation"

        The definition of **Assumed Disjointness** comes from the paper:
        [Language Model Analysis for Ontology Subsumption Inference](https://aclanthology.org/2023.findings-acl.213).

    !!! note "Assumed Disjointness (Definition)"
        Two class expressions $C$ and $D$ are assumed to be disjoint if they meet the followings:

        1. By adding the disjointness axiom of them into the ontology, $C$ and $D$ are **still satisfiable**.
        2. $C$ and $D$ **do not have a common descendant** (otherwise $C$ and $D$ can be satisfiable but their
        common descendants become the bottom $\bot$.)

    Note that the special case where $C$ and $D$ are already disjoint is covered by the first check.
    The paper also proposed a practical alternative to decide Assumed Disjointness.
    See [`check_assumed_disjoint_alternative`][deeponto.onto.OntologyReasoner.check_assumed_disjoint_alternative].

    Examples:
        Suppose pre-load an ontology `onto` from the disease ontology file `doid.owl`.

        ```python
        >>> c1 = onto.get_owl_object("http://purl.obolibrary.org/obo/DOID_4058")
        >>> c2 = onto.get_owl_object("http://purl.obolibrary.org/obo/DOID_0001816")
        >>> onto.reasoner.check_assumed_disjoint(c1, c2)
        [SUCCESSFULLY] Adding the axiom DisjointClasses(<http://purl.obolibrary.org/obo/DOID_0001816> <http://purl.obolibrary.org/obo/DOID_4058>) into the ontology.
        [CHECK1 True] input classes are still satisfiable;
        [SUCCESSFULLY] Removing the axiom from the ontology.
        [CHECK2 False] input classes have NO common descendant.
        [PASSED False] assumed disjointness check done.
        False
        ```
    """
    # banner_message("Check Asssumed Disjointness")

    entity_type = self.get_entity_type(owl_class1)
    assert entity_type == self.get_entity_type(owl_class2)

    # adding the disjointness axiom of `class1`` and `class2``
    disjoint_axiom = getattr(self.owl_data_factory, f"getOWLDisjoint{entity_type}Axiom")([owl_class1, owl_class2])
    undo_change = self.onto.add_axiom(disjoint_axiom, return_undo=True)
    self.load_reasoner(self.reasoner_type)

    # check if they are still satisfiable
    still_satisfiable = self.owl_reasoner.isSatisfiable(owl_class1)
    still_satisfiable = still_satisfiable and self.owl_reasoner.isSatisfiable(owl_class2)
    logger.info(f"[CHECK1 {still_satisfiable}] input classes are still satisfiable;")

    # remove the axiom and re-construct the reasoner
    undo_change_result = self.onto.owl_onto.applyChange(undo_change)
    logger.info(f"[{str(undo_change_result)}] Removing the axiom from the ontology.")
    self.load_reasoner(self.reasoner_type)

    # failing first check, there is no need to do the second.
    if not still_satisfiable:
        logger.info("Failed `satisfiability check`, skip the `common descendant` check.")
        logger.info(f"[PASSED {still_satisfiable}] assumed disjointness check done.")
        return False

    # otherwise, the classes are still satisfiable and we should conduct the second check
    has_common_descendants = self.check_common_descendants(owl_class1, owl_class2)
    logger.info(f"[CHECK2 {not has_common_descendants}] input classes have NO common descendant.")
    logger.info(f"[PASSED {not has_common_descendants}] assumed disjointness check done.")
    return not has_common_descendants

check_assumed_disjoint_alternative(owl_class1, owl_class2, verbose=False)

Check if two OWL class expressions satisfy the Assumed Disjointness.

Paper

The definition of Assumed Disjointness comes from the paper: Language Model Analysis for Ontology Subsumption Inference.

The practical alternative version of check_assumed_disjoint with following conditions:

Assumed Disjointness (Practical Alternative)

Two class expressions \(C\) and \(D\) are assumed to be disjoint if they

  1. do not have a subsumption relationship between them,
  2. do not have a common descendant (in TBox),
  3. do not have a common instance (in ABox).

If all the conditions have been met, then we assume class1 and class2 as disjoint.

Examples:

Suppose pre-load an ontology onto from the disease ontology file doid.owl.

>>> c1 = onto.get_owl_object("http://purl.obolibrary.org/obo/DOID_4058")
>>> c2 = onto.get_owl_object("http://purl.obolibrary.org/obo/DOID_0001816")
>>> onto.reasoner.check_assumed_disjoint(c1, c2, verbose=True)
[CHECK1 True] input classes have NO subsumption relationship;
[CHECK2 False] input classes have NO common descendant;
Failed the `common descendant check`, skip the `common instance` check.
[PASSED False] assumed disjointness check done.
False
In this alternative implementation, we do no need to add and remove axioms which will then be time-saving.

Source code in src/deeponto/onto/ontology.py
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
def check_assumed_disjoint_alternative(
    self, owl_class1: OWLClassExpression, owl_class2: OWLClassExpression, verbose: bool = False
):
    r"""Check if two OWL class expressions satisfy the Assumed Disjointness.

    !!! credit "Paper"

        The definition of **Assumed Disjointness** comes from the paper:
        [Language Model Analysis for Ontology Subsumption Inference](https://aclanthology.org/2023.findings-acl.213).

    The practical alternative version of [`check_assumed_disjoint`][deeponto.onto.OntologyReasoner.check_assumed_disjoint]
    with following conditions:


    !!! note "Assumed Disjointness (Practical Alternative)"
        Two class expressions $C$ and $D$ are assumed to be disjoint if they

        1. **do not** have a **subsumption relationship** between them,
        2. **do not** have a **common descendant** (in TBox),
        3. **do not** have a **common instance** (in ABox).

    If all the conditions have been met, then we assume `class1` and `class2` as disjoint.

    Examples:
        Suppose pre-load an ontology `onto` from the disease ontology file `doid.owl`.

        ```python
        >>> c1 = onto.get_owl_object("http://purl.obolibrary.org/obo/DOID_4058")
        >>> c2 = onto.get_owl_object("http://purl.obolibrary.org/obo/DOID_0001816")
        >>> onto.reasoner.check_assumed_disjoint(c1, c2, verbose=True)
        [CHECK1 True] input classes have NO subsumption relationship;
        [CHECK2 False] input classes have NO common descendant;
        Failed the `common descendant check`, skip the `common instance` check.
        [PASSED False] assumed disjointness check done.
        False
        ```
        In this alternative implementation, we do no need to add and remove axioms which will then
        be time-saving.
    """
    # banner_message("Check Asssumed Disjointness (Alternative)")

    # # Check for entailed disjointness (short-cut)
    # if self.check_disjoint(owl_class1, owl_class2):
    #     print(f"Input classes are already entailed as disjoint.")
    #     return True

    # Check for entailed subsumption,
    # common descendants and common instances

    has_subsumption = self.check_subsumption(owl_class1, owl_class2)
    has_subsumption = has_subsumption or self.check_subsumption(owl_class2, owl_class1)
    if verbose:
        logger.info(f"[CHECK1 {not has_subsumption}] input classes have NO subsumption relationship;")
    if has_subsumption:
        if verbose:
            logger.info("Failed the `subsumption check`, skip the `common descendant` check.")
            logger.info(f"[PASSED {not has_subsumption}] assumed disjointness check done.")
        return False

    has_common_descendants = self.check_common_descendants(owl_class1, owl_class2)
    if verbose:
        logger.info(f"[CHECK2 {not has_common_descendants}] input classes have NO common descendant;")
    if has_common_descendants:
        if verbose:
            logger.info("Failed the `common descendant check`, skip the `common instance` check.")
            logger.info(f"[PASSED {not has_common_descendants}] assumed disjointness check done.")
        return False

    # TODO: `check_common_instances` is still experimental because we have not tested it with ontologies of rich ABox.
    has_common_instances = self.check_common_instances(owl_class1, owl_class2)
    if verbose:
        logger.info(f"[CHECK3 {not has_common_instances}] input classes have NO common instance;")
        logger.info(f"[PASSED {not has_common_instances}] assumed disjointness check done.")
    return not has_common_instances

Last update: April 18, 2023
Created: April 18, 2023
GitHub: @Lawhy   Personal Page: yuanhe.wiki