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 |
variant Herbivore of concept Diet: one of: in mammals.dlf | http://example.com/animals/mammals#Diet_Herbivore |
fact Biscuit a Dog in data.dlf | http://example.com/animals/data#Biscuit |
A one of: variant is named <Concept>_<Variant> in the namespace of the
concept’s file, so two closed concepts of one file can share a variant name. A
concept’s own @iri_name does not change its variants’ IRIs; a file-level one
does, as for every entity of the file. A fact is named by its id in the
namespace of the file that declares it, so facts written in data.dlf are
<package>/data#<id>; a fact id written prefixed (ex:Biscuit) or as an IRI
is used as written.
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 these 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 or property: overrides just that entity's IRI
concept Mammal:
@iri_name <http://animals.kingdom/Mammals>
property occupiesTemporalRegion
@iri_name <http://purl.obolibrary.org/obo/BFO_0000199>
# Under a `has` line: the field's property IRI, for every `has` of that name in the file
concept Truc:
has dependence: Dep
@iri_name <https://example.com/truc_dep>
@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. Every reference to a concept or property with its own
@iri_name (sub, inverse of, domain, with p: C, fact properties, from
any file of the package) is written with that IRI; the Dolfin name only
survives as its skos:prefLabel / rdfs:label.
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:label "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 |
#@ owl + concept C | C rdf:type owl:Class |
concept C: sub P | C rdfs:subClassOf P |
concept C: equivalent to P | C owl:equivalentClass P, one statement per type and per line |
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 |
(A or B) type | [ a owl:Class ; owl:unionOf ( A B ) ] |
(A and B) type | [ a owl:Class ; owl:intersectionOf ( A B ) ] |
not A type | [ a owl:Class ; owl:complementOf A ] |
with p: C type | [ a owl:Restriction ; owl:onProperty p ; owl:someValuesFrom C ], see Restrictions |
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) |
Class expressions nest, in rdfs:domain and rdfs:range alike:
has z: (X or (Y and not T)) gives
rdfs:range [ a owl:Class ; owl:unionOf ( X [ a owl:Class ; owl:intersectionOf ( Y [ a owl:Class ; owl:complementOf T ] ) ] ) ].
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.
A concept is rdfs:Class unless its leading comment block carries the bare
#@ owl marker, which makes it owl:Class.
Importing Turtle sets the marker on every class typed owl:Class (including
one typed both owl:Class and rdfs:Class), so the type round-trips.
Restrictions: with
Each with is one owl:Restriction blank node; with
joined to a type is an owl:intersectionOf.
| Dolfin | OWL |
|---|---|
with p: C | [ a owl:Restriction ; owl:onProperty p ; owl:someValuesFrom C ] |
with p: only C | [ a owl:Restriction ; owl:onProperty p ; owl:allValuesFrom C ] |
with p: 2 C | [ a owl:Restriction ; owl:onProperty p ; owl:qualifiedCardinality "2"^^xsd:nonNegativeInteger ; owl:onClass C ] |
with p: at least 2 C | … owl:minQualifiedCardinality "2"^^xsd:nonNegativeInteger ; owl:onClass C |
with p: at most 3 C | … owl:maxQualifiedCardinality "3"^^xsd:nonNegativeInteger ; owl:onClass C |
with p: 1..4 C | [ a owl:Class ; owl:intersectionOf ( min 1 restriction max 4 restriction ) ] |
with p: = x | [ a owl:Restriction ; owl:onProperty p ; owl:hasValue x ] |
with ^p: C | [ a owl:Restriction ; owl:onProperty [ owl:inverseOf p ] ; owl:someValuesFrom C ] |
T with p: C, or T + a block of with lines | [ a owl:Class ; owl:intersectionOf ( T restriction … ) ] |
N..M is two restrictions because OWL 2 allows a single cardinality per
restriction. A nested block narrows the filler into an intersection:
concept Act:
sub Process
with occupiesTemporalRegion: TemporalRegion
with occurrentPartOf: TemporalInterval
gives
:Act rdfs:subClassOf [ a owl:Class ; owl:intersectionOf ( :Process
[ a owl:Restriction ; owl:onProperty :occupiesTemporalRegion ;
owl:someValuesFrom [ a owl:Class ; owl:intersectionOf ( :TemporalRegion
[ a owl:Restriction ; owl:onProperty :occurrentPartOf ;
owl:someValuesFrom :TemporalInterval ] ) ] ] ) ] .
Defined concepts: equivalent to
sub X and equivalent to X render X the same way; only the predicate
changes. Under either line, a with block gives the intersection:
concept Parent:
equivalent to Person
with hasChild: Person
gives
:Parent owl:equivalentClass [ a owl:Class ; owl:intersectionOf ( :Person
[ a owl:Restriction ; owl:onProperty :hasChild ;
owl:someValuesFrom :Person ] ) ] .
With sub Person in place of equivalent to Person, the same expression is
the object of rdfs:subClassOf.
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:float |
boolean | xsd:boolean |
date | xsd:date |
date_time | xsd:dateTime |
time | xsd:time |
duration | xsd:duration |
An int value is always written "42"^^xsd:integer, never xsd:int: xsd:int
is the 32-bit subtype of xsd:integer, and Dolfin does not bound int.
Quantities
Quantities use the QUDT vocabulary
(qudt: = http://qudt.org/schema/qudt/,
quantitykind: = http://qudt.org/vocab/quantitykind/, unit IRIs under
http://qudt.org/vocab/unit/):
| Dolfin | Turtle |
|---|---|
has weight: mass | :weight a owl:DatatypeProperty ; rdfs:range quantitykind:Mass |
range mass used | quantitykind:Mass a qudt:QuantityKind, rdfs:Datatype . |
has nap: duration | :nap a owl:DatatypeProperty ; rdfs:range xsd:duration |
weight quantity(3 kg) | :weight "3"^^<http://qudt.org/vocab/unit/KiloGM> |
nap quantity(20 min), nap duration(20 min) | :nap "PT20M"^^xsd:dayTimeDuration |
| a unit used in the data | <unit> a qudt:Unit, rdfs:Datatype ; qudt:conversionMultiplier … ; qudt:hasDimensionVector <qkdv> ; qudt:symbol "kg" ; qudt:hasQuantityKind quantitykind:Mass . |
a unit QUDT lacks (unitdef, km/min) | datatype <namespace>unit/<canonical>, same qudt: triples |
concept Mass (legacy) | nothing: no rdfs:Class for a built-in dimension |
Unit IRIs are written in full, not as unit: prefixed names. The qudt: and
quantitykind: prefixes are declared only when the output has quantities. See
Units & Quantities for the details.
Comments and labels
A declaration’s name, leading comment and #@ glossary: annotation become
RDFS / SKOS annotations:
| Source | RDFS / SKOS |
|---|---|
#@ glossary: pref_label[@xx]= (alias prefLabel) | skos:prefLabel + rdfs:label |
#@ glossary: label[@xx]= | rdfs:label (the first one of a language without pref_label also skos:prefLabel) |
declaration name, when no label/pref_label is given | skos:prefLabel + rdfs:label (untagged) |
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.
This holds for a has line as for a concept: a plain # comment directly
above has describes that field, as its rdfs:comment. Only a #@ glossary:
definition argument writes skos:definition; a comment never does.
concept Recipe:
# How long the recipe takes, preparation included.
has total_time: duration
→ :total_time rdfs:comment "How long the recipe takes, preparation included.",
and no skos:definition.
pref_label and label, per language
Labels are resolved per language; untagged labels form their own language.
Every pref_label and label becomes an rdfs:label. In each language, one of
them is also the skos:prefLabel:
pref_label@xxis the preferred label ofxx. Write at most one per language; an extra one is kept as a plainrdfs:label.- Without a
pref_label@xx, the firstlabel@xxis promoted to preferred label; later ones stay plainrdfs:label. - With a
pref_label@xx, everylabel@xxstays plainrdfs:label, even one written before it. - With no
labeland nopref_labelat all, the declaration name is the untagged preferred label. A tagged label alone is enough to suppress it.
This applies to concepts, has fields, properties and enum members (a
#@ glossary: block right above the member line).
#@ glossary:
#@ label@en Kitty
#@ pref_label@en Cat
#@ label@fr Chat
#@ label@fr Minou
concept Cat
:Cat rdf:type rdfs:Class
; skos:prefLabel "Cat"@en # pref_label@en
; rdfs:label "Cat"@en
; rdfs:label "Kitty"@en # en has a pref_label: plain label
; skos:prefLabel "Chat"@fr # first label@fr, promoted
; rdfs:label "Chat"@fr
; rdfs:label "Minou"@fr
…
concept Dog with no glossary labels gets skos:prefLabel "Dog" and
rdfs:label "Dog".
Back from Turtle it reverses: each skos:prefLabel is a pref_label[@xx],
each other rdfs:label a label[@xx]. An untagged label equal to the
declaration name is left out when it is the only label, since writing it
again regenerates it. A fact gets no label from its id, so on a fact every
label is kept: its glossary and doc comment come back above the fact line.
Cardinality
A has field’s cardinality becomes an OWL restriction on the owning class:
| Dolfin cardinality | OWL restriction on the class |
|---|---|
one (1..1) | owl:equivalentClass [ owl:cardinality 1 ], property is owl:FunctionalProperty |
at most 1 (0..1) | owl:subClassOf [ owl:minCardinality 0 ; owl:maxCardinality 1 ], property is owl:FunctionalProperty |
some (1..*) | owl:subClassOf [ owl:minCardinality 1 ] |
n (exactly n) | owl:subClassOf [ owl:cardinality n ] |
n..m, at least n, at most n | owl:subClassOf [ owl:minCardinality n ; owl:maxCardinality m ] (no max when unbounded) |
optional (0..1) | owl:subClassOf [ owl:maxCardinality 1 ] |
any (default, 0..*) | no restriction |
A property is owl:FunctionalProperty exactly when the maximum is 1: one
and at most 1 (also 1, n..1), never optional: it is 0..1 too, but marks a
field that may be missing, not a functional property. A field name shared by several concepts is functional only when
every one of them has maximum 1.
A standalone property p: D -> R reads its cardinalities the same way: a
maximum of 1 before the range gives owl:FunctionalProperty, before the domain
owl:InverseFunctionalProperty. Back from Turtle, either one reads as
at most 1 on that side.
Property domain and range lines
A domain X / range X body line gives the same rdfs:domain /
rdfs:range triple as the one-line D -> R; X renders like any type
expression, so a with block becomes nested intersection and restriction
blank nodes:
property partOfProcess
domain Process
with occupiesTemporalRegion: TemporalRegion
range Process
with ^occupiesTemporalRegion: TemporalInterval
gives
:partOfProcess rdf:type owl:ObjectProperty
; rdfs:domain [ a owl:Class ; owl:intersectionOf ( :Process
[ a owl:Restriction ; owl:onProperty :occupiesTemporalRegion ;
owl:someValuesFrom :TemporalRegion ] ) ]
; rdfs:range [ a owl:Class ; owl:intersectionOf ( :Process
[ a owl:Restriction ;
owl:onProperty [ owl:inverseOf :occupiesTemporalRegion ] ;
owl:someValuesFrom :TemporalInterval ] ) ] .
A side that is not given writes no triple. A property without a range is an
owl:ObjectProperty; one whose range is a primitive type is an
owl:DatatypeProperty, as with the one-line form.
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 quantity range such as 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 radeau 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.
radeau 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-, property- or field-level@iri_name <iri>. Ahasfield whose property IRI is not<its concept's file>#<name>(another vocabulary, another file) keeps it with an@iri_name <iri>line under thehas. The recovered directive uses the absolute form (a@iri_name "segment"is recovered as the equivalent absolute IRI).- Opaque IRIs get readable names. A class or standalone property whose
local name is letters, an optional
_, then digits (BFO_0000015,ont00001857) is named from itsrdfs:label(English first), camel-cased ("process"→Process,"occupies temporal region"→occupiesTemporalRegion), with an@iri_name <full IRI>line keeping the real IRI. Every reference uses the new name. A label that clashes with another name in the same file, starts with a digit, or has no usable letters keeps the local name. - Class expressions come back as
withlines. Each restriction shape of Restrictions:withis read back,N..Mincluded; an intersection of a type and restrictions becomes the block form, and a restriction in a filler a nested block. Complexrdfs:subClassOfobjects becomesublines,owl:equivalentClassbecomesequivalent to. A property whoserdfs:domainorrdfs:rangeholds a restriction getsdomain/rangelines instead ofD -> R, and one with neither is a bareproperty p. A property only used in restrictions is still declared. A shape outside this list (a datatype filler,owl:onDataRange,owl:oneOf, facets) is left as before, never half-read. Unqualified cardinalities (owl:minCardinality…) stayhasfields. - 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.