Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jbarrasa/goingmeta/llms.txt

Use this file to discover all available pages before exploring further.

Session 36 (Season 2, Episode 9 — May 2025) closes the Season 2 arc by addressing the structural challenge that emerges once you have more than one ontology: they partially overlap, use different naming conventions, define the same concept in slightly different ways, and may have conflicting property constraints. This session presents a catalogue of structural reconciliation patterns — from identifying equivalent classes using owl:equivalentClass to merging sub-hierarchies and handling property definition conflicts — all implemented programmatically using Python and RDFLib.

Watch the Recording

Full live-stream replay on YouTube

Session Code

RDFLib reconciliation notebooks and scripts

Why Reconciliation Is Necessary

In real ontology management scenarios — especially after the agentic catalog-building workflow from Session 35 — you accumulate multiple ontologies that describe overlapping domains. A Person class in one ontology may be the same as a Human class in another. A worksFor property in one ontology may be equivalent to employedBy in another. Before these ontologies can serve as a unified schema for KG construction, the overlaps and conflicts must be resolved. Reconciliation operates at three levels:
  1. Class equivalence — identifying classes across ontologies that describe the same concept
  2. Hierarchy alignment — deciding how sub-class trees from different ontologies should be merged
  3. Property conflict resolution — handling cases where two ontologies define the same or equivalent properties differently

Pattern 1: Declaring Equivalent Classes

The simplest reconciliation pattern is declaring owl:equivalentClass between concepts in two ontologies. This preserves both ontologies’ terms while asserting that they refer to the same real-world category.
from rdflib import Graph, URIRef, Namespace
from rdflib.namespace import OWL, RDF, RDFS

# Load both ontologies
g1 = Graph()
g1.parse("ontologies/sales.ttl")

g2 = Graph()
g2.parse("ontologies/insurance.ttl")

# Create a reconciliation graph
r = Graph()

SALES = Namespace("http://example.org/sales#")
INS = Namespace("http://example.org/insurance#")

# Assert that Person in sales ontology is equivalent to Customer in insurance ontology
r.add((SALES.Person, OWL.equivalentClass, INS.Customer))
r.add((SALES.Contract, OWL.equivalentClass, INS.Policy))

# Serialize the reconciliation as a separate alignment file
r.serialize("reconciliation/sales-insurance-alignment.ttl", format="ttl")
Storing the equivalence declarations in a separate alignment file rather than modifying either source ontology keeps the originals intact. The alignment file can be loaded alongside both ontologies whenever a unified view is needed.

Pattern 2: Merging Sub-Hierarchies

When two ontologies define overlapping but non-identical class hierarchies, reconciliation may require inserting bridging classes or redirecting rdfs:subClassOf relationships.
from rdflib import Graph, URIRef
from rdflib.namespace import RDFS, OWL, RDF

def find_leaf_classes(g):
    """Return all classes that have no subclasses in the given graph."""
    all_classes = set(g.subjects(RDF.type, OWL.Class))
    classes_with_subclasses = set(g.objects(None, RDFS.subClassOf))
    return all_classes - classes_with_subclasses

def get_class_hierarchy(g, cls):
    """Return the ancestor chain for a class, from cls up to the root."""
    ancestors = []
    current = cls
    while True:
        parents = list(g.objects(current, RDFS.subClassOf))
        if not parents:
            break
        current = parents[0]
        ancestors.append(current)
    return ancestors

def find_common_ancestor(g1, g2, cls1, cls2):
    """
    Find labels shared between the ancestor chains of cls1 (in g1)
    and cls2 (in g2), indicating a potential alignment point.
    """
    chain1 = {str(c) for c in get_class_hierarchy(g1, cls1)}
    chain2 = {str(c) for c in get_class_hierarchy(g2, cls2)}
    return chain1 & chain2
When a common ancestor is identified, the reconciliation graph can declare the bridge:
from rdflib import Graph, URIRef, Namespace
from rdflib.namespace import RDFS, OWL

MERGED = Namespace("http://example.org/merged#")

r = Graph()
r.bind("merged", MERGED)

# Bridge: InsurancePerson and SalesPerson both subclass the merged LegalEntity
r.add((URIRef("http://example.org/insurance#Person"), RDFS.subClassOf, MERGED.LegalEntity))
r.add((URIRef("http://example.org/sales#Person"), RDFS.subClassOf, MERGED.LegalEntity))
r.add((MERGED.LegalEntity, RDF.type, OWL.Class))

Pattern 3: Detecting Equivalent Properties

Properties are often reconciled by function: two properties with different names may have the same domain, range, and semantic intent. RDFLib’s SPARQL interface makes it straightforward to query for candidates:
from rdflib import Graph, ConjunctiveGraph
from rdflib.namespace import RDF, RDFS, OWL

def find_property_candidates(g1, g2):
    """
    Find pairs of properties from g1 and g2 that share the same
    (local name) or compatible (domain, range) signatures.
    """
    props1 = set(g1.subjects(RDF.type, OWL.ObjectProperty))
    props2 = set(g2.subjects(RDF.type, OWL.ObjectProperty))

    candidates = []
    for p1 in props1:
        p1_local = p1.split('#')[-1].split('/')[-1].lower()
        for p2 in props2:
            p2_local = p2.split('#')[-1].split('/')[-1].lower()
            # Check for matching local names (case-insensitive)
            if p1_local == p2_local:
                candidates.append((p1, p2, "name_match"))
                continue
            # Check for matching domain and range
            p1_doms = set(g1.objects(p1, RDFS.domain))
            p2_doms = set(g2.objects(p2, RDFS.domain))
            p1_rans = set(g1.objects(p1, RDFS.range))
            p2_rans = set(g2.objects(p2, RDFS.range))
            if p1_doms & p2_doms and p1_rans & p2_rans:
                candidates.append((p1, p2, "domain_range_match"))
    return candidates
Once candidates are identified, they can be declared equivalent using owl:equivalentProperty:
from rdflib import Graph, URIRef
from rdflib.namespace import OWL

r = Graph()
r.add((
    URIRef("http://example.org/sales#worksFor"),
    OWL.equivalentProperty,
    URIRef("http://example.org/hr#employedBy")
))

Pattern 4: Handling Conflicting Property Definitions

Conflicts arise when two ontologies define what appears to be the same property but with incompatible domains or ranges. The reconciliation strategy depends on the nature of the conflict:
Conflict typeReconciliation approach
Same name, different domainRename one property or introduce a disjointness assertion
Same name, compatible rangeDeclare owl:equivalentProperty and align the ranges with a broader shared class
Incompatible cardinality constraintsKeep both constraints; document the discrepancy in rdfs:comment
One property is a specialization of the otherDeclare rdfs:subPropertyOf rather than equivalence
from rdflib import Graph, URIRef, Literal
from rdflib.namespace import OWL, RDFS, RDF

r = Graph()

SALES = "http://example.org/sales#"
HR = "http://example.org/hr#"

# worksFor is a subproperty of the more general employs
r.add((URIRef(SALES + "worksFor"), RDFS.subPropertyOf, URIRef(HR + "hasEmploymentRelation")))

# Document the conflict for future reviewers
r.add((
    URIRef(SALES + "worksFor"),
    RDFS.comment,
    Literal("Narrower than hr:hasEmploymentRelation — applies only to full-time employees")
))

Building a Merged View

Once the alignment graph is populated, a ConjunctiveGraph (or a simple merge operation) assembles a unified view that can be passed to getSchemaFromOnto():
from rdflib import ConjunctiveGraph, Graph

def build_merged_view(onto_paths: list, alignment_path: str) -> Graph:
    """
    Load multiple ontologies and an alignment file into a single merged graph.
    """
    merged = Graph()
    for path in onto_paths:
        g = Graph()
        g.parse(path)
        for triple in g:
            merged.add(triple)

    alignment = Graph()
    alignment.parse(alignment_path)
    for triple in alignment:
        merged.add(triple)

    return merged
# Usage
merged = build_merged_view(
    onto_paths=["ontologies/sales.ttl", "ontologies/insurance.ttl"],
    alignment_path="reconciliation/sales-insurance-alignment.ttl"
)

# The merged graph can now be used for schema extraction
from onto_utils import getSchemaFromOnto
schema = getSchemaFromOnto(merged)

Reconciliation Workflow Summary

1

Load and compare ontologies

Parse both ontologies with RDFLib. Use SPARQL queries or Python iteration to find overlapping class names, shared local names, and compatible domain/range pairs.
2

Identify equivalences

For each pair of matching concepts, decide between owl:equivalentClass, owl:equivalentProperty, or rdfs:subClassOf/rdfs:subPropertyOf. Document your reasoning with rdfs:comment.
3

Resolve conflicts

For incompatible definitions, choose a reconciliation strategy: renaming, specialization, or a neutral bridging class/property. Record the decision in the alignment file.
4

Build the merged view

Use build_merged_view() to produce a single RDFLib Graph that includes both source ontologies and the alignment. Pass it to getSchemaFromOnto() for downstream KG construction.
5

Validate with SHACL

After merging, run SHACL validation (as covered in Session 28) against any existing graph data to confirm that the reconciled schema does not introduce new violations.

Key Takeaways

Alignment files, not mutations

Store reconciliation decisions in a separate alignment Turtle file. Never modify source ontologies — the alignment is the artefact that records the design decision.

RDFLib as the reconciliation engine

Python + RDFLib provides full programmatic access to OWL axioms, SPARQL queries over multiple graphs, and straightforward triple-level manipulation for alignment authoring.

owl:equivalentClass vs rdfs:subClassOf

Choose equivalence when two classes describe exactly the same concept across ontologies. Choose subClassOf when one is a specialization of the other. The distinction matters for downstream reasoning.

Integration with agentic workflows

The reconciliation patterns here complement the ontology catalog from Session 35 — when the catalog holds multiple overlapping ontologies, reconciliation produces the unified schema for extraction.

Build docs developers (and LLMs) love