Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .dlf package to an OWL graph.
  • Turtle → Dolfin: reconstruct a .dlf package 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:

  1. a base IRI (the package identity),
  2. the file path of the declaration inside the package,
  3. 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):

DolfinIRI
concept Mammal in mammals.dlfhttp://example.com/animals/mammals#Mammal
concept Animal in the root filehttp://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.dlfhttp://example.com/animals/mammals#Diet_Herbivore
fact Biscuit a Dog in data.dlfhttp://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 fileNamespace used for entitiesconcept 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#> produced http://ex.org/ns##Foo. It now produces http://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.

DolfinOWL / RDF
concept CC rdf:type rdfs:Class
#@ owl + concept CC rdf:type owl:Class
concept C: sub PC rdfs:subClassOf P
concept C: equivalent to PC 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 blockowl: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 ] ) ] ) ].

Video 2.4.2 · Union types, chapter “In RDF” (0:37–0:56). Watch on PeerTube

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.

DolfinOWL
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 fieldOWL triple on the owl:Ontology node
versionowl:versionInfo
descriptionrdfs:comment
authordc:creator (dc: = http://purl.org/dc/elements/1.1/)

Primitive types

DolfinXSD datatype
stringxsd:string
intxsd:integer
floatxsd:float
booleanxsd:boolean
datexsd:date
date_timexsd:dateTime
timexsd:time
durationxsd: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/):

DolfinTurtle
has weight: mass:weight a owl:DatatypeProperty ; rdfs:range quantitykind:Mass
range mass usedquantitykind: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:

SourceRDFS / 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 givenskos: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@xx is the preferred label of xx. Write at most one per language; an extra one is kept as a plain rdfs:label.
  • Without a pref_label@xx, the first label@xx is promoted to preferred label; later ones stay plain rdfs:label.
  • With a pref_label@xx, every label@xx stays plain rdfs:label, even one written before it.
  • With no label and no pref_label at 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.

Video 2.4.1 · Multilingual descriptions, full episode. Watch on PeerTube

Cardinality

A has field’s cardinality becomes an OWL restriction on the owning class:

Dolfin cardinalityOWL 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 nowl: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:

DolfinOWL
symmetricrdf:type owl:SymmetricProperty
reflexiverdf:type owl:ReflexiveProperty
transitiverdf:type owl:TransitiveProperty
sub qrdfs:subPropertyOf q
inverse of qowl:inverseOf q
equivalent to qowl:equivalentProperty q
equivalent to ^qowl: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.

Video 3.4 · Rules with comparisons, chapter “Rules in the Turtle” (1:28–1:43). Watch on PeerTube
Compared values=!=< <= > >=
Numbers, dates, times, durationsmath:equalTomath:notEqualTomath:lessThan, math:notGreaterThan, math:greaterThan, math:notLessThan
Quantities (quantity(...), or a quantity range such as mass)dq:equalTodq:notEqualTodq:lessThan, dq:lessThanOrEqual, dq:greaterThan, dq:greaterThanOrEqual
Strings, booleans, IRIs, individuals of a conceptlog:equalTolog:notEqualTomath: 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:

DolfinN3
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, Msame, with math:notGreaterThan, math:equalTo, or both bounds
any ?v [C]: PC 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 ?v without a [ … ] constraint: there is no domain for ?v to range over.
  • A count whose sub-patterns bind variables other than ?v and the rule’s own (e.g. ?r distance ?d): log:collectAllIn would count one ?v once 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 in main.dlf. Turtle that carries no rdfs:isDefinedBy (e.g. graphs not produced by this compiler) falls back to deriving the file from the entity IRI structure.
  • @iri_name overrides 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>. A has field whose property IRI is not <its concept's file>#<name> (another vocabulary, another file) keeps it with an @iri_name <iri> line under the has. 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 its rdfs: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 with lines. Each restriction shape of Restrictions: with is read back, N..M included; an intersection of a type and restrictions becomes the block form, and a restriction in a filler a nested block. Complex rdfs:subClassOf objects become sub lines, owl:equivalentClass becomes equivalent to. A property whose rdfs:domain or rdfs:range holds a restriction gets domain / range lines instead of D -> R, and one with neither is a bare property 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 …) stay has fields.
  • 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_version is 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.