RDF Studio implements an on-demand OWL 2 RL reasoner using SPARQL-based materialization. The reasoner executes forward-chaining rules as SPARQL INSERT WHERE queries running natively in Oxigraph's Rust engine, then stores inferred triples in a dedicated named graph.
┌─ Frontend (Ontology Editor) ─────────────────────┐ │ │ │ [Reasoner Toggle: ON/OFF] │ │ │ │ │ ▼ │ │ POST /api/v1/reasoner/materialize │ │ │ │ │ ▼ │ │ Inferred edges appear as dashed lines │ │ Inferred superClasses show "Inferred" chip │ │ Stats bar: "496,381 inferences (40s)" │ │ │ └────────────────────────────────────────────────────┘ ┌─ Backend (FastAPI) ──────────────────────────────┐ │ │ │ reasoner_service.py + sparql_materializer.py │ │ 1. Clear inferred graph │ │ 2. Run 8 schema-level OWL 2 RL rules │ │ 3. Run 11 instance-level OWL 2 RL rules │ │ 4. Iterate until fixpoint (no new triples) │ │ 5. Update database config with stats │ │ │ └────────────────────────────────────────────────────┘
The SPARQL materializer won because all rule evaluation runs in Oxigraph's Rust engine — Python only orchestrates the rule loop. This makes it orders of magnitude faster than pure-Python owlrl for large datasets.
Real results from the Northwind ontology (722 asserted → 2338 total triples):
The transitive closure of the class hierarchy. Example:
Customer rdfs:subClassOf Organization
Organization
owl:Thing
Customer rdfs:subClassOf owl:Thing
When a class has an owl:equivalentClass with an intersectionOf, the reasoner decomposes it:
owl:equivalentClass
intersectionOf
Order equivalentClass (hasCustomer exactly 1 Customer) ∩ (hasOrderDetail min 1 OrderDetail) ∩ (hasShipper exactly 1 Shipper)
Order subClassOf
If one direction is asserted, the other is inferred:
manages owl:inverseOf reportsTo
reportsTo owl:inverseOf manages
Given instance data, property chains produce new relationships:
boughtProduct = inverse(hasCustomer) → inverse(belongsToOrder) → hasProduct
CustomerA boughtProduct Chai
The AllDisjointClasses groups are expanded to pairwise disjointness:
AllDisjointClasses
AllDisjointClasses(Product, Order, OrderDetail, Category)
Product disjointWith Order
Product disjointWith OrderDetail
Product disjointWith Category
Order disjointWith OrderDetail
Order disjointWith Category
OrderDetail disjointWith Category
The Model Editor toolbar has two independent reasoning controls:
Click the Reasoner switch to overlay inferred subclass relationships on the graph as dashed lines:
(HermiT DL)
(RL fallback)
The overlay combines:
The [✓] icon button runs an independent DL consistency check, regardless of whether the inferred edges toggle is on or off:
The SPARQL materializer catches explicit contradictions (e.g., an instance of two disjoint classes) but doesn't perform tableau-based unsatisfiability checking.
Workaround: After reasoning, check if any class becomes equivalentClass owl:Nothing — this is the OWL 2 RL signal for unsatisfiability.
equivalentClass owl:Nothing
OWL 2 RL can't reason about which of several possible types an unnamed individual might be.
Why it doesn't matter: This only affects instance-level reasoning with incomplete data. Our reasoner operates on the ontology schema (TBox), not instances.
The SPARQL materializer doesn't support SWRL.
Better alternative: Datalog rules — decidable, portable to all three triplestores, and more powerful in practice. See the "Datalog vs SWRL" topic.
OWL RL (the SPARQL materializer) handles transitive subclass closure, domain/range inference, equivalent class decomposition, and inverse property symmetry. But it cannot infer relationships that require tableau-based reasoning over equivalence classes with existential restrictions — the hallmark of OWL 2 DL.
The Model Editor's Inferred Edges toggle uses HermiT for exactly these cases. Here is a concrete, fully traceable example from the LUBM (Lehigh University Benchmark) ontology — a standard academic benchmark used to compare RDF reasoners.
The LUBM univ-bench ontology defines three relevant classes:
GraduateCourse — explicitly a subclass of Course:
GraduateCourse
Course
:GraduateCourse rdfs:subClassOf :Course .
GraduateStudent — defined with two restrictions, no explicit subClassOf :Student:
GraduateStudent
subClassOf :Student
:GraduateStudent rdfs:subClassOf :Person , [ owl:onProperty :takesCourse ; owl:someValuesFrom :GraduateCourse ] .
Student — defined as an equivalence class (this is the key):
Student
:Student owl:equivalentClass [ owl:intersectionOf ( :Person [ owl:onProperty :takesCourse ; owl:someValuesFrom :Course ] ) ] .
Note the asymmetry: UndergraduateStudent explicitly says rdfs:subClassOf :Student. GraduateStudent does not — that relationship must be inferred.
UndergraduateStudent
rdfs:subClassOf :Student
1. GraduateStudent subClassOf (takesCourse SOME GraduateCourse) ← asserted 2. GraduateCourse subClassOf Course ← asserted 3. ∴ GraduateStudent subClassOf (takesCourse SOME Course) ← inferred (existential restriction propagation via subclass) 4. Student ≡ Person AND (takesCourse SOME Course) ← asserted equivalence 5. GraduateStudent subClassOf Person ← asserted 6. ∴ GraduateStudent subClassOf Student ← INFERRED by HermiT
Step 3 requires propagating an existential restriction through a subclass relationship. Step 6 requires understanding owl:equivalentClass in both directions — "anything that is a Person who takes some Course IS a Student." Neither step is possible with OWL RL alone.
With the LUBM ontology loaded in a database and the Reasoner toggle ON:
UndergraduateStudent → Student
OWL 2 RL's rules operate by pattern matching on existing triples. The rule tables (4–9 in the OWL 2 RL spec) include:
cax-sco
C subClassOf D, x a C → x a D
scm-sco
A subClassOf B, B subClassOf C → A subClassOf C
But there is no RL rule that combines:
someValuesFrom
...to conclude C subClassOf D. That reasoning pattern requires a DL reasoner (HermiT, Pellet, FaCT++) — which is why the Model Editor's inferred edges toggle uses HermiT as its primary engine.
C subClassOf D
This is a fully traceable, real-world example from the Northwind database.Open the Query tab and run the queries yourself to see it live.
The Northwind database has employees typed as nwo:Person, stored in the data named graph. Each employee has properties like foaf:firstName, foaf:lastName, foaf:title — these are standard FOAF vocabulary properties.
nwo:Person
data
foaf:firstName
foaf:lastName
foaf:title
The vocabulary graphs (foaf.ttl, schema-org.ttl, dcterms.ttl) define how standard vocabularies relate:
foaf.ttl
schema-org.ttl
dcterms.ttl
foaf.ttl (vocabulary graph):
foaf:Person a rdfs:Class, owl:Class ; rdfs:label "Person" ; rdfs:subClassOf foaf:Agent . foaf:firstName rdfs:domain foaf:Person . foaf:lastName rdfs:domain foaf:Person . foaf:title rdfs:domain foaf:Person . foaf:Agent owl:equivalentClass dcterms:Agent .
schema-org.ttl (vocabulary graph):
schema:Person a rdfs:Class ; rdfs:label "Person" ; owl:equivalentClass foaf:Person .
The reasoner's prp-dom rule says: if ?x ?p ?y and ?p rdfs:domain ?C, then ?x a ?C.
prp-dom
?x ?p ?y
?p rdfs:domain ?C
?x a ?C
Step by step:
1. employee-1 foaf:firstName "Nancy" (asserted in data) 2. foaf:firstName rdfs:domain foaf:Person (in vocabulary graph) 3. → prp-dom infers: employee-1 a foaf:Person ← STEP 1 4. foaf:Person rdfs:subClassOf foaf:Agent (in vocabulary graph) 5. → cax-sco infers: employee-1 a foaf:Agent ← STEP 2 6. foaf:Agent owl:equivalentClass dcterms:Agent (in vocabulary graph) 7. → scm-eqc infers: employee-1 a dcterms:Agent ← STEP 3 8. schema:Person owl:equivalentClass foaf:Person (in vocabulary graph) 9. → employee-1 a schema:Person ← STEP 4
The result for a single employee:
nwo:Employee
foaf:Person
foaf:Agent
dcterms:Agent
schema:Person
schema:Thing
Verify in SPARQL:
PREFIX nwo: <http://example.org/ontology/northwind#> SELECT DISTINCT ?type WHERE { <http://example.org/northwind/id/employee-1> a ?type }
Data integration works automatically: If you map a CSV column to foaf:firstName, every row immediately becomes a foaf:Person, foaf:Agent, and dcterms:Agent — no extra mapping.
Cross-vocabulary queries work: SELECT ?s WHERE { ?s a dcterms:Agent } returns employees, even though no one declared them as Dublin Core agents.
SELECT ?s WHERE { ?s a dcterms:Agent }
Vocabulary bridging is transparent: Because foaf:Agent ≡ dcterms:Agent and schema:Person ≡ foaf:Person, three major vocabularies are automatically aligned for your data.
foaf:Agent ≡ dcterms:Agent
schema:Person ≡ foaf:Person
The ontology editor doesn't need to show this: foaf:Agent and dcterms:Agent are standard classes from imported vocabularies, not domain classes from the Northwind ontology. The class sidebar intelligently deduplicates them by label so you see one "Agent" pill, not two.
┌──────────────────┐ │ dcterms:Agent │ │ (Dublin Core) │ └────────▲─────────┘ │ owl:equivalentClass ┌────────┴─────────┐ │ foaf:Agent │◄── inferred via cax-sco │ (FOAF) │ └────────▲─────────┘ │ rdfs:subClassOf ┌────────┴─────────┐ │ foaf:Person │◄── inferred via prp-dom │ (FOAF) │ (from foaf:firstName's domain) └────────▲─────────┘ │ prp-dom rule │ ┌──────────────────┐ ┌──────────────────┐ │ employee-1 │ │ employee-9 │ │ foaf:firstName │ │ foaf:firstName │ │ "Nancy" │ │ "Anne" │ └──────────────────┘ └──────────────────┘
This is a single, self-contained example that demonstrates domain inference, subclass propagation, equivalent class symmetry, and cross-vocabulary bridging — all from one asserted foaf:firstName triple.
When you run a DESCRIBE query on an instance (e.g., DESCRIBE nwd:order-10485) or click a node in the graph explorer, you may notice that the result includes rdf:type assertions pointing to blank node IRIs like _:e8f0d10f0ae16a75f14b1c8469eca097:
DESCRIBE
DESCRIBE nwd:order-10485
rdf:type
_:e8f0d10f0ae16a75f14b1c8469eca097
nwd:order-10485 rdf:type nwo:Order ; rdf:type rdfs:Resource ; rdf:type _:e8f0d10f0ae16a75f14b1c8469eca097 ; # ← blank node rdf:type _:a8b4037880db81ff76db25357dea587f ; # ← blank node ...
These blank nodes are OWL restriction classes materialized by the reasoner. They are not errors, and they are not noise in your data.
OWL ontologies use anonymous class expressions to define structural constraints on named classes. For example, the Northwind ontology says:
nwo:Order rdfs:subClassOf [ a owl:Restriction ; owl:onProperty nwo:hasEmployee ; owl:qualifiedCardinality "1" ; owl:onClass nwo:Employee ] .
The [ ... ] syntax creates an anonymous (blank) node — a restriction class with no IRI. This blank node gets a random internal identifier like _:e8f0d10f0ae16a75f14b1c8469eca097.
[ ... ]
The OWL 2 RL reasoner applies the cax-sco rule (subclass type propagation). Because nwo:Order rdfs:subClassOf _:restriction, every Order instance satisfies that restriction, so the reasoner infers:
nwo:Order rdfs:subClassOf _:restriction
nwd:order-10485 rdf:type _:e8f0d10f0ae16a75f14b1c8469eca097 .
This is correct OWL semantics — the instance is a member of that anonymous restriction class.
Each blank node encodes a specific constraint:
owl:onProperty nwo:hasEmployee ; owl:qualifiedCardinality "1"
owl:onProperty nwo:hasCustomer ; owl:qualifiedCardinality "1"
owl:onProperty nwo:hasOrderDetail ; owl:minQualifiedCardinality "1"
owl:onProperty nwo:hasShipper ; owl:qualifiedCardinality "1"
owl:onProperty nwo:orderDate ; owl:cardinality "1"
The owl:intersectionOf blank node (_:a4dacbeedafbe659a96ca14abc735791) bundles several of these restrictions into a single anonymous conjunction: Order ≡ (hasCustomer exactly 1) ∩ (hasOrderDetail min 1) ∩ (hasShipper exactly 1).
owl:intersectionOf
_:a4dacbeedafbe659a96ca14abc735791
Order ≡ (hasCustomer exactly 1) ∩ (hasOrderDetail min 1) ∩ (hasShipper exactly 1)
The Query tab executes SPARQL against all named graphs simultaneously — data, ontology, taxonomy, vocabulary, and any inferred triples. Because blank node restriction classes live in the ontology graph and their instance-level type assertions are materialized into the data graph by the reasoner, a DESCRIBE query will return both asserted data triples and these inferred type triples in the same result.
This is expected and correct. The Query tab is designed to give you the full picture of everything in the store.
Blank nodes appear as grey nodes in the graph canvas when they are returned as part of a DESCRIBE result. Because blank nodes have no IRI, they cannot be navigated to or expanded further:
_:ca691f169...