Problems with inverse properties in owlready2

133 views Asked by At

I encountered a difficulty: I created an object property in owlready2 (for example: has_parent) and defined it as inverse of another one (for example: has_child). When I relate an individual (“John” for example) to the other (“Kevin” for example) by the first property to create “John has_parent Kevin” as a triple, it is expectable for the inverse triple “Kevin has_child John” to be created after the reasoning process. But it isn’t so. And when I Query “to which individual does relate Kevin by the has_child property” before the reasoning process, it answers: John! But this is not the problem itself. The problem arises when I perform the reasoning process and then save both reasoning results and the main ontology itself: the triple “Kevin has_child John” doesn’t exist in the ontology, nor does in the reasoning results!

Another problem is that when I define a domain and a range for an object property in owlready2, and define another property to be the inverse of the first one, it is expectable for the domain and the range of the inverse property to be set as the range and the domain of the first one respectively after the reasoning. But it isn’t so. And no domain and range is set for the inverse property after reasoning.

I tried to define the inverse properties in another way but it wasn't successful.

heres the code:

from owlready2 import *
onto = get_ontology("http://test.org/Family#")
with onto:

    '''defining base classes'''
    class Human(Thing): pass
    class Man(Human): pass
    class Woman(Human): pass
    AllDisjoint([Man, Woman])

    '''defining properties'''
    class has_couple(Human>>Human): pass
    class has_wife(has_couple, Man>>Woman): pass
    class has_husbend(has_couple):
        inverse=has_wife
    class has_child (Human>>Human): pass
    class has_parent (ObjectProperty):
        inverse=has_child

    '''defining individuals'''
    John = Man("John")
    Kevin = Man("Kevin")

    '''defining relations between individuals'''
    John = Man("John", has_parent=[Kevin])

print(Kevin.has_child)

'''reasoning'''
with onto:
    sync_reasoner_pellet(infer_property_values=True, infer_data_property_values = True)

print(has_husbend.domain)

'''saving'''
onto.save("test.owl", format = "rdfxml")
1

There are 1 answers

3
Marijn On

This answer is only about the inverse property, not about the domain and range.

I could not verify it in the documentation but I assume it is a design decision of owlready2 to implement the semantics of owl:inverseOf, and therefore return additional triples in a query, but to exclude these additional triples from the save() function.

The reason for this is the general principle that you should not add (redundant) triples to a knowledge base that were not present in the original ontology. Adding such triples automatically could cause a large and unwanted increase in file size and memory requirements, for example if there is a definition for an inverse property that applies to a property that each individual has.

Furthermore it is possible that the automatic application of the inverse semantics leads to incorrect facts for certain more complex relations, and you want a human-in-the-loop or some explainability algorithm to check if the results are correct. However, if you just enter the derived facts in the knowledge base then it is no longer possible to distinguish original facts from new facts, so in that case checking results (or even knowing that new facts have been added) is no longer possible.

In summary: this is expected behavior from owlready2. If you want to use the derived facts like Kevin has_child John then you should use owlready2 itself or another engine that implements owl:inverseOf.

However, if you do want all triples in xml, then you can output them yourself using rdflib by looping over all instances, and for each instance looping over all properties.

Code:

from owlready2 import *
from rdflib import Graph, URIRef, Namespace
from rdflib.namespace import RDF, RDFS, OWL

onto = get_ontology("http://test.org/Family#")

with onto:
    '''defining base classes'''
    class Human(Thing): pass
    
    class Man(Human): pass
    
    class Woman(Human): pass

    AllDisjoint([Man, Woman])

    '''defining properties'''
    class has_couple(Human>>Human): pass
    class has_wife(has_couple, Man>>Woman): pass
    class has_husband(has_couple):
        inverse=has_wife
    class has_child (Human>>Human): pass
    class has_parent (ObjectProperty):
        inverse=has_child

'''defining individuals'''
Kevin = Man("Kevin")

'''defining relations between individuals'''
John = Man("John", has_parent=[Kevin])
Mary = Woman("Mary", has_husband=[Kevin])

print("Kevin.has_child:", Kevin.has_child)
print("Kevin.has_wife:", Kevin.has_wife)

fam = Namespace("http://test.org/Family#")
graph = Graph(base="http://test.org/Family")
graph.bind('fam', fam)
A = RDF.type

for person in Human.instances():
    # add a node for this person
    graph.add((URIRef('#'+person.name), A, OWL.NamedIndividual))
    # add all rdf:type nodes (usually just 1)
    for entity_type in person.is_a:
        graph.add((URIRef('#'+person.name), A, URIRef('#'+entity_type.name)))
    # add all person-property-object triples
    for prop in person.get_properties():
        # a property can have multiple values
        for value in prop[person]:
            graph.add((URIRef('#'+person.name), fam.term(prop.name), URIRef('#'+value.name)))

# print xml, don't nest nodes (max_depth=1)
print(graph.serialize(format="pretty-xml", max_depth=1))

This prints the following:

<?xml version="1.0" encoding="utf-8"?>
<rdf:RDF xml:base="http://test.org/Family"
  xmlns:ns1="#"
  xmlns:fam="http://test.org/Family#"
  xmlns:owl="http://www.w3.org/2002/07/owl#"
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
  <owl:NamedIndividual rdf:about="#Mary">
    <rdf:type rdf:resource="#Woman"/>
    <fam:has_husband rdf:resource="#Kevin"/>
  </owl:NamedIndividual>
  <owl:NamedIndividual rdf:about="#Kevin">
    <rdf:type rdf:resource="#Man"/>
    <fam:has_wife rdf:resource="#Mary"/>
    <fam:has_child rdf:resource="#John"/>
  </owl:NamedIndividual>
  <owl:NamedIndividual rdf:about="#John">
    <rdf:type rdf:resource="#Man"/>
    <fam:has_parent rdf:resource="#Kevin"/>
  </owl:NamedIndividual>
</rdf:RDF>

Some notes:

  • I added an individual Mary with has_husband=[Kevin] to test the inverse has_husband/has_wife as well
  • I corrected the spelling "husbend", correct is "husband"
  • I tried to copy as much as possible the xml syntax used by owlready2 itself. The only difference that I could not modify is that owlready2 uses a plain relation <has_child> whereas rdflib with this code produces <fam:has_child> with the alias fam: for the namespace.