Turtle & OWL Correspondence
Dolfin is a source language for OWL ontologies. Compiling a package produces a single Turtle document (plus a companion N3 file for rules, and a companion SPARQL file for queries). The reverse is also supported: an OWL/RDF graph can be recovered back into a Dolfin package. This chapter describes the mapping in both directions.
Two conversions exist:
- Dolfin → Turtle: compile a
.dlfpackage to an OWL graph. - Turtle → Dolfin: reconstruct a
.dlfpackage from an OWL graph.
The mapping is defined at the level of triples, not text. A round trip
Dolfin → Turtle → Dolfin preserves the ontology’s meaning; it does not
guarantee a byte-identical source file (see Round trips).
How IRIs are named
Every concept, property, and individual gets an IRI. IRIs are built from three ingredients:
- a base IRI (the package identity),
- the file path of the declaration inside the package,
- the local name of the entity.
The base IRI
The base IRI comes from the package declaration in package.dlf:
package <http://example.com/animals>:
dolfin_version "1"
version "0.1.0"
If the package is declared with an absolute IRI (<http://…>), that IRI is
the base directly. If it is declared with a bare name (package animals:),
the base is <compiler-base-iri>/animals, where the compiler base IRI is
supplied at compile time (default http://example.org/).
File path → namespace segment
Each ontology file contributes a namespace segment derived from its path
relative to the package root. A file named mammals.dlf produces the namespace
IRI:
http://example.com/animals/mammals#
The filename (without .dlf) becomes the last path segment, and a # fragment
terminator is appended. Nested files nest on disk and in the IRI: a file at
vertebrates/mammals.dlf yields …/animals/vertebrates/mammals#.
The base namespace (declarations that belong to the package root rather
than a sub-file) uses the base IRI directly with no extra path segment. On the
way back, base-namespace entities are written to main.dlf, with
@iri_name <base/> when the base IRI ends in /.
Local names and prefix labels
An entity’s local name is its Dolfin name, appended to its namespace IRI. The
namespace ends with a terminator, # unless an @iri_name says otherwise
(see The namespace terminator):
| Dolfin | IRI |
|---|---|
concept Mammal in mammals.dlf | http://example.com/animals/mammals#Mammal |
concept Animal in the root file | http://example.com/animals#Animal |
concept Foo in a file with @iri_name <http://ex.org/ns/> | http://ex.org/ns/Foo |
In the emitted Turtle, entities are referenced with a prefix label taken
from the filename. Mammal in mammals.dlf is written mammals:Mammal;
entities in the base namespace use the empty prefix, :Animal. Each file’s
namespace IRI is bound with an @prefix declaration at the top of the document.
A Dolfin fact reference keeps the Turtle meaning of the empty prefix:
owner :JohnSmith is emitted as :JohnSmith, in the base namespace, whatever
file it is written in. To reference the JohnSmith fact of data.dlf, write
the bare owner JohnSmith, emitted as data:JohnSmith. No fact lives in the
base namespace (package.dlf only holds the manifest), so the analyzer warns
on every :Name fact reference.
Overriding the IRI with @iri_name
The @iri_name annotation overrides the derived IRI. It has three forms:
# Absolute: replaces the whole namespace IRI for this file
@iri_name <http://animals.kingdom/Mammalian>
# Local segment: replaces only the last path segment
@iri_name "Mammalian" # → http://example.com/animals/Mammalian#
# On a single concept: overrides just that concept's IRI
concept Mammal:
@iri_name <http://animals.kingdom/Mammals>
@iri_name affects only the IRI. The prefix label used to reference the
entity is still derived from the filename, and sibling files and sibling
concepts are unaffected.
The namespace terminator
Entity IRIs are namespace + terminator + local name. The terminator is
decided by the file-level @iri_name:
@iri_name in the file | Namespace used for entities | concept Foo becomes |
|---|---|---|
| none (path-derived) | …/animals/mammals# | …/animals/mammals#Foo |
"Mammalian" (segment) | …/animals/Mammalian# | …/animals/Mammalian#Foo |
<http://ex.org/ns> (no terminator) | http://ex.org/ns# | http://ex.org/ns#Foo |
<http://ex.org/ns#> (ends in #) | http://ex.org/ns# | http://ex.org/ns#Foo |
<http://ex.org/ns/> (ends in /) | http://ex.org/ns/ | http://ex.org/ns/Foo |
A declared IRI that already ends in # or / keeps that terminator; anything
else gets the default #. The @prefix declarations in the Turtle follow the
same rule, so ns:Foo and <http://ex.org/ns/Foo> always agree.
Behaviour change. Older versions always appended
#, so@iri_name <http://ex.org/ns#>producedhttp://ex.org/ns##Foo. It now produceshttp://ex.org/ns#Foo. If you worked around the doubled#, remove the workaround.
A concept-level @iri_name is never modified: its IRI is used verbatim.
File resources (rdfs:isDefinedBy)
To make files (and @iri_name overrides) recoverable, every emitted entity
carries an rdfs:isDefinedBy triple pointing at its file resource — the
file’s canonical, path-derived namespace IRI with no trailing terminator and
no @iri_name override applied:
mammals:Mammal rdf:type rdfs:Class
; skos:prefLabel "Mammal"
; rdfs:isDefinedBy <http://example.com/animals/mammals> .
Each non-base file also declares its file resource and links it to the package ontology, so the package’s file set is explicit:
<http://example.com/animals/mammals> rdfs:isDefinedBy <http://example.com/animals> .
Base-namespace entities are defined by the package IRI itself (their file
resource equals the owl:Ontology subject), so no self-loop is emitted.
Because the file resource is override-free, the reverse direction uses it to
(1) assign each entity to its real file even when @iri_name rewrote the entity
IRIs, and (2) detect that an override was used — by comparing the entity’s
actual IRI against <file-resource>#<local>.
Declaration correspondence
The following table summarises the core mapping from Dolfin declarations to OWL triples.
| Dolfin | OWL / RDF |
|---|---|
concept C | C rdf:type rdfs:Class |
concept C: sub P | C rdfs:subClassOf P |
has p: T (T primitive) | p rdf:type owl:DatatypeProperty ; rdfs:domain C ; rdfs:range xsd:… |
has p: T (T a concept) | p rdf:type owl:ObjectProperty ; rdfs:domain C ; rdfs:range T |
enum / one of (a, b, …) | owl:equivalentClass [ owl:oneOf ( … ) ] + one owl:NamedIndividual per variant |
fact block | owl:NamedIndividual with rdf:type and property-value triples (the ABox) |
A field name used by several concepts of one file is a single property: its
rdfs:domain is [ a owl:Class ; owl:unionOf ( C1 C2 … ) ] (and its range the
union of their ranges when they differ), so an instance of C1 is never
inferred to be a C2.
Package metadata
| Dolfin manifest field | OWL triple on the owl:Ontology node |
|---|---|
version | owl:versionInfo |
description | rdfs:comment |
author | dc:creator (dc: = http://purl.org/dc/elements/1.1/) |
Primitive types
| Dolfin | XSD datatype |
|---|---|
string | xsd:string |
int | xsd:integer |
float | xsd:double |
boolean | xsd:boolean |
date | xsd:date |
date_time | xsd:dateTime |
time | xsd:time |
duration | xsd:duration |
Quantities
A quantity(...) value is not a primitive type. It compiles to a typed literal
whose datatype is a canonical unit IRI ("45"^^<https://dolfin.dev/unit/kg>),
accompanied by a once-per-unit definition block that states the unit’s
coefficient, dimension, and symbol with dq: predicates. See
Units & Quantities for the full mapping.
Comments and labels
A declaration’s name, leading comment and #@ glossary: annotation become
RDFS / SKOS annotations:
| Source | RDFS / SKOS |
|---|---|
declaration name (or #@ glossary: pref_label[@xx]=) | skos:prefLabel |
leading comment text (# … / #xx> …) | rdfs:comment |
#@ glossary: definition= / definition@xx= | skos:definition |
#@ glossary: alt_label[@xx]= | skos:altLabel |
#@ glossary: scope_note= | skos:scopeNote |
A #xx> prefix on a leading comment line gives the literal its language tag
(#fr> … → rdfs:comment "…"@fr, one triple per language); a plain # … line
yields an untagged literal. Likewise definition@xx= yields a tagged
skos:definition and a plain definition= an untagged one.
Cardinality
A has field’s cardinality becomes an OWL restriction on the owning class:
| Dolfin cardinality | OWL restriction on the class |
|---|---|
one | owl:equivalentClass [ owl:cardinality 1 ], property is owl:FunctionalProperty |
optional | owl:equivalentClass [ owl:maxCardinality 1 ], property is owl:FunctionalProperty |
some | owl:subClassOf [ owl:minCardinality 1 ] |
exactly n | owl:subClassOf [ owl:cardinality n ] |
n to m | owl:subClassOf [ owl:minCardinality n ; owl:maxCardinality m ] |
any (default) | no restriction |
Property axioms
Axioms declared on a property map to OWL property characteristics:
| Dolfin | OWL |
|---|---|
symmetric | rdf:type owl:SymmetricProperty |
reflexive | rdf:type owl:ReflexiveProperty |
transitive | rdf:type owl:TransitiveProperty |
sub q | rdfs:subPropertyOf q |
inverse of q | owl:inverseOf q |
equivalent to q | owl:equivalentProperty q |
equivalent to ^q | owl:equivalentProperty [ owl:inverseOf q ] |
equivalent to a . b (chain) | owl:propertyChainAxiom ( a b ) via an rdfs:subPropertyOf node |
equivalent to q+ | transitive + propertyChainAxiom ( q self ) |
equivalent to q* | transitive + reflexive + propertyChainAxiom ( q self ) |
Rules
Rules do not fit the OWL description-logic fragment. They compile to N3
(Notation3) { … } => { … } implications, written to a companion N3 document
alongside the Turtle. The N3 file shares the same @prefix bindings as the
Turtle so the two are consistent.
Comparisons
A comparison in a match: constraint
block compiles to an N3 builtin. Which family is chosen depends on what is
compared: the literal on the right, or, against a variable, the declared
range of the property whose value is constrained.
| Compared values | = | != | < <= > >= |
|---|---|---|---|
| Numbers, dates, times, durations | math:equalTo | math:notEqualTo | math:lessThan, math:notGreaterThan, math:greaterThan, math:notLessThan |
Quantities (quantity(...), or a dimension range such as unit.Mass) | dq:equalTo | dq:notEqualTo | dq:lessThan, dq:lessThanOrEqual, dq:greaterThan, dq:greaterThanOrEqual |
| Strings, booleans, IRIs, individuals of a concept | log:equalTo | log:notEqualTo | math: as for numbers |
?p age [ = 18 ] # ?_v1 math:equalTo "18"^^xsd:integer .
?p name [ = "Bob" ] # ?_v2 log:equalTo "Bob" .
?q friend [ != ?f ] # ?_v3 log:notEqualTo ?f .
log:equalTo is term identity: "18"^^xsd:integer and "18.0"^^xsd:decimal
are different terms, and "Bob" differs from "Bob"@en. math:equalTo
compares values, so it only holds between numbers, dates or times. A variable
whose type cannot be inferred, or a float property holding quantities, is
compared with math:.
Quantifiers
Quantifiers use the N3 log: and list: builtins,
evaluated in the reasoning scope ?_scope:
| Dolfin | N3 |
|---|---|
none ?v [C]: P | ?_scope log:notIncludes { ?v C . P } . |
all ?v [C]: P | ( { ?v C } { P } ) log:forAllIn ?_scope . |
at least N ?v [C]: P | ( ?v { ?v C . P } ?L ) log:collectAllIn ?_scope . ?L list:length ?n . ?n math:notLessThan N . |
at most N, exactly N, between N, M | same, with math:notGreaterThan, math:equalTo, or both bounds |
any ?v [C]: P | C and P inline |
A rule is never emitted with a weaker condition than its source. When a
quantifier has no faithful encoding, the whole rule is written as a comment
with the reason, and raft build prints a warning:
all ?vwithout a[ … ]constraint: there is no domain for?vto range over.- A count whose sub-patterns bind variables other than
?vand the rule’s own (e.g.?r distance ?d):log:collectAllInwould count one?vonce per?d.
raft build --emit n3-rules (for retox) follows the same rules: quantified
rules are emitted live, and retox’s ReasoningDataset evaluates them in strata.
Loading fails when a quantifier depends on its own conclusion (a cycle through a
quantifier), or when a quantified rule derives rdfs:subClassOf,
rdfs:subPropertyOf or owl:sameAs.
Round trips
In Agrafe this direction is the Import from Turtle feature.
Reconstructing Dolfin from an OWL graph works at the triple level: it reads the
owl:Ontology node, the classes, properties, restrictions, individuals, and
SKOS/dc/rdfs metadata, and rebuilds the package layout from rdfs:isDefinedBy
(falling back to the entity IRIs when it is absent).
A Dolfin → Turtle → Dolfin round trip preserves meaning but normalises
form. Keep in mind:
- Files are rebuilt from
rdfs:isDefinedBy. Each entity’s file resource names its file (…/animals/mammals→mammals.dlf); base-namespace entities land inmain.dlf. Turtle that carries nordfs:isDefinedBy(e.g. graphs not produced by this compiler) falls back to deriving the file from the entity IRI structure. @iri_nameoverrides are reconstructed. When an entity’s IRI deviates from its file resource, the override is recovered: a file-wide, uniform deviation becomes a file-level@iri_name <ns>(or<ns/>when every entity shares a/-terminated namespace;#wins if both occur); an isolated one becomes a concept-level@iri_name <iri>. The recovered directive uses the absolute form (a@iri_name "segment"is recovered as the equivalent absolute IRI).- Entities outside the package base are dropped. Only entities defined by
the package (via
rdfs:isDefinedBy, or IRI-under-base in the fallback) are reconstructed. Imported/external vocabulary is treated as external and skipped. dolfin_versionis not carried in the graph and defaults to"1"on the way back.- Comments and formatting are not preserved. Only the structured
annotations that became triples (
rdfs:comment, SKOS labels,dc:creator) are recovered.