Skip to content

failure to infer domain across multiple subproperty levels when destination is a named graph #76

@robertmuil

Description

@robertmuil

SKOS has: skos:broader -> skos:broaderTransitive -> skos:semanticRelation
where skos:semanticRelation has domain and range skos:Concept.

An OWL-RL reasoner should infer the domain/range for both skos:broader and skos:broaderTransitive to be skos:Concept.

owlrl correctly infers both of these when operating on the default graph of a Dataset, but it fails to infer to the 2nd level of the property hierarchy (skos:broader) these when the destination of the inferencing is a named graph.

NB: from the below it can be seen that there is a large difference in inferred triples between the two destinations, so this is likely a bigger issue than just domain (or range) propagation across subproperties.

The following tests owlrl with a SKOS-like multi-level subproperty hierarchy in which the domain (or range, I imagine) is specified for the superproperty. This is the output, and the script to produce it is below:

=== Test 1: Default Graph ===

Parse into default: 11 triples in total:
{   '<urn:x-rdflib:default>': 11}
selected domain inferences: 0

Inference into <urn:inferences1>: 139 triples in total:
{   '<urn:inferences1>': 128,
    '<urn:x-rdflib:default>': 11}
selected domain inferences: 2
  0: [<urn:inferences1>   ]           ex:broader -rdfs:domain-> ex:Concept 
  1: [<urn:inferences1>   ] ex:broaderTransitive -rdfs:domain-> ex:Concept 

=== Test 2: Named Graph ===

Parse into named graph <urn:ontology>: 11 triples in total:
{   '<urn:ontology>': 11,
    '<urn:x-rdflib:default>': 0}
selected domain inferences: 0

Inference into <urn:inferences2>: 78 triples in total:
{   '<urn:inferences2>': 67,
    '<urn:ontology>': 11,
    '<urn:x-rdflib:default>': 0}
selected domain inferences: 1
  0: [<urn:inferences2>   ] ex:broaderTransitive -rdfs:domain-> ex:Concept 

=== Summary ===
Default graph infers broaderTransitive domain: ✅ True
Default graph infers broader domain:           ✅ True
Named graph infers broaderTransitive domain:   ✅ True
Named graph infers broader domain:             ❌ False

Script to produce the above:

from pprint import pprint

from owlrl import DeductiveClosure
from owlrl.OWLRL import OWLRL_Semantics
from rdflib import OWL, RDF, RDFS, Dataset, Graph, Namespace, URIRef


EX = Namespace("http://example.org/")


def create_skos_like_ontology():
    """Create SKOS-like multi-level property hierarchy."""
    g = Graph()
    g.bind("ex", EX)

    # Define the top-level property with domain and range
    g.add((EX.semanticRelation, RDF.type, RDF.Property))
    g.add((EX.semanticRelation, RDF.type, OWL.ObjectProperty))
    g.add((EX.semanticRelation, RDFS.domain, EX.Concept))
    g.add((EX.semanticRelation, RDFS.range, EX.Concept))

    # Define middle-level transitive property
    g.add((EX.broaderTransitive, RDF.type, RDF.Property))
    g.add((EX.broaderTransitive, RDF.type, OWL.ObjectProperty))
    g.add((EX.broaderTransitive, RDF.type, OWL.TransitiveProperty))
    g.add((EX.broaderTransitive, RDFS.subPropertyOf, EX.semanticRelation))

    # Define leaf property (like skos:broader)
    g.add((EX.broader, RDF.type, RDF.Property))
    g.add((EX.broader, RDF.type, OWL.ObjectProperty))
    g.add((EX.broader, RDFS.subPropertyOf, EX.broaderTransitive))

    return g


def print_ds(title: str, ds: Dataset) -> None:
    """Print dataset summary and selected inferences."""
    print(f"\n{title}: {len(ds)} triples in total:")
    pprint({g.n3(): ln for g, ln in graph_lengths(ds).items()}, indent=4, width=20)

    domain_assertions = list(ds.quads((EX.broader, RDFS.domain, None))) + list(
        ds.quads((EX.broaderTransitive, RDFS.domain, None))
    )
    print(f"selected domain inferences: {len(domain_assertions)}")
    nm = ds.namespace_manager  # For pretty printing
    for ii, (s, p, o, g) in enumerate(domain_assertions):
        _g = g or URIRef("NONE")
        print(f"  {ii:d}: [{_g.n3(nm):<20}] {s.n3(nm):>20} -{p.n3(nm)}-> {o.n3(nm)} ")


print("=== Test 1: Default Graph ===")
ds1 = Dataset()
ds1.bind("ex", EX)
ds1.parse(data=create_skos_like_ontology().serialize(format="turtle"), format="turtle")
print_ds("Parse into default", ds1)

inferences1 = ds1.graph(URIRef("urn:inferences1"))
DeductiveClosure(OWLRL_Semantics).expand(ds1, inferences1)
print_ds(f"Inference into {inferences1.identifier.n3()}", ds1)


print("\n=== Test 2: Named Graph ===")
ds2 = Dataset()
named_graph = ds2.graph(URIRef("urn:ontology"))
named_graph.parse(
    data=create_skos_like_ontology().serialize(format="turtle"), format="turtle"
)
print_ds(f"Parse into named graph {named_graph.identifier.n3()}", ds2)

inferences2 = ds2.graph(URIRef("urn:inferences2"))
DeductiveClosure(OWLRL_Semantics).expand(named_graph, inferences2)
print_ds(f"Inference into {inferences2.identifier.n3()}", ds2)

print("\n=== Summary ===")
print(
    "Default graph infers broaderTransitive domain: "
    + (
        "✅ True"
        if any(ds1.quads((EX.broaderTransitive, RDFS.domain, None)))
        else "❌ False"
    )
)
print(
    "Default graph infers broader domain:           "
    + ("✅ True" if any(ds1.quads((EX.broader, RDFS.domain, None))) else "❌ False")
)
print(
    "Named graph infers broaderTransitive domain:   "
    + (
        "✅ True"
        if any(ds2.quads((EX.broaderTransitive, RDFS.domain, None)))
        else "❌ False"
    )
)
print(
    "Named graph infers broader domain:             "
    + ("✅ True" if any(ds2.quads((EX.broader, RDFS.domain, None))) else "❌ False")
)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions