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
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
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
Each such object has a FOL representation. We can examine this usingto_sentences
:
for s in HasParent.to_sentences():
print(s)
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")
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)
We can examine all axioms the reasoner had to work with:
for s in reasoner.theory.sentences:
print(s)
Consistency Checking
Next we will extend our ontology. We will:
- add transitive predicates for
HasAncestor
andHasDescendant
- add an
Asymmetric
constraint that prevents loops in the graph.
We will use this program:
show("examples/family_v2.py")
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)
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)
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)
reasoner.register(Parent)
model = reasoner.model()
for t in model.iter_retrieve("Parent"):
print(t)
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.