import inspect
import itertools
from abc import ABC
from dataclasses import dataclass
from enum import Enum
from typing import Any, Callable, Iterable, List, Optional, Tuple, Type, TypeAlias, Union
from pyhornedowl import PyIndexedOntology, model
from pyhornedowl.model import (
IRI,
AnnotatedComponent,
AnnotationAssertion,
AsymmetricObjectProperty,
Class,
ClassExpression,
Component,
DatatypeLiteral,
DisjointClasses,
EquivalentClasses,
IrreflexiveObjectProperty,
LanguageLiteral,
ObjectAllValuesFrom,
ObjectIntersectionOf,
ObjectPropertyExpression,
ObjectSomeValuesFrom,
ObjectUnionOf,
ReflexiveObjectProperty,
SimpleLiteral,
SubClassOf,
SubObjectPropertyOf,
SymmetricObjectProperty,
TransitiveObjectProperty,
)
from oaklib.datamodels.vocabulary import (
OWL_ASYMMETRIC_PROPERTY,
OWL_IRREFLEXIVE_PROPERTY,
OWL_REFLEXIVE_PROPERTY,
OWL_SYMMETRIC_PROPERTY,
OWL_TRANSITIVE_PROPERTY,
)
from oaklib.interfaces.basic_ontology_interface import BasicOntologyInterface
from oaklib.types import CURIE
Axiom: TypeAlias = Component
Ontology: TypeAlias = PyIndexedOntology
LITERAL_TYPES = (SimpleLiteral, DatatypeLiteral, LanguageLiteral)
class OwlProfile(Enum):
EL = "EL"
DL = "DL"
OWL_FULL = "OWL-Full"
RL = "RL"
QC = "QL"
@dataclass
class ReasonerConfiguration:
reasoner: Optional[str] = None
reasoner_version: Optional[str] = None
implements_profiles: Optional[List[OwlProfile]] = None
@dataclass
class AxiomFilter:
type: Optional[Type[Any]] = None
about: Optional[Union[CURIE, List[CURIE]]] = None
references: Optional[CURIE] = None
func: Optional[Callable[..., Any]] = None
ontologies: Optional[List[CURIE]] = None
def set_type(self, axiom_type: Union[str, Type[Any]]) -> None:
if isinstance(axiom_type, str):
matches = [obj for n, obj in inspect.getmembers(model) if n == axiom_type]
if len(matches) == 1:
self.type = matches[0]
elif len(matches) == 0:
raise ValueError(f"No such axiom type: {axiom_type}")
else:
raise ValueError(f"Multiple matches {axiom_type} => {matches}")
else:
self.type = axiom_type
[docs]
@dataclass
class OwlInterface(BasicOntologyInterface, ABC):
"""
Presents an ontology as an OWL ontology using the py-horned-owl object model.
We leverage the :ref:`funowl_datamodel`, now backed by py-horned-owl.
Currently there is one implementation, the :ref:`funowl_implementation`.
**Quick examples**
Load a local OWL file through the default selector logic and inspect asserted
OWL axioms:
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
for axiom in oi.subclass_axioms(subclass="GO:0005634"):
print(type(axiom).__name__, axiom)
Filter annotation assertions for a single subject:
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
labels = list(
oi.annotation_assertion_axioms(
subject="GO:0005634",
property="rdfs:label",
)
)
Project lightweight graph-style relationships from OWL axioms:
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
direct = list(oi.relationships(subjects=["GO:0005634"]))
entailed = list(
oi.relationships(
subjects=["GO:0005634"],
include_entailed=True,
)
)
**Reasoning status**
``OwlInterface`` does not currently expose a pluggable OWL reasoner. For the
horned-owl-backed implementation, ``reasoner_configurations()`` returns an
empty list and ``ReasonerConfiguration`` is not accepted by axiom filtering.
Some graph-facing methods can still return a lightweight, implementation-
specific closure when ``include_entailed=True`` is used. Treat that as
projected graph closure over supported OWL patterns, not as complete OWL
reasoning or satisfiability checking.
In future the SqlDatabase implementation will implement this, as well as:
- owlery
- robot/owlapi via py4j
"""
functional_writer: Any = None
def owl_ontology(self) -> Ontology:
raise NotImplementedError
def axioms(self, reasoner: Optional[ReasonerConfiguration] = None) -> Iterable[Axiom]:
raise NotImplementedError
def filter_axioms(
self, conditions: AxiomFilter, reasoner: Optional[ReasonerConfiguration] = None
) -> Iterable[Axiom]:
if reasoner is not None:
raise ValueError
for axiom in self.axioms(reasoner=reasoner):
if self._axiom_matches(axiom, conditions):
yield axiom
def set_axioms(self, axioms: List[Axiom]) -> None:
raise NotImplementedError
[docs]
def subclass_axioms(
self,
subclass: Optional[CURIE] = None,
superclass: Optional[CURIE] = None,
reasoner: Optional[ReasonerConfiguration] = None,
) -> Iterable[SubClassOf]:
"""
Gets all SubClassOf axioms matching criterion
:param subclass: if specified, constrains to axioms where this is the subclass
:param superclass: if specified, constrains to axioms where this is the superclass
:param reasoner:
:return:
**Example**
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
for axiom in oi.subclass_axioms(subclass="GO:0005634"):
print(axiom)
"""
for axiom in self.axioms(reasoner=reasoner):
if isinstance(axiom, SubClassOf):
if subclass is not None and not self._entity_matches(axiom.sub, subclass):
continue
if superclass is not None and not self._entity_matches(axiom.sup, superclass):
continue
yield axiom
[docs]
def equivalence_axioms(
self,
about: Optional[CURIE] = None,
references: Optional[CURIE] = None,
reasoner: Optional[ReasonerConfiguration] = None,
) -> Iterable[EquivalentClasses]:
"""
All EquivalentClasses axioms matching criteria
:param about:
:param references:
:param reasoner:
:return:
**Example**
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
eq_axioms = list(oi.equivalence_axioms(about="GO:0031965"))
"""
return self.filter_axioms(
reasoner=reasoner,
conditions=AxiomFilter(type=EquivalentClasses, about=about, references=references),
)
[docs]
def annotation_assertion_axioms(
self, subject: Optional[CURIE] = None, property: Optional[CURIE] = None, value: Any = None
) -> Iterable[AnnotationAssertion]:
"""
Filters all matching annotation axioms
:param subject:
:param property:
:param value:
:return:
**Example**
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
labels = list(
oi.annotation_assertion_axioms(
subject="GO:0005634",
property="rdfs:label",
)
)
"""
for axiom in self.axioms():
if isinstance(axiom, AnnotationAssertion):
if subject is not None and not self._entity_matches(axiom.subject, subject):
continue
if property is not None and not self._entity_matches(axiom.ann.ap, property):
continue
if value is not None and not self._entity_matches(axiom.ann.av, value):
continue
yield axiom
[docs]
def disjoint_pairs(
self, subjects: Optional[Iterable[CURIE]] = None
) -> Iterable[Tuple[CURIE, CURIE]]:
"""
Gets all disjoint pairs of entities
:param subjects:
:return:
"""
for axiom in self.axioms():
if isinstance(axiom, DisjointClasses):
for c1, c2 in itertools.combinations(axiom.first, 2):
if not subjects or (c1 in subjects or c2 in subjects):
yield c1, c2
[docs]
def is_disjoint(self, subject: CURIE, object: CURIE) -> bool:
"""
Checks if two entities are declared or entailed disjoint.
:param subject:
:param object:
:return:
"""
raise NotImplementedError
def owl_classes(self) -> Iterable[Class]:
raise NotImplementedError
def owl_individuals(self) -> Iterable[Class]:
raise NotImplementedError
[docs]
def is_satisfiable(self, curie: CURIE) -> bool:
"""
Note: this may move to the validation interface
:param curie:
:return:
"""
raise NotImplementedError
[docs]
def reasoner_configurations(self) -> List[ReasonerConfiguration]:
"""
Lists all available reasoner configurations
:return:
**Example**
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
configs = oi.reasoner_configurations()
"""
return []
def entity_iri_to_curie(self, entity: IRI) -> CURIE:
raise NotImplementedError
@staticmethod
def _axiom_component(axiom: Union[Axiom, AnnotatedComponent]) -> Axiom:
if isinstance(axiom, AnnotatedComponent):
return axiom.component
return axiom
@staticmethod
def _entity_iri(entity: Any) -> Optional[IRI]:
if isinstance(entity, IRI):
return entity
first = getattr(entity, "first", None)
if isinstance(first, IRI):
return first
return None
@staticmethod
def _literal_value(entity: Any) -> Optional[str]:
if isinstance(entity, LITERAL_TYPES):
return entity.literal
return None
def _entity_matches(self, entity: Any, curie: Union[CURIE, Any]):
iri = self._entity_iri(entity)
if iri is not None:
return curie == self.entity_iri_to_curie(iri)
literal_value = self._literal_value(entity)
if literal_value is not None:
return curie == literal_value or curie == entity
return False
def _axiom_matches(self, axiom: Axiom, conditions: AxiomFilter) -> bool:
if conditions.type is not None:
if not isinstance(axiom, conditions.type):
return False
if conditions.about is not None:
if isinstance(conditions.about, list):
if not any(e for e in self._axiom_is_about_curies(axiom) if e in conditions.about):
return False
else:
if not any(e for e in self._axiom_is_about_curies(axiom) if e == conditions.about):
return False
if conditions.references is not None:
if not any(
e for e in self._axiom_references_curies(axiom) if e == conditions.references
):
return False
return True
[docs]
def axiom_is_about(self, axiom: Axiom) -> Iterable[IRI]:
"""
Gives an axiom, yield all of the entity IRIs which this axiom is *about*
For example, a SubClassOf axiom is about the IRI in the subClassOf expression
We use a consistent definition of *about* as in the OWLAPI
:param axiom:
:return: entity IRI iterator
"""
if isinstance(axiom, SubClassOf):
for e in self._expression_is_about(axiom.sub):
yield e
elif isinstance(axiom, EquivalentClasses):
for x in axiom.first:
for e in self._expression_is_about(x):
yield e
else:
pass
[docs]
def axiom_references(self, axiom: Axiom) -> Iterable[IRI]:
"""
Gives an axiom, yield all of the entity IRIs which this axiom references
(i.e. entities in the signature)
:param axiom:
:return: entity IRI iterator
"""
if isinstance(axiom, SubClassOf):
for e in self._expression_references(axiom.sub):
yield e
for e in self._expression_references(axiom.sup):
yield e
elif isinstance(axiom, EquivalentClasses) or isinstance(axiom, DisjointClasses):
for x in axiom.first:
for e in self._expression_references(x):
yield e
else:
pass
def _expression_references(
self, ex: Union[ClassExpression, ObjectPropertyExpression]
) -> Iterable[IRI]:
iri = self._entity_iri(ex)
if iri is not None:
yield iri
elif isinstance(ex, ObjectIntersectionOf) or isinstance(ex, ObjectUnionOf):
for x in ex.first:
for r in self._expression_references(x):
yield r
elif isinstance(ex, ObjectSomeValuesFrom) or isinstance(ex, ObjectAllValuesFrom):
for x in self._expression_references(ex.ope):
yield x
for x in self._expression_references(ex.bce):
yield x
def _expression_is_about(self, ex: ClassExpression) -> Iterable[IRI]:
iri = self._entity_iri(ex)
if iri is not None:
yield iri
def _axiom_references_curies(self, axiom: Axiom) -> Iterable[CURIE]:
for e in self.axiom_references(axiom):
yield self.entity_iri_to_curie(e)
def _axiom_is_about_curies(self, axiom: Axiom) -> List[CURIE]:
return [self.entity_iri_to_curie(e) for e in self.axiom_is_about(axiom)]
[docs]
def property_characteristics(self, property: CURIE) -> Iterable[CURIE]:
"""
Gets all property characteristics for a given property
:param property:
:return:
**Example**
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
characteristics = list(oi.property_characteristics("BFO:0000050"))
"""
pc_tuples = [
(TransitiveObjectProperty, OWL_TRANSITIVE_PROPERTY),
(SymmetricObjectProperty, OWL_SYMMETRIC_PROPERTY),
(AsymmetricObjectProperty, OWL_ASYMMETRIC_PROPERTY),
(ReflexiveObjectProperty, OWL_REFLEXIVE_PROPERTY),
(IrreflexiveObjectProperty, OWL_IRREFLEXIVE_PROPERTY),
]
pcs = tuple([pc[0] for pc in pc_tuples])
for axiom in self.axioms():
if isinstance(axiom, pcs):
iri = self._entity_iri(axiom.first)
if iri is None:
continue
for pc, pc_curie in pc_tuples:
if isinstance(axiom, pc) and self.entity_iri_to_curie(iri) == property:
yield pc_curie
[docs]
def transitive_object_properties(self) -> Iterable[CURIE]:
"""
Gets all transitive object properties
:return:
**Example**
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
transitive_properties = list(oi.transitive_object_properties())
"""
for axiom in self.axioms():
if isinstance(axiom, TransitiveObjectProperty):
iri = self._entity_iri(axiom.first)
if iri is not None:
yield self.entity_iri_to_curie(iri)
[docs]
def simple_subproperty_of_chains(self) -> Iterable[Tuple[CURIE, List[CURIE]]]:
"""
Gets all property chains with a named super-property.
:return:
Example
-------
.. code-block:: python
from oaklib import get_adapter
oi = get_adapter("path/to/my-ontology.owl")
chains = list(oi.simple_subproperty_of_chains())
"""
for axiom in self.axioms():
if isinstance(axiom, SubObjectPropertyOf) and isinstance(axiom.sub, list):
super_iri = self._entity_iri(axiom.sup)
if super_iri is None:
continue
chain_iris = [self._entity_iri(p) for p in axiom.sub]
if all(iri is not None for iri in chain_iris):
chain = [self.entity_iri_to_curie(iri) for iri in chain_iris]
yield self.entity_iri_to_curie(super_iri), chain