Skip to content

OWL-DL Integration Tutorial

This tutorial is a brief introduction to the OWL-DL integration in typed-logic. Some basic familiarity with OWL-DL is assumed, see the OWL2 Primer for a good introduction.

This tutorial shows how to represent OWL TBoxes (Ontologies) as Python programs, together with representing ABoxes (Individuals and Individual-level facts) as Python objects.

The tutorial is a Jupyter notebook - you can run it interactively in a Jupyter environment, or simply read it as a document.

Initial imports

We will import the necessary classes from the integrations package:

from typedlogic.integrations.frameworks.owldl import (Thing,
                                                      TopDataProperty,
                                                      TopObjectProperty)

A simple class

Next we'll define a simple class Person.

class Person(Thing):
    """A person, living or dead."""

This works just as a normal Python class -- but note that this class is somewhat unusual in that it only has a single attribute, iri, which is the IRI of the individual in the OWL ontology. Actual attributes of a Person, such as their age or list of friends would be represented as instances of properties.

Let's instantiate a person:

AKIRA = Person("Akira")

The only direct property is the IRI. We can examine it:

AKIRA.iri
'Akira'

For conversion to OWL serializations this will need to be an actual IRI - for now we treat it as having an implicit base prefix. The OWL-DL integration is not concerned with serializations, only with logical aspects.

Defining Object Properties

We define object properties by subclassing TopObjectProperty. For example, we can define a property HasParent that relates a person to their parent:

class HasParent(TopObjectProperty):
    """A property that relates a person to their parent."""
    domain = Person
    range = Person

Note that we are setting two class variables here. These are class variables and fixed for all instances of HasParent.

Next we'll start collecting some ObjectPropertyAssertions

BIANCA = Person("Bianca")
a2b = HasParent(AKIRA.iri, BIANCA.iri)
a2b
HasParent(subject='Akira', object='Bianca')

All TopObjectProperty classes are treated as typed-logic Predicate Definitions for binary predicates, where the domain and range are individuals.

We can examine these:

a2b.subject, a2b.object
('Akira', 'Bianca')

Each such object has a FOL representation. We can examine this usingto_sentences:

for s in HasParent.to_sentences():
    print(s)
∀P: None, I: None, J: None : (HasParent(?I, ?J) -> TopObjectProperty(?I, ?J))
∀I: None, J: None : (HasParent(?I, ?J) -> Person(?I))
∀I: None, J: None : (HasParent(?I, ?J) -> Person(?J))

Normally there is no need to examine these assertions, they will be accessed for us by a reasoning mechanism, but for now they can be a useful way of examining the semantics (at the ABox level) of the ontology.

Here we can see that in addition to the trivial inference that all HasParent assertions are of type HasParent, we also see that the subject and object are of type Person (from the domain and range constraints).

Entailments: Defining inverses

Next we will introduce the concept of entailing inverses.

We will make the HasChild property the inverse of HasParent:

class HasChild(TopObjectProperty):
    """A property that relates a person to their child."""
    inverse_of = HasParent

Finding entailments using a reasoner

We have declared some axioms, now it's time to find some entailments. For this we will use a Reasoner object:

from typedlogic.integrations.frameworks.owldl.reasoner import OWLReasoner
reasoner = OWLReasoner()

Here the reasoner is just a wrapper onto an existing Solver (by default, Clingo).

Next we will load an ontology declared as python. For convenience, we have aggregated the declarations into a single file examples/family_v1.py.

from examples.utils import show
show("examples/family_v1.py")
from typedlogic.integrations.frameworks.owldl import (
    Thing,
    TopObjectProperty,
)


class Person(Thing):
    """A person, living or dead."""
    pass

class HasParent(TopObjectProperty):
    """A property that relates a person to their parent."""
    domain = Person
    range = Person

class HasChild(TopObjectProperty):
    """A property that relates a person to their child."""
    inverse_of = HasParent

We can now load this ontology (TBox) into the reasoner:

reasoner.init_from_file("examples/family_v1.py")

Next we will add facts. We will add the single fact we declared earlier.

Note that we could feed in the instance declarations from earlier, but we don't need to here as these will be entailed by the domain and range constraints.

reasoner.add(a2b)

Now we'll find a model and all entailments for the predicate HasChild:

model = reasoner.model()
for t in model.iter_retrieve("HasChild"):
    print(t)
HasChild(Bianca, Akira)

We can examine all axioms the reasoner had to work with:

for s in reasoner.theory.sentences:
    print(s)
∀I: None : (Person(?I) -> Thing(?I))
∀P: None, I: None, J: None : (HasChild(?I, ?J) -> TopObjectProperty(?I, ?J))
∀I: None, J: None : (HasChild(?I, ?J) <-> HasParent(?J, ?I))
∀P: None, I: None, J: None : (HasParent(?I, ?J) -> TopObjectProperty(?I, ?J))
∀I: None, J: None : (HasParent(?I, ?J) -> Person(?I))
∀I: None, J: None : (HasParent(?I, ?J) -> Person(?J))
HasParent(Akira, Bianca)

Consistency Checking

Next we will extend our ontology. We will:

  • add transitive predicates for HasAncestor and HasDescendant
  • add an Asymmetric constraint that prevents loops in the graph.

We will use this program:

show("examples/family_v2.py")
from typedlogic.integrations.frameworks.owldl import Thing, TopObjectProperty, SubObjectPropertyOf


class Person(Thing):
    """A person, living or dead."""
    pass



class HasAncestor(TopObjectProperty):
    """A property that relates a person to their parent."""

class HasDescendant(TopObjectProperty):
    """A property that relates a person to their child."""
    transitive = True
    asymmetric = True

class HasParent(HasAncestor):
    """A property that relates a person to their parent."""
    domain = Person
    range = Person

class HasChild(HasDescendant):
    """A property that relates a person to their child."""
    inverse_of = HasParent

__axioms__ = [ SubObjectPropertyOf(HasParent, HasAncestor), SubObjectPropertyOf(HasChild, HasDescendant) ]

Here we opted to declare the SubObjectPropertyOf axioms directly at the ontology level, rather than "Frame-style" in the predicate definitions. This illustrates how "Manchester-style" and "Functional-style" can be mixed. The Functional-style can be useful for avoiding forward declarations.

We'll now load this ontology into the reasoner:

reasoner.init_from_file("examples/family_v2.py")

Now we can add some facts. First we will add two direct parent-child relationships, that can be chained together to form a grandparent-grandchild relationship:

CARRIE = Person("Carrie")
reasoner.add([HasParent(AKIRA.iri, BIANCA.iri), HasParent(BIANCA.iri, CARRIE.iri)])
model = reasoner.model()
for t in model.iter_retrieve("HasDescendant"):
    print(t)
HasDescendant(Carrie, Bianca)
HasDescendant(Bianca, Akira)
HasDescendant(Carrie, Akira)

assert len(list(model.iter_retrieve("HasDescendant"))) == 3

As expected we have 3 descendant relationships - two direct and one indirect.

Adding a cycle

Now we will see what happens when a cycle is introduced, for example: Alice -> Jie -> Bob -> Alice

reasoner.add(HasParent(CARRIE.iri, AKIRA.iri))
assert reasoner.coherent() is False

The ontology is correctly inferred to be incoherent.

Multiple Models

Next, we will introduce uncertainty into the knowledge base. We will add an assertion that one of the following is true:

  • HasParent(Carrie, Deepa)
  • HasParent(Carrie, Dmitri)

Formally, this introduces multiple alternate models. The term "model" here is used in the sense of model-theoretic semantics, you can think of this as being "possible worlds" where the axioms are true.

Depending on the underlying Solver, we may be able to enumerate all models. Here we will use the default solver, Clingo, which is based in Answer Set Programming (ASP), which supports this.

reasoner.remove(HasParent(CARRIE.iri, AKIRA.iri))
assert reasoner.coherent()
from typedlogic import ExactlyOne
DEEPA = Person("Deepa")
DMITRI = Person("Dmitri")
reasoner.add(ExactlyOne(HasParent(CARRIE.iri, DEEPA.iri), HasParent(CARRIE.iri, DMITRI.iri)))
for n, model in enumerate(reasoner.model_iter()):
    print("Model", n)
    for t in model.iter_retrieve("HasAncestor", AKIRA.iri):
        print(" .. ", t)
Model 0
 ..  HasAncestor(Akira, Bianca)
 ..  HasAncestor(Akira, Carrie)
 ..  HasAncestor(Akira, Dmitri)
Model 1
 ..  HasAncestor(Akira, Bianca)
 ..  HasAncestor(Akira, Carrie)
 ..  HasAncestor(Akira, Deepa)

Classification Example

from typedlogic.integrations.frameworks.owldl import ObjectIntersectionOf

class Parent(Thing):
    """A parent of a person."""
    equivalent_to = ObjectIntersectionOf(Person, HasParent.some(Person))
for s in Parent.to_sentences():
    print(s)
∀I: None : (Parent(?I) -> Thing(?I))
∀I: None : (Parent(?I) <-> (Person(?I)) & (∃(HasParent(?I, ?J)) & (Person(?J))))

reasoner.register(Parent)
model = reasoner.model()
for t in model.iter_retrieve("Parent"):
    print(t)
Parent(Carrie)
Parent(Bianca)
Parent(Akira)

assert len(list(model.iter_retrieve("Parent"))) == 3

Next Steps

Consult the documentation for the various OWL constructs. These all follow the OWL2 Functional Syntax, and for attribute names we follow py-horned-owl as far as possible.