A Semantechs teaching resource

The Bookshop Trail an ontology course

Seventy-six lessons that start with a word, a thing and the idea in between, and end with an OWL 2 DL vocabulary a reasoner has agreed with. Meaning, systematics, naming, definitions and design principles take half the course; RDFS, SKOS and OWL 2 take a quarter, as one idea at rising commitment; standing on gist, measuring quality, and living with change take the rest. Every lesson opens in the browser editor, and every claim a tool could check has been checked.

76lessons
12modules
64measured by a reasoner, a checker or a validator
137terms shown
1editor, in the browser

Start here

Read the brief: a page from the Trail's committee, written loosely on purpose. Then press Open in the editor on o01. The file arrives in the editor at semantechs.co.uk/turtle-editor-viewer, the graph view draws it, and the header in the file is the lesson. Read the header before you look at the graph.

Each lesson decides one thing about the Bookshop Trail's vocabulary and says why. The Measured block under it is what the tools found when the file was put through them: the OWL 2 profile checker, HermiT, an OWL 2 RL closure like the editor's own reasoner, the editor's own SHACL engine, the Ontology Quality Suite, the OQuaRE metrics, and the lesson's own queries. Where a lesson seeds a fault, the block shows the tool finding it.

The triangle from o01 runs through the whole course. Each lesson names the corner it works on: the symbol (naming), the thought (definitions, scope, agreement) or the referent (kinds, roles, sets, axioms). RDFS, SKOS and OWL 2 arrive in modules 06 and 07 as one idea at rising commitment, and the lessons before them are what makes the commitment worth making.

A suggested order

00 → 01 → 02 in full → 03 (o15, o17, o19) → 04 in full → 05 in full → 06 (o35, o36, o38, o40) → 07 (o43, o46, o47, o48) → 08 (o52, o56) → 09 (o59, o61) → 10 (o66, o68) → 11 (o70, o71) → 12. Modules 02, 04 and 05 are where the thinking is; the rest is what the thinking looks like written down.

Module 00

The lab

Twenty minutes on the tool before the first lesson. Everything runs in the Turtle Editor Viewer at semantechs.co.uk/turtle-editor-viewer: the parser, the graph view, the SPARQL engine, the OWL 2 RL reasoner behind Show Facts, and the SHACL engine with its Inference dropdown, all inside the page. The full text is lessons/00-the-lab/README.md.

One file, one tab

Every lesson is a Turtle file with a header that carries the whole lesson. Open in the editor loads it; the graph view draws it; Add Prefixes then the SPARQL panel runs the queries at the end of the file.

Press Get All after any edit, or the SPARQL panel answers about the file as it was.

Show labels, Node Labels and Property Labels are ticked when the editor opens, and put labels where the IRIs would be. Untick all three to see the IRIs; module 03 needs them.

Show Facts

The editor's reasoner is an OWL 2 RL engine. It types individuals, closes transitive and symmetric properties, follows inverses and chains, merges under functional properties, and never reports a contradiction. The Measured block under each lesson says what an OWL 2 RL closure added, which is what Show Facts shows.

Shapes and the Inference dropdown

Some lessons carry a companion shapes file; the button opens it in a second tab already selected. Validate runs it. The rule lesson in module 08 also sets Inference to rules, iterated, which the dropdown beside Shapes changes.

What needs a terminal

Unsatisfiable classes, the inferred hierarchy, explanations and profile verdicts come from a description-logic reasoner, HermiT through ROBOT; the check identifiers from the Ontology Quality Suite. The SHACL numbers do not: they come from shacl, the same engine the editor runs, through its Python bindings. Each lesson's Runs in line says which, and the Measured block already has the result.

The same measurements, from the command line

./scripts/setup-tools.ps1                  # fetches ROBOT, gist, gistBFO, gUFO and BFO into lib/
pip install -r scripts/requirements.txt     # rdflib, owlrl, shacl, ontology-quality-suite
python scripts/check_lessons.py             # every lesson, every instrument; about two minutes
python scripts/check_lessons.py o47 --show  # one lesson, with the inferred triples printed
Module 01

Meaning

Before there is a class or a property there is a word, the thing it stands for, and the idea in between. The triangle of reference is the oldest picture of that, and it is the picture this whole course is drawn on; so this module puts the Bookshop Trail's nouns onto it, shows the two ways in which a word fails, and introduces the one kind of symbol that is globally unique and that a machine can compare, which is the IRI, and which is what takes us from strings to things.

Sources
O01

A word, a thing, and the idea in between

Every term in the brief is a symbol standing for a referent by way of a thought, and the three are kept apart from the first line.

Corner: all three, introduced · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

# The candidate terms from the brief, as symbols with the thought written
# beside them. No classes, no properties: nodes, labels, comments.

bt:candidate-wigtown
    rdfs:label    "Wigtown"@en ;
    rdfs:comment  "The town in Dumfries and Galloway, Scotland's book town. Not the festival, the ward or the bay."@en .

bt:candidate-area
    rdfs:label    "area"@en ;
    rdfs:comment  "Used in the brief for two things: a shop's floor area in square metres, and a council area. Two thoughts under one symbol; see o02."@en .

bt:candidate-book
    rdfs:label    "book"@en ;
    rdfs:comment  "Used in the brief for a title, an edition and a copy on a shelf. Three thoughts under one symbol; see o02."@en .

bt:candidate-author
    rdfs:label    "author"@en ;
    rdfs:comment  "Someone who wrote a book. Also used for whoever appears at an event, who may be a translator."@en .

bt:candidate-shop
    rdfs:label    "shop"@en ;
    rdfs:comment  "One symbol of three: shop, bookshop, store. One thought; see o02."@en .

bt:candidate-founded
    rdfs:label    "founded"@en ;
    rdfs:comment  "The year a shop opened. The guidebooks disagree for four shops, and the brief asks that both dates be kept with their source."@en .

Why

Ogden and Richards drew it in 1923 and nobody has improved on it since. At one corner sits the symbol, which here is the word 'Wigtown' as it appears in the spreadsheet; at another sits the referent, the town itself on the Solway coast, which does not care in the least what it is called; and at the third sits the thought, the idea of the town in somebody's head, and it is the thought that connects the other two. The symbol does not refer to the town directly but by way of someone's understanding of it, which is why the base of the triangle is drawn as a dotted line.

That dotted line is where every data integration problem I have met has lived. Two people share the symbol 'area' and hold quite different thoughts under it, one of square metres and the other of council boundaries, and nothing in the spreadsheet says which is meant. An ontology is a way of fixing the thought corner in writing, so that a shared symbol carries a shared meaning rather than a private one.

This file is the smallest possible start. Each candidate term from the brief becomes a node with a label, which is the symbol, and a comment, which is the thought written down; the referent stays where it is and the node points at it. Nothing here is a class or a property yet, and that comes later, and comes more easily, once the three corners have become a habit.

Diagram

                      THOUGHT
                 (the idea of the town)
                     /          \
        symbolises  /            \  refers to
                   /              \
             SYMBOL  . . . . . . . REFERENT
            "Wigtown"               the town
             a string           on the Solway coast

   The dotted base: a symbol stands for a referent only through
   a thought. Share the symbol, and you have not yet shared the
   thought. Writing the thought down is what an ontology is for.

What to take away

  • Symbol, thought and referent are three different things, and once you keep them apart most of the confusion around naming, defining and identity becomes easy to state, if not always easy to settle.
  • A label is a symbol for people and a definition is the thought written down, and neither of them is the thing itself.
  • The brief is a pile of symbols, so the first job is to find out how many referents stand behind them, and it is rarely one per word.

Try it

Press Open in the editor and the graph view draws each candidate as a node with its label and comment. Pick 'Subjects' and choose one, then ask yourself which corner of the triangle each of the three things on the screen belongs to: the IRI, the label and the comment.

Measured

Parses12 triples

Sources

O02

One word, many things; many words, one thing

'Book' is split into three referents and 'shop', 'bookshop' and 'store' are merged into one, because identity follows the referent and not the word.

Corner: symbol and referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

# 'book', split by referent. Three things, three identifiers; the word is a
# label on all of them.

bt:candidate-work
    rdfs:label    "book"@en, "work"@en, "title"@en ;
    rdfs:comment  "What an author wrote, independent of any printing. Translated, it becomes another work."@en .

bt:candidate-edition
    rdfs:label    "book"@en, "edition"@en ;
    rdfs:comment  "A printing of a work by a publisher, with an ISBN if made after 1970."@en .

bt:candidate-copy
    rdfs:label    "book"@en, "copy"@en ;
    rdfs:comment  "A physical copy of an edition, on a shelf in one shop at one price. The Trail counts these in bs:StockRecord rather than naming each."@en .

# 'shop', merged by referent. One thing, three labels.

bt:candidate-bookshop
    rdfs:label    "bookshop"@en, "shop"@en, "store"@en ;
    rdfs:comment  "A business that sells books from premises. The building it trades from is a different referent; o05 and o10."@en .

Why

A word can fail in two directions. Homonymy is one symbol stretched over several referents, and 'book' in the brief is the example: it means the title someone wrote, the edition with an ISBN, and the copy on the shelf at a price, so that when a customer asks 'do you have the book?' three different answers are all correct. Synonymy is several symbols for one referent, and shop, bookshop and store are one thing three ways, which means a search that matches on the string will find a third of what it should.

Both failures have the same cure. Decide how many referents there really are, give each one its own identifier, and hang the words off the identifiers as labels, so that the identifier decides identity and the words are free to vary. Google called this 'things, not strings' in 2012, and the phrase has stuck because it is exactly right.

The Trail keeps 'book' as one class, bs:Work, and gives it an ISBN, which is really an edition's property rather than a work's. That is a recorded compromise, and o10 comes back to it, but it is already a better state than the brief's, where the compromise existed and nobody could see it.

Diagram

   HOMONYMY: one symbol, three referents           SYNONYMY: three symbols, one referent

        "book" ---.---> the title (a work)              "shop"     --.
                  |---> the edition (an ISBN)            "bookshop" --+--> bt:shop-inkwell
                  '---> the copy (a shelf price)         "store"    --'

   cure: one identifier per referent               cure: one identifier, three labels

What to take away

  • Count referents rather than words: a symbol with two referents needs two identifiers, and two symbols with one referent need only one.
  • Labels are cheap and can be as plural as you like, whereas identifiers are the thing that has to be unique.
  • The Trail's bs:Work is a title and an edition in one class, and written down that is a decision, while left unwritten it would be a trap.

Measured

Parses14 triples
Query: Every symbol with more than one node behind it1 row
  • "book" "3"

Afterwards, in the SPARQL panel

Every symbol with more than one node behind it (on the model)
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?label (COUNT(?node) AS ?referents)
WHERE { ?node rdfs:label ?label }
GROUP BY ?label
HAVING (COUNT(?node) > 1)
ORDER BY DESC(?referents)

Sources

O03

Shared meaning is an agreement

A definition is treated as the record of an agreement, with who made it and when, and a disagreement is modelled rather than settled by the modeller.

Corner: thought · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# The agreement that 'founded' and 'opened' are one term, with the record of
# who agreed it. dct:source and skos:editorialNote carry the trail.

bt:candidate-founded
    rdfs:label          "founded"@en, "opened"@en ;
    rdfs:comment        "The year a shop or publisher began trading."@en ;
    skos:editorialNote  "Agreed at the committee of 12 March 2025 that 'founded' and 'opened' are the same term. Where sources disagree about the year, keep every claim with its source; do not choose."@en ;
    dct:source          bt:committee-2025-03-12 .

bt:committee-2025-03-12
    rdfs:label    "Trail committee, 12 March 2025"@en ;
    rdfs:comment  "The meeting that agreed the first list of terms."@en .

# A disagreement that stays open: two claims about one fact, each with a
# source. This is what the brief asked for, and it is data, not vocabulary.

bt:claim-ex-libris-1921
    rdfs:label    "Ex Libris founded in 1921, per the national register"@en ;
    dct:source    bt:source-national-register .

bt:claim-ex-libris-1923
    rdfs:label    "Ex Libris founded in 1923, per the trail guide"@en ;
    dct:source    bt:source-trail-guide-2024 .

Why

Two people looking at the same referent hold two thoughts, and the triangle has no mechanism of its own for making them one. What makes a meaning shared is an agreement, that this symbol from now on means this, and here is the sentence we all signed up to. That sentence is the definition, and it is a social artefact before it is a technical one; the process of arriving at it is where a team finds out what it actually thinks, and in my experience that is at least half the value of the exercise.

Some disagreements, though, are not misunderstandings waiting to be cleared up. The SemOps Manual's example is 'active customer', where finance and product each need a different definition for reasons that are load-bearing for them, and the right move is to model both, well labelled, and let someone with the authority decide which goes in the board pack. The modeller's job is to make the difference visible, not to adjudicate it, and the wrong move is the one that happens by default, where the modeller picks one, ships it, and half the business ignores the model from then on.

The Trail has a small version of this. 'Founded' and 'opened' are agreed to mean the same thing because the committee said so, but the founding year of Ex Libris is not agreed, and the brief asks for both claims to be kept with their source. That is a disagreement about a fact rather than about a meaning, and the RDF 1.2 layer of the data records it as such; what this lesson adds is the record of the agreement itself, who decided that founded means opened, and when.

Diagram

   your thought          my thought
       |                      |
       '------> agreement <---'
                   |
            written down as
                   |
              a definition       + who decided, and when
                   |
             carried by the symbol, from now on

   A disagreement that will not close is modelled as two terms,
   each defined, and the choice between them is somebody's decision.

What to take away

  • A definition is an agreement in writing, and it is the thing that lets a shared symbol carry a shared thought.
  • Record who agreed it and when, because definitions get argued over again and the trail of the decision is what stops the same argument coming round each year.
  • Do not resolve a disagreement that is really about territory; model both meanings with clear labels and hand the choice to someone with the authority to make it stick.
  • Building the vocabulary is a social process, and often the most useful part of the whole undertaking, because people find out what they mean.

Measured

Parses11 triples

Sources

O04

A symbol that is globally unique, and that a machine can read

Every referent gets an IRI, because an IRI is the one kind of symbol that cannot collide with anyone else's and that a machine can compare, join on and look up.

Corner: symbol · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

bt:place-wigtown
    rdfs:label    "Wigtown"@en, "Baile na h-Uige"@gd ;
    rdfs:comment  "The town. The IRI is the Trail's own; the labels are anyone's."@en ;
    owl:sameAs    <http://dbpedia.org/resource/Wigtown> .

bt:festival-wigtown
    rdfs:label    "Wigtown Book Festival"@en, "Wigtown"@en ;
    rdfs:comment  "The festival, which people also call Wigtown. A different referent, so a different IRI."@en .

# A local code, kept for the spreadsheet's sake. It identifies inside the
# Trail's own systems and nowhere else, so it is a label here, not a name.
bt:place-wigtown
    skos:notation  "WGT-04" .

Why

A label is a symbol for people. 'Wigtown' works perfectly well in conversation and fails in a database, where the festival and the town both answer to it and no machine can tell them apart. A code works inside one system, EVT-001 or row 14 or the shop id, and fails the moment two systems meet, because both of them have an EVT-001.

An IRI is a symbol built to survive that meeting. It is globally unique by construction, because the http scheme delegates the right to mint names under a domain to whoever holds the domain, so that example.org/bookshop-trail/place-wigtown can only have been coined by one party and nobody else's place-wigtown can be confused with it; the Web Architecture document calls this URI ownership. And it is machine-readable in a precise and rather modest sense: a machine cannot understand it, but it can test two of them for equality by comparing the strings, join two graphs wherever the same IRI appears in both, and, because it is an http IRI, fetch it and get the definition back. Nothing more is claimed and nothing more is needed. The same document also asks for opacity, meaning a machine should read nothing into the spelling of an IRI, and that is why gist and BFO can both be right about naming, one readable and one numeric, as o15 discusses.

The IRI is still not the referent. It is a third kind of symbol sitting on the symbol corner beside the label, and RDF Concepts section 1.3 says in as many words that what it refers to is fixed by convention and definition rather than by the IRI itself. owl:sameAs is the statement that two IRIs share a referent, and the Trail uses it to say that its Wigtown is DBpedia's.

Diagram

   symbols on the symbol corner            what a machine can do with each

   label   "Wigtown"@en                    match a string; collide with the festival
   code    WGT-04                          match inside one system; collide outside it
   IRI     bt:place-wigtown                compare for identity anywhere
           = https://example.org/          join two graphs on it
             bookshop-trail/place-wigtown  fetch it and read the definition
                                           cannot be minted by anyone but the owner of example.org

   bt:place-wigtown  owl:sameAs  <http://dbpedia.org/resource/Wigtown>
   "these two symbols, minted by two owners, share one referent"

What to take away

  • Global uniqueness comes from ownership, since only the holder of a domain can mint IRIs under it; that is the whole trick, and it turns out to be enough.
  • Machine-readable means comparable, joinable and fetchable, and it does not mean understood.
  • An IRI is opaque to a machine, so its spelling is for people and the definition it resolves to is what fixes the referent.
  • owl:sameAs joins two owners' symbols at the referent, and it is for identity rather than resemblance.

Try it

In the SPARQL panel, count how many distinct subjects carry the label 'Wigtown', and then count the distinct IRIs. The first number is what a string search sees, and the second is what a graph sees.

Measured

Parses8 triples
Query: Symbols that name more than one thing1 row
  • "Wigtown" "2"
Query: What the Trail's Wigtown is the same as1 row
  • bt:place-wigtown <http://dbpedia.org/resource/Wigtown>

Afterwards, in the SPARQL panel

Symbols that name more than one thing (on the model)
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?label (COUNT(DISTINCT ?thing) AS ?things)
WHERE { ?thing rdfs:label ?label }
GROUP BY ?label
HAVING (COUNT(DISTINCT ?thing) > 1)
What the Trail's Wigtown is the same as (on the model)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?ours ?theirs WHERE { ?ours owl:sameAs ?theirs }

Sources

O05

A particular or a kind

Wigtown, The Inkwell and Rhona Blackwood are particulars; Settlement, Bookshop and Person are kinds; rdf:type is the link, and a kind is a referent with an IRI like any other.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

bs:Settlement
    a             rdfs:Class ;
    rdfs:label    "Settlement"@en ;
    rdfs:comment  "A town or city."@en .

bs:Bookshop
    a             rdfs:Class ;
    rdfs:label    "Bookshop"@en ;
    rdfs:comment  "A business that sells books from premises."@en .

bs:Person
    a             rdfs:Class ;
    rdfs:label    "Person"@en ;
    rdfs:comment  "A human being."@en .

bt:place-wigtown
    a           bs:Settlement ;
    rdfs:label  "Wigtown"@en .

bt:shop-inkwell
    a           bs:Bookshop ;
    rdfs:label  "The Inkwell"@en .

bt:author-rhona-blackwood
    a           bs:Person ;
    rdfs:label  "Rhona Blackwood"@en .

# A genre is a particular here, not a class: something a shop specialises in
# and a work belongs to. Module 04 and module 06 make the case in full.
bt:genre-crime-fiction
    a           skos:Concept ;
    rdfs:label  "Crime Fiction"@en .

Why

Some symbols in the brief pick out one thing, such as Wigtown, The Inkwell, Rhona Blackwood or the Lodestone Group, while others pick out a kind of thing, such as town, shop, author or publisher. The first are particulars, which OWL calls individuals, and the second are kinds, which it calls classes; the relation between them is rdf:type, and it is the first relation worth writing down, because most of the brief's questions are questions about kinds, about which shops, which towns and which events.

A kind is a referent too. bs:Settlement refers to the kind, town, in just the way that bt:place-wigtown refers to the town, and it gets an IRI on the same terms, which is what lets you say things about the kind itself: what it is called in Welsh, what its definition is, and which other kinds it falls under. The class hierarchy of module 02 is nothing more than a set of statements about kinds.

The line between particular and kind is not always where the brief puts it. 'Crime fiction' looks like a kind of book, and a class of works is one fair reading of it, but it is also a thing a shop specialises in, and a shop is not a member of a class of books. So the Trail treats genres as particulars, concepts in a scheme, in order that a work and a shop can both point at the same one. o24 returns to that choice, which I think is the most consequential one in the course.

Diagram

   particulars (individuals)          rdf:type        kinds (classes)

   bt:place-wigtown        ------------------------>  bs:Settlement
   bt:shop-inkwell         ------------------------>  bs:Bookshop
   bt:author-rhona-blackwood --------------------->  bs:Person
   bt:genre-crime-fiction  ------------------------>  skos:Concept   <- a particular, on purpose

   Both columns are referents with IRIs. The right-hand ones can be
   labelled, defined and arranged under each other, like anything else.

What to take away

  • rdf:type says which kind a particular belongs to, and it is the first structural statement, the one every question about 'which shops' depends on.
  • A class is a referent with an IRI, a label and a definition of its own, and saying things about kinds is what a taxonomy is.
  • Whether something is a particular or a kind is a decision rather than a fact, and genre is the case where the obvious answer is not the best one.

Measured

Parses17 triples; 3 classes; 1 concept; 4 individuals
Query: Every particular and its kind4 rows
  • bt:genre-crime-fiction skos:Concept
  • bt:shop-inkwell bs:Bookshop
  • bt:author-rhona-blackwood bs:Person
  • bt:place-wigtown bs:Settlement

Afterwards, in the SPARQL panel

Every particular and its kind (on the model)
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?thing ?kind WHERE { ?thing a ?kind . FILTER(?kind != rdfs:Class) } ORDER BY ?kind

Sources

O06

A relationship is a thing too

The brief's verbs become properties with IRIs of their own: locatedIn, wrote, stocks, heldAt; its measurements become datatype properties; and a property is defined like any other term.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

bs:locatedIn
    a             rdf:Property ;
    rdfs:label    "located in"@en ;
    rdfs:comment  "The settlement a shop or publisher trades in."@en .

bs:wrote
    a             rdf:Property ;
    rdfs:label    "wrote"@en ;
    rdfs:comment  "The person wrote the work. Translating is a different relationship."@en .

bs:stocks
    a             rdf:Property ;
    rdfs:label    "stocks"@en ;
    rdfs:comment  "The shop has copies of the work on its shelves."@en .

bs:heldAt
    a             rdf:Property ;
    rdfs:label    "held at"@en ;
    rdfs:comment  "The shop where an event took place."@en .

bs:influencedBy
    a             rdf:Property ;
    rdfs:label    "influenced by"@en ;
    rdfs:comment  "Whom this author read first. Direct influence only; several removes is a question for a path."@en .

bs:founded
    a             rdf:Property ;
    rdfs:label    "founded"@en ;
    rdfs:comment  "The year a shop or publisher began trading. A value, not a thing."@en .

bs:floorArea
    a             rdf:Property ;
    rdfs:label    "floor area"@en ;
    rdfs:comment  "Selling floor in square metres."@en .

bt:shop-inkwell
    bs:locatedIn  bt:place-wigtown ;
    bs:founded    "1979" ;
    bs:floorArea  "140.0" .

Why

The nouns give us kinds and particulars, and the verbs and the 'has' phrases give us relationships: a shop is in a town, an author wrote a work, a shop stocks a work, an event was held at a shop. Each of these is a referent in its own right, a kind of connection that can hold between many pairs, and it gets an IRI, a label and a definition on the same terms as a class, so the triangle applies to it unchanged.

Some relationships end in a value rather than a thing, such as the year a shop opened, its floor area, the number of staff, or whether there is a cafe, and these are datatype properties. The distinction matters later because OWL requires every property to be one or the other and never both, but it is worth making now for a simpler reason, which is that a value has no identity of its own; 140 square metres is not a thing with an IRI but a measurement of one.

Two relationships from the brief are worth pausing on. 'Specialises in' points a shop at a genre, and 'in one or more genres' points a work at the very same genres, which is the argument for genres being particulars (o05). And 'influenced by' runs from author to author at several removes, which the brief's question 7 wants followed, and that is a property with a shape the reasoner will have views on in module 07.

Diagram

   noun phrase in the brief          what it becomes

   "a shop is in a town"             bs:locatedIn      object property   Bookshop -> Settlement
   "the author wrote it"             bs:wrote          object property   Person   -> Work
   "the shop stocks it"              bs:stocks         object property   Bookshop -> Work
   "held at the shop"                bs:heldAt         object property   Event    -> Bookshop
   "who influenced them"             bs:influencedBy   object property   Person   -> Person

   "year opened"                     bs:founded        datatype property Bookshop -> a year
   "area (square metres)"            bs:floorArea      datatype property Bookshop -> a number
   "whether there is a cafe"         bs:hasCafe        datatype property Bookshop -> true/false

   A property is a referent: it has an IRI, a label, a definition.

What to take away

  • Verbs become object properties and measurements become datatype properties, and both are terms with IRIs and definitions like any other.
  • A value has no identity of its own, which is the line between an object property and a datatype property, and OWL will hold you to it.
  • The brief's 'at several removes' is a property that will need transitivity, a path or a rule, so it is worth noticing now; module 08 answers it three ways.

Measured

Parses24 triples

Sources

O07

Scope, and the questions that fix it

The ontology covers what the brief's ten questions need and nothing the brief only mentions; the questions are written into the ontology as its statement of scope.

Corner: thought · Runs in the browser

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

<https://example.org/bookshop-trail/schema>
    a             owl:Ontology ;
    rdfs:label    "The Bookshop Trail schema"@en ;
    rdfs:comment  "A vocabulary for a network of independent bookshops in real British towns: the shops, the towns and areas they are in, the books they stock, the people who wrote them, the events they hold, and the walking routes between them. Out of scope: festivals, wards, weather, tills, and anything the ten questions below do not need."@en ;
    skos:scopeNote "Competency questions, from the committee's brief: (1) which shops are in Wales, the south west, or a given council area; (2) which towns have no bookshop; (3) what can be walked to from a shop, and how far; (4) which shops specialise in crime fiction including its kinds; (5) who appeared at events in York and what it cost; (6) which books have no ISBN; (7) which authors were influenced, at any remove, by Rhona Blackwood; (8) which imprints belong ultimately to the Lodestone Group; (9) when Ex Libris opened and who says so; (10) which shops stock a book and the cheapest copy."@en ;
    dct:source    <https://github.com/pwin/Ontology_Course/blob/main/data/the-brief.md> .

Why

The brief mentions a bay, a festival, a council ward, the weather that cancels a reading, and the till, and none of them is in scope. The way to know is the list of questions at the end of it, because an ontology exists to answer questions, and a term that no question needs is a term that will be defined badly, since nobody will use it often enough to notice.

Competency questions are the oldest tool in ontology engineering and still the best test of scope. Written as prose they are a requirement, and written as SPARQL they are a test, and the SemOps Manual makes the point that a question captured once and run forever is the cheapest way there is to survive maintenance. The Bookshop Trail already has its questions written as SPARQL, since the SPARQL course is 145 of them, and module 12 runs a selection against the ontology this course builds.

The OBO Foundry lists scope as a founding principle, FP-005, which asks that an ontology have a clearly specified scope and content. The Trail states its scope in two sentences on the ontology node and lists the ten questions beside it, which is enough, and which is also more than most ontologies carry.

Diagram

   in the brief                 in scope?     why

   shops, towns, books, ...       yes         questions 1 to 10 need them
   the walking trail              yes         question 3
   who influenced whom            yes         question 7
   the festival                   no          no question asks about it
   the council ward, the bay      no          named only to say what Wigtown is not
   the weather, the till          no          not mentioned in any question

   A term nobody asks about is a term nobody will keep right.

What to take away

  • Scope is fixed by the questions, so write them down, keep them with the ontology, and refuse any term that no question needs.
  • A competency question in SPARQL is a test, which makes the SPARQL course this ontology's test suite, and module 12 uses it that way.
  • State the scope on the ontology itself, in a couple of sentences; OBO FP-005 asks for exactly this, and few ontologies do it.

Measured

Parses5 triples

Sources

Module 02

Systematics

A taxonomy is a set of claims about what things are, and some of those claims are wrong in ways that do not show until the ontology is being used. OntoClean gives us four questions to ask of every class, which are whether an instance could stop being one, what makes two of them the same, whether each one is a whole, and whether it exists only in relation to something else; and the answers decide which subclass links will bear weight. The module ends with those checks written as queries, and with gist read through them.

Sources
O08

A concrete kind and a role: Person and Author

Person is a rigid kind and Author is a role a person plays, so Author is placed under Person and never the other way round.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix oc:   <https://example.org/bookshop-trail/ontoclean#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

oc:rigidity    a owl:AnnotationProperty ; rdfs:label "rigidity"@en ;
    rdfs:comment "+R rigid: no instance can stop being one. ~R anti-rigid: every instance could stop. -R neither."@en .
oc:identity    a owl:AnnotationProperty ; rdfs:label "identity"@en ;
    rdfs:comment "+I carries a criterion of identity; +O supplies its own; -I carries none."@en .
oc:unity       a owl:AnnotationProperty ; rdfs:label "unity"@en ;
    rdfs:comment "+U every instance is a whole under one relation; ~U no instance is; -U neither."@en .
oc:dependence  a owl:AnnotationProperty ; rdfs:label "dependence"@en ;
    rdfs:comment "+D an instance exists only if some other thing does; -D it does not."@en .

bs:Person
    a              owl:Class ;
    rdfs:label     "Person"@en ;
    rdfs:comment   "A human being."@en ;
    oc:rigidity    "+R" ;
    oc:identity    "+O" ;
    oc:dependence  "-D" .

bs:Author
    a                owl:Class ;
    rdfs:subClassOf  bs:Person ;
    rdfs:label       "Author"@en ;
    rdfs:comment     "Someone who wrote at least one work. A role: played by a person, for as long as the works exist."@en ;
    oc:rigidity      "~R" ;
    oc:identity      "+I" ;
    oc:dependence    "+D" .

bs:Work
    a              owl:Class ;
    rdfs:label     "Work"@en ;
    oc:rigidity    "+R" ;
    oc:identity    "+O" .

bt:author-rhona-blackwood
    a           bs:Author, bs:Person ;
    rdfs:label  "Rhona Blackwood"@en .

Why

Ask of any class whether one of its instances could stop being one and still exist. Rhona Blackwood cannot stop being a person, because if she is not a person she is not anything, and so Person is rigid, which OntoClean writes +R. She could easily have stopped being an author, by never writing the second book or never the first, and she would still have been Rhona Blackwood, so Author is anti-rigid, written ~R, meaning that every instance of it could in principle fail to be one. Guarino and Welty call the first kind of class a type and the second a role, and of everything OntoClean offers this distinction is the one I find myself using most.

A role carries two marks rather than one, since as well as being anti-rigid it is dependent, written +D: nobody is an author except by way of a work, so Author exists only in relation to Work, in the way that Customer exists only in relation to a sale and Employee only in relation to an employer. The Trail's own definition says as much, calling an author someone who wrote at least one work in this dataset.

What follows for the hierarchy is a rule rather than a preference. An anti-rigid class may sit under a rigid one, because a role is always played by things of some kind, and Author under Person is therefore unobjectionable; but a rigid class may never sit under an anti-rigid one, because every person would then be an author by necessity, which is exactly what makes a role a role. o09 shows what that looks like when it goes wrong. The set picture says the same thing: the set of authors is a subset of the set of persons, and which subset it is changes as books get written, while the set of persons does not change at all on that account.

Diagram

                     bs:Person   +R   rigid: an instance cannot stop being one
                        |             (Rhona is a person in every circumstance)
                        |  rdfs:subClassOf   allowed: anti-rigid under rigid
                        |
                     bs:Author   ~R   anti-rigid: every instance could stop
                                  +D   dependent: on some Work

   the set of authors is a subset of the set of persons, and
   which subset it is depends on what has been written. The set
   of persons does not depend on the books.

What to take away

  • Rigidity is the first question to ask of a class, and it is simply whether an instance could stop being one and still exist: +R if never, ~R if always possible.
  • A role is both anti-rigid and dependent, and Author, Customer, Employee, Patient and Member are all of them things of some kind, in a relation, for a while.
  • A role goes under the kind that plays it, and a kind never goes under a role.

Measured

Parses34 triples; 3 classes; 6 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; nothing new
Query: The rigidity of every class3 rows
  • bs:Author "~R" bs:Person
  • bs:Person "+R"
  • bs:Work "+R"

Afterwards, in the SPARQL panel

The rigidity of every class (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?class ?rigidity ?under WHERE {
  ?class oc:rigidity ?rigidity .
  OPTIONAL { ?class rdfs:subClassOf ?under }
} ORDER BY ?class

Sources

O09

The subsumption that will not bear weight

'Every bookshop is a trail member' is read as a role, not a subclass, and the draft that put Bookshop under TrailMember is reversed.

Corner: referent · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix oc:   <https://example.org/bookshop-trail/ontoclean#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

oc:rigidity    a owl:AnnotationProperty ; rdfs:label "rigidity"@en ;
    rdfs:comment "+R rigid: no instance can stop being one. ~R anti-rigid: every instance could stop. -R neither."@en .
oc:identity    a owl:AnnotationProperty ; rdfs:label "identity"@en ;
    rdfs:comment "+I carries a criterion of identity; +O supplies its own; -I carries none."@en .
oc:unity       a owl:AnnotationProperty ; rdfs:label "unity"@en ;
    rdfs:comment "+U every instance is a whole under one relation; ~U no instance is; -U neither."@en .
oc:dependence  a owl:AnnotationProperty ; rdfs:label "dependence"@en ;
    rdfs:comment "+D an instance exists only if some other thing does; -D it does not."@en .

bs:Bookshop
    a              owl:Class ;
    rdfs:label     "Bookshop"@en ;
    rdfs:comment   "A business that sells books from premises."@en ;
    oc:rigidity    "+R" ;
    oc:identity    "+O" .

bs:TrailMember
    a              owl:Class ;
    rdfs:label     "Trail member"@en ;
    rdfs:comment   "A bookshop that is currently on the trail. Shops join and leave."@en ;
    oc:rigidity    "~R" ;
    oc:identity    "+I" ;
    oc:dependence  "+D" .

# The draft, kept so the query below can find it: rigid under anti-rigid.
bs:Bookshop
    rdfs:subClassOf  bs:TrailMember .

Why

The committee's first draft said that a bookshop is a trail member, and somebody wrote it down as Bookshop rdfs:subClassOf TrailMember, which reads naturally enough and is nonetheless wrong. OntoClean says why in a single line: an anti-rigid class cannot subsume a rigid one. Being on the trail is something a shop is for now, since the brief tells us that shops have left and one has joined twice, so TrailMember is ~R, while Bookshop is +R, because a shop that leaves the trail is still perfectly well a bookshop. Put the rigid class under the anti-rigid one and every bookshop becomes a member by necessity, which is precisely what the brief denies.

The right way round is TrailMember under Bookshop, so that a member is a bookshop, for a while, in a relation to the trail; then a shop can leave and the model has no opinion about whether it stops being a shop.

The English 'is a' is the trap here, because 'a bookshop is a trail member' and 'a translation is a work' use the same two words for quite different claims, the first about a role that every current member happens to play and the second about what a translation is in every circumstance. Rigidity is how you tell them apart when the grammar will not. This file keeps the wrong draft in place so that the query at the end has something to find, and o12 shows the corrected version.

Diagram

   the draft                                the rule

   bs:TrailMember   ~R  (for now)           an anti-rigid class (~R)
        ^                                    may not subsume
        |  rdfs:subClassOf    <- wrong       a rigid class (+R)
        |
   bs:Bookshop      +R  (always)

   'every bookshop is a member by necessity': what the model says,
   and what the brief denies. Reversed, it says what was meant:
   a member is a bookshop, in a relation to the trail, for a while.

What to take away

  • 'Is a' in English covers both essence and role, and rigidity is what tells the two apart.
  • An anti-rigid class over a rigid one is forbidden, it is the commonest error in a first draft, and a query over the annotations will find it.
  • Membership of a trail, a scheme or a club is a role, so model it under the kind that plays it, or as a relation.

Measured

Parses26 triples; 2 classes; 6 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
Query: Rigid classes placed under anti-rigid ones1 row
  • bs:Bookshop bs:TrailMember

Afterwards, in the SPARQL panel

Rigid classes placed under anti-rigid ones (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?sub ?super WHERE {
  ?sub rdfs:subClassOf ?super .
  ?sub oc:rigidity "+R" .
  ?super oc:rigidity "~R" .
}

Sources

O10

Identity: what makes two of them the same

Work, Edition and Copy carry different criteria of identity, so they are three classes and disjoint; Work is what the Trail keeps, with the compromise written on it.

Corner: referent · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix oc:   <https://example.org/bookshop-trail/ontoclean#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

oc:rigidity    a owl:AnnotationProperty ; rdfs:label "rigidity"@en ;
    rdfs:comment "+R rigid: no instance can stop being one. ~R anti-rigid: every instance could stop. -R neither."@en .
oc:identity    a owl:AnnotationProperty ; rdfs:label "identity"@en ;
    rdfs:comment "+I carries a criterion of identity; +O supplies its own; -I carries none."@en .
oc:unity       a owl:AnnotationProperty ; rdfs:label "unity"@en ;
    rdfs:comment "+U every instance is a whole under one relation; ~U no instance is; -U neither."@en .
oc:dependence  a owl:AnnotationProperty ; rdfs:label "dependence"@en ;
    rdfs:comment "+D an instance exists only if some other thing does; -D it does not."@en .

bs:Work
    a                   owl:Class ;
    rdfs:label          "Work"@en ;
    rdfs:comment        "A book: what an author wrote, in a particular published form."@en ;
    skos:editorialNote  "Work and Edition in one class, by decision: the Trail never needs two editions of one work. The ISBN, pages and price are an edition's; the author and title are a work's. Split the class the day a second edition appears."@en ;
    oc:rigidity         "+R" ;
    oc:identity         "+O" ;
    oc:unity            "+U" .

bs:Copy
    a                   owl:Class ;
    rdfs:label          "Copy"@en ;
    rdfs:comment        "A physical copy of a work, on a shelf. Not modelled individually in the Trail; counted in bs:StockRecord."@en ;
    oc:rigidity         "+R" ;
    oc:identity         "+O" ;
    oc:unity            "+U" .

bs:StockRecord
    a                   owl:Class ;
    rdfs:label          "Stock record"@en ;
    rdfs:comment        "How many copies of a work one shop holds, at what price. A count, not a whole."@en ;
    oc:rigidity         "+R" ;
    oc:identity         "+O" ;
    oc:unity            "-U" .

[] a owl:AllDisjointClasses ;
   owl:members ( bs:Work bs:Copy bs:StockRecord ) .

Why

A class carries a criterion of identity when there is a way to tell whether two of its instances are one thing or two. Two works are the same if they have the same author and the same text, whatever printing you happen to hold; two editions are the same if they share an ISBN, whatever copy you hold; and two copies are the same only if they are the same object on the same shelf. That is one word, three tests, and a thing that passes one can fail another, since two copies of one edition are the same edition and different copies.

OntoClean's rule follows from that. Classes with incompatible criteria of identity are disjoint, and a class that carries a criterion, +I, may not sit above one that carries none, -I, because the criterion is inherited downward. The practical use runs in the other direction, though: when a draft has one class doing two jobs, the criterion of identity is how you notice, and it is how you decide how many classes there really ought to be.

The Trail's bs:Work is a work and an edition in one class, since it has an author and a text and it also has an ISBN, pages and a price. That is a compromise, made because the dataset never needs to say that two editions are one work, and it is written on the class as an editorial note, which is all the difference between a compromise and a mistake. The set picture makes it plain enough: a set needs a rule for when two members are one member, and 'same ISBN' and 'same text' are not the same rule.

Diagram

   class            two of them are the same if       identity   unity

   Work             same author and text               +O         +U  (a whole under 'is a text')
   Edition          same ISBN                          +O         +U
   Copy             same physical object               +O         +U
   StockLine        same shop and same edition         +O         -U  (a count, not a whole)

   incompatible criteria  =>  disjoint classes
   bs:Work in the Trail = Work + Edition, by a recorded decision

What to take away

  • A criterion of identity is the test for one or two, a class that has one is a sortal, and different tests mean different classes.
  • Book is three sortals, work and edition and copy, and deciding how many of them you need is a scope decision that belongs on the class in writing.
  • Unity asks whether each instance is a whole, so that a stock line, being a count of copies, is ~U where a work is +U.

Measured

Parses41 triples; 3 classes; 6 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; nothing new

Sources

O11

Phases: the same thing, for a while

Living and dead authors, and past and scheduled events, are phases of one kind, not kinds of their own; gist's ContemporaryEvent and HistoricalEvent are the published example.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix oc:   <https://example.org/bookshop-trail/ontoclean#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

oc:rigidity    a owl:AnnotationProperty ; rdfs:label "rigidity"@en ;
    rdfs:comment "+R rigid: no instance can stop being one. ~R anti-rigid: every instance could stop. -R neither."@en .
oc:identity    a owl:AnnotationProperty ; rdfs:label "identity"@en ;
    rdfs:comment "+I carries a criterion of identity; +O supplies its own; -I carries none."@en .
oc:unity       a owl:AnnotationProperty ; rdfs:label "unity"@en ;
    rdfs:comment "+U every instance is a whole under one relation; ~U no instance is; -U neither."@en .
oc:dependence  a owl:AnnotationProperty ; rdfs:label "dependence"@en ;
    rdfs:comment "+D an instance exists only if some other thing does; -D it does not."@en .

bs:Event
    a             owl:Class ;
    rdfs:label    "Event"@en ;
    rdfs:comment  "Something that happened, or will happen, at a bookshop on a given day."@en ;
    oc:rigidity   "+R" ;
    oc:identity   "+O" .

# Two phases of an event, as gist has them. Shown here as classes so the
# distinction is visible; the Trail carries the same information as dates.
bs:ScheduledEvent
    a                owl:Class ;
    rdfs:subClassOf  bs:Event ;
    rdfs:label       "Scheduled event"@en ;
    rdfs:comment     "An event with a planned date that has not yet happened. A phase: the same event becomes a past event."@en ;
    oc:rigidity      "~R" ;
    oc:dependence    "-D" .

bs:PastEvent
    a                owl:Class ;
    rdfs:subClassOf  bs:Event ;
    rdfs:label       "Past event"@en ;
    rdfs:comment     "An event that has taken place. A phase."@en ;
    oc:rigidity      "~R" ;
    oc:dependence    "-D" .

# The same distinction as a property and a query, which is what the Trail does.
bs:eventDate  a owl:DatatypeProperty ; rdfs:label "event date"@en .

bt:event-inkwell-2025-03-08
    a             bs:Event ;
    rdfs:label    "Reading with Rab Fingal"@en ;
    bs:eventDate  "2025-03-08T19:00:00"^^xsd:dateTime .

Why

A role is anti-rigid because of a relation to something else, an author because of a work and a member because of a trail, whereas a phase is anti-rigid because of a condition inside the thing itself, as with a living person, a scheduled event or a child, and the instance simply passes through it. Both are ~R, a phase is -D and a role is +D, and that is the whole of the difference. Guizzardi's UFO keeps the two apart as Phase and Role, and gUFO carries them as gufo:Phase and gufo:Role.

gist has two phases in plain sight, since gist:ContemporaryEvent is an event that has started and not ended while gist:HistoricalEvent is one that has ended, and the scope note on the first says it exactly: when the event actually ends, it will cease being contemporary. The same event passes from the one to the other and stays a gist:Event throughout, and gistUFO types gist:Offer and gist:ContingentObligation as phases of a commitment for the same reason.

The Trail records death as the absence of bs:died and an event's pastness by its date, so neither is a class, and that is the usual and I think the right choice, because a phase is often better carried by a property than by a subclass: the property changes, and a class assertion should not have to. Model a phase as a class when you need to say things about the members of the phase as a set, and leave it as a property when all you will do is ask.

Diagram

   anti-rigid, two ways

   ROLE    ~R  +D    by a relation to something else
           Author (to a Work)   TrailMember (to the trail)   Publisher (to works published)

   PHASE   ~R  -D    by a condition in the thing itself
           LivingPerson (no bs:died)     ScheduledEvent (a planned start, no actual one)
           gist:ContemporaryEvent -> gist:HistoricalEvent, one event, two phases

   In the Trail, both phases here are properties, not classes:
   ask 'FILTER NOT EXISTS { ?p bs:died ?y }' rather than type a class.

What to take away

  • A phase is anti-rigid from the inside and a role is anti-rigid from a relation, and both go under the kind rather than above it.
  • gist models the two phases of an event as classes because it wants to say things about all current events; the Trail does not, so it uses a property instead.
  • A class is for saying something about a set of members, so if all you will do is ask, a property and a query are cheaper and change more gracefully.

Note

The Trail's data types event dates as xsd:date, which is not in the OWL 2 datatype map, so a file that used one would fall out of OWL 2 DL. This file uses xsd:dateTime, which is in the map. o50 is about that trap, and the SPARQL course's README has the full story.

Measured

Parses36 triples; 3 classes; 1 datatype property; 6 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
Query: Anti-rigid classes, and whether each is a role or a phase2 rows
  • bs:ScheduledEvent "phase"
  • bs:PastEvent "phase"

Afterwards, in the SPARQL panel

Anti-rigid classes, and whether each is a role or a phase (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
SELECT ?class (IF(?d = "+D", "role", "phase") AS ?kind) WHERE {
  ?class oc:rigidity "~R" ; oc:dependence ?d .
}

Sources

O12

The backbone

The rigid classes of the Trail form its backbone taxonomy; roles and phases hang off it and never form part of it.

Corner: referent · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix oc:   <https://example.org/bookshop-trail/ontoclean#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

oc:rigidity    a owl:AnnotationProperty ; rdfs:label "rigidity"@en ;
    rdfs:comment "+R rigid: no instance can stop being one. ~R anti-rigid: every instance could stop. -R neither."@en .
oc:identity    a owl:AnnotationProperty ; rdfs:label "identity"@en ;
    rdfs:comment "+I carries a criterion of identity; +O supplies its own; -I carries none."@en .
oc:unity       a owl:AnnotationProperty ; rdfs:label "unity"@en ;
    rdfs:comment "+U every instance is a whole under one relation; ~U no instance is; -U neither."@en .
oc:dependence  a owl:AnnotationProperty ; rdfs:label "dependence"@en ;
    rdfs:comment "+D an instance exists only if some other thing does; -D it does not."@en .

bs:Place        a owl:Class ; rdfs:label "Place"@en ;       oc:rigidity "+R" ; oc:identity "+O" .
bs:Settlement   a owl:Class ; rdfs:subClassOf bs:Place ; rdfs:label "Settlement"@en ;   oc:rigidity "+R" ; oc:identity "+I" .
bs:CouncilArea  a owl:Class ; rdfs:subClassOf bs:Place ; rdfs:label "Council area"@en ; oc:rigidity "+R" ; oc:identity "+I" .
bs:Region       a owl:Class ; rdfs:subClassOf bs:Place ; rdfs:label "Region"@en ;       oc:rigidity "+R" ; oc:identity "+I" .
bs:Country      a owl:Class ; rdfs:subClassOf bs:Place ; rdfs:label "Country"@en ;      oc:rigidity "+R" ; oc:identity "+I" .
bs:Bookshop     a owl:Class ; rdfs:label "Bookshop"@en ;    oc:rigidity "+R" ; oc:identity "+O" .
bs:Person       a owl:Class ; rdfs:label "Person"@en ;      oc:rigidity "+R" ; oc:identity "+O" .
bs:Publisher    a owl:Class ; rdfs:label "Publisher"@en ;   oc:rigidity "+R" ; oc:identity "+O" ;
    skos:editorialNote "Treated as a kind here. gist would make it a role of an Organization; the Trail has no organisations that are not publishers or shops, so the role has nothing to be a role of."@en .
bs:Work         a owl:Class ; rdfs:label "Work"@en ;        oc:rigidity "+R" ; oc:identity "+O" .
bs:Translation  a owl:Class ; rdfs:subClassOf bs:Work ; rdfs:label "Translation"@en ; oc:rigidity "+R" ; oc:identity "+I" .
bs:Series       a owl:Class ; rdfs:label "Series"@en ;      oc:rigidity "+R" ; oc:identity "+O" .
bs:Event        a owl:Class ; rdfs:label "Event"@en ;       oc:rigidity "+R" ; oc:identity "+O" .
bs:TrailSegment a owl:Class ; rdfs:label "Trail segment"@en ; oc:rigidity "+R" ; oc:identity "+O" .
bs:StockRecord  a owl:Class ; rdfs:label "Stock record"@en ; oc:rigidity "+R" ; oc:identity "+O" .
bs:Source       a owl:Class ; rdfs:label "Source"@en ;      oc:rigidity "+R" ; oc:identity "+O" .

# The roles, hanging off the backbone.
bs:Author       a owl:Class ; rdfs:subClassOf bs:Person ;   rdfs:label "Author"@en ;       oc:rigidity "~R" ; oc:dependence "+D" .
bs:TrailMember  a owl:Class ; rdfs:subClassOf bs:Bookshop ; rdfs:label "Trail member"@en ; oc:rigidity "~R" ; oc:dependence "+D" .

Why

Strip a taxonomy down to its rigid classes and what is left is the backbone, the kinds every instance necessarily belongs to, arranged by subsumption. Guarino and Welty give the backbone its name and its test, which is that every instance in the domain falls under some backbone class and stays under it, the backbone being made only of classes that nothing can leave; the roles and phases are then attached beneath whichever backbone class plays them. A taxonomy built this way can be extended without collapsing, because the part that carries identity never depends on the part that changes.

The Trail's backbone has ten kinds at the top, which are Place, Bookshop, Person, Publisher, Work, Series, Event, TrailSegment, StockRecord and Source, with four subkinds under Place and one under Work. Author is the one role in the shipped vocabulary, and Translation is not a role, because a translation is a translation in every circumstance and is therefore a subkind. Publisher is a fair argument either way: the Trail makes it a kind, gist would call it a role of an organisation, and the argument is recorded here rather than settled, with o14 giving the vocabulary to record it in.

In set terms the backbone is the family of sets that nothing leaves, and the roles are subsets of them that things enter and leave.

Diagram

   the backbone (all +R)                          hanging off it (~R)

   Place
     Settlement  CouncilArea  Region  Country
   Bookshop  . . . . . . . . . . . . . . . . . . . TrailMember (role)
   Person  . . . . . . . . . . . . . . . . . . . . Author (role)
   Publisher  (a kind here; a role of Organization in gist)
   Work
     Translation  (a subkind: a translation in every circumstance)
   Series   Event   TrailSegment   StockRecord   Source

   every particular falls under a backbone class, and stays there

What to take away

  • The backbone is the rigid part of the taxonomy, so build it first and let the roles hang off it.
  • A subkind is rigid and inherits its identity criterion, as Translation does from Work and Settlement from Place, whereas a role is not a subkind at all.
  • Where a class could be either, say which you chose and why; Publisher is the Trail's example of that.

Measured

Parses90 triples; 17 classes; 6 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; nothing new
OQuaRE (scripts/metrics.py)17 classes, 0 object properties, 0 datatype properties, 10 roots, 13 leaves
MetricValueScore 1 to 5
LCOMOnto1.5385
WMCOnto05
DITOnto25
NACOnto0.5385
NOCOnto15
CBOOnto15
RFCOnto15
NOMOnto05
RROnto0%1
AROnto0%1
INROnto0%1
CROnto0%1
ANOnto106%5
TMOnto15
Query: The backbone: rigid classes and their rigid parents15 rows
  • bs:Bookshop
  • bs:Event
  • bs:Person
  • bs:Place
  • bs:Publisher
  • bs:Series
  • bs:Source
  • bs:StockRecord
Query: Roles, and the backbone class each hangs from2 rows
  • bs:TrailMember bs:Bookshop
  • bs:Author bs:Person

Afterwards, in the SPARQL panel

The backbone: rigid classes and their rigid parents (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?class ?parent WHERE {
  ?class oc:rigidity "+R" .
  OPTIONAL { ?class rdfs:subClassOf ?parent . ?parent oc:rigidity "+R" }
} ORDER BY ?parent ?class
Roles, and the backbone class each hangs from (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?role ?kind WHERE { ?role oc:rigidity "~R" ; rdfs:subClassOf ?kind . ?kind oc:rigidity "+R" }

Sources

O13

The taxonomy checks itself

OntoClean's constraints are written as SPARQL queries over the annotations, so that a taxonomy can be checked in the editor before a reasoner ever sees it.

Corner: referent · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix oc:   <https://example.org/bookshop-trail/ontoclean#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

oc:rigidity    a owl:AnnotationProperty ; rdfs:label "rigidity"@en ;
    rdfs:comment "+R rigid: no instance can stop being one. ~R anti-rigid: every instance could stop. -R neither."@en .
oc:identity    a owl:AnnotationProperty ; rdfs:label "identity"@en ;
    rdfs:comment "+I carries a criterion of identity; +O supplies its own; -I carries none."@en .
oc:unity       a owl:AnnotationProperty ; rdfs:label "unity"@en ;
    rdfs:comment "+U every instance is a whole under one relation; ~U no instance is; -U neither."@en .
oc:dependence  a owl:AnnotationProperty ; rdfs:label "dependence"@en ;
    rdfs:comment "+D an instance exists only if some other thing does; -D it does not."@en .

bs:Bookshop     a owl:Class ; oc:rigidity "+R" ; oc:identity "+O" ; oc:unity "+U" ; oc:dependence "-D" .
bs:TrailMember  a owl:Class ; oc:rigidity "~R" ; oc:identity "+I" ; oc:unity "+U" ; oc:dependence "+D" .
bs:Person       a owl:Class ; oc:rigidity "+R" ; oc:identity "+O" ; oc:unity "+U" ; oc:dependence "-D" .
bs:Author       a owl:Class ; oc:rigidity "~R" ; oc:identity "+I" ; oc:unity "+U" ; oc:dependence "+D" .
bs:Place        a owl:Class ; oc:rigidity "+R" ; oc:identity "+O" ; oc:unity "-U" ; oc:dependence "-D" .
bs:Settlement   a owl:Class ; oc:rigidity "+R" ; oc:identity "+I" ; oc:unity "+U" ; oc:dependence "-D" .

# Two seeded violations, so that the queries can be seen to fire.
bs:Bookshop  rdfs:subClassOf  bs:TrailMember .     # rigid under anti-rigid
bs:Person    rdfs:subClassOf  bs:Author .          # independent under dependent

# Two sound links, for contrast.
bs:Settlement  rdfs:subClassOf  bs:Place .
bs:Author      rdfs:subClassOf  bs:Person .

Why

Each meta-property inherits downward in one direction, and each constraint is then the same sentence with a different letter in it, that a class marked one way may not subsume a class marked the other. Anti-rigid may not subsume rigid; a class with a criterion of identity may not subsume one without; unity may not subsume non-unity, and anti-unity may not subsume unity; dependent may not subsume independent; and two classes with incompatible criteria of identity must be disjoint.

Because the marks are annotations in the graph, each constraint is a SPARQL query, and a taxonomy can be checked in the editor's SPARQL panel with nothing installed at all. This file seeds two violations, the TrailMember draft from o09 and a dependent class over an independent one, and each of them breaks two constraints at once, so two of the queries come back with two rows. A clean run is an empty result, which is the very thing the SHACL course warns about, since an empty result also means the query matched nothing; keep the seeded file, then, to prove that the queries fire.

Neither the Ontology Quality Suite nor OQuaRE has anything to say about OntoClean, and that is no oversight in either of them, because rigidity is not in the graph unless somebody writes it there. This is the cheapest way I know of writing it there.

Diagram

   inherits downward     so the constraint is            query

   ~R                    ~R may not subsume +R            rigid under anti-rigid
   +I                    +I may not subsume -I            no-identity under identity
   +U, ~U                +U may not subsume -U            non-unity under unity
                         ~U may not subsume +U            unity under anti-unity
   +D                    +D may not subsume -D            independent under dependent
   (identity)            incompatible IC => disjoint      checked in o10

   seeded here: Bookshop (+R, -D) under TrailMember (~R, +D)
                Person   (+R, -D) under Author      (~R, +D)
   each link breaks the rigidity rule and the dependence rule at once

What to take away

  • Every OntoClean constraint has the form 'X may not subsume Y', and written as a query over the annotations each one is a single line.
  • Keep a file with the violations in it, because a check that has never fired is indistinguishable from a check that cannot.
  • No metric suite sees rigidity, so it has to be recorded, and once it is recorded it can be checked like anything else in the graph.

Measured

Parses48 triples; 6 classes; 6 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
Query: Rigid under anti-rigid2 rows
  • bs:Bookshop bs:TrailMember
  • bs:Person bs:Author
Query: Independent under dependent2 rows
  • bs:Bookshop bs:TrailMember
  • bs:Person bs:Author
Query: No identity under identity0 rows
Query: Non-unity under unity0 rows

Afterwards, in the SPARQL panel

Rigid under anti-rigid (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?sub ?super WHERE { ?sub rdfs:subClassOf ?super ; oc:rigidity "+R" . ?super oc:rigidity "~R" }
Independent under dependent (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?sub ?super WHERE { ?sub rdfs:subClassOf ?super ; oc:dependence "-D" . ?super oc:dependence "+D" }
No identity under identity (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?sub ?super WHERE { ?sub rdfs:subClassOf ?super ; oc:identity "-I" . ?super oc:identity ?i . FILTER(?i IN ("+I", "+O")) }
Non-unity under unity (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?sub ?super WHERE { ?sub rdfs:subClassOf ?super ; oc:unity "-U" . ?super oc:unity "+U" }

Sources

O14

Reading gist through OntoClean

The Trail's classes are typed with gUFO's Kind, SubKind, Role and Phase, the way gistUFO types gist's, and the typing is checked to agree with the annotations.

Corner: referent · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix oc:   <https://example.org/bookshop-trail/ontoclean#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix gufo: <http://purl.org/nemo/gufo#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

oc:rigidity    a owl:AnnotationProperty ; rdfs:label "rigidity"@en ;
    rdfs:comment "+R rigid: no instance can stop being one. ~R anti-rigid: every instance could stop. -R neither."@en .
oc:identity    a owl:AnnotationProperty ; rdfs:label "identity"@en ;
    rdfs:comment "+I carries a criterion of identity; +O supplies its own; -I carries none."@en .
oc:unity       a owl:AnnotationProperty ; rdfs:label "unity"@en ;
    rdfs:comment "+U every instance is a whole under one relation; ~U no instance is; -U neither."@en .
oc:dependence  a owl:AnnotationProperty ; rdfs:label "dependence"@en ;
    rdfs:comment "+D an instance exists only if some other thing does; -D it does not."@en .

# gUFO's types, declared so this file stands on its own in OWL 2 DL.
gufo:Kind     a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .
gufo:SubKind  a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .
gufo:Role     a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .
gufo:Phase    a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .

bs:Person       a owl:Class, gufo:Kind ;    oc:rigidity "+R" ; oc:dependence "-D" .
bs:Bookshop     a owl:Class, gufo:Kind ;    oc:rigidity "+R" ; oc:dependence "-D" .
bs:Work         a owl:Class, gufo:Kind ;    oc:rigidity "+R" ; oc:dependence "-D" .
bs:Place        a owl:Class, gufo:Kind ;    oc:rigidity "+R" ; oc:dependence "-D" .
bs:Settlement   a owl:Class, gufo:SubKind ; oc:rigidity "+R" ; oc:dependence "-D" ; rdfs:subClassOf bs:Place .
bs:Translation  a owl:Class, gufo:SubKind ; oc:rigidity "+R" ; oc:dependence "-D" ; rdfs:subClassOf bs:Work .
bs:Author       a owl:Class, gufo:Role ;    oc:rigidity "~R" ; oc:dependence "+D" ; rdfs:subClassOf bs:Person .
bs:TrailMember  a owl:Class, gufo:Role ;    oc:rigidity "~R" ; oc:dependence "+D" ; rdfs:subClassOf bs:Bookshop .
bs:PastEvent    a owl:Class, gufo:Phase ;   oc:rigidity "~R" ; oc:dependence "-D" .

# A deliberate disagreement, for the query to find: typed a Kind, marked a role.
bs:Publisher    a owl:Class, gufo:Kind ;    oc:rigidity "~R" ; oc:dependence "+D" .

Why

gistUFO is a small published file that types every gist class with one of gUFO's kinds of class, so that gist:Person is a gufo:Kind, gist:Contract a gufo:SubKind, gist:ContractTerm a gufo:Role, gist:Offer a gufo:Phase and gist:Category a gufo:AbstractIndividualType. Those are OntoClean's distinctions under names: a Kind is rigid and supplies identity, a SubKind is rigid and inherits it, a Role is anti-rigid and relational, and a Phase is anti-rigid and intrinsic. Reading that file is the quickest way I know to see the theory applied to a real ontology by people who then had to live with the result.

The typing is done by punning, since gist:Person is an owl:Class and is also an individual of the class gufo:Kind; OWL 2 DL allows one IRI to be both and the reasoner keeps the two readings apart, as module 07 explains, and here it is enough that the profile checker agrees.

This file types the Trail's classes the same way and adds one query, which asks that a class typed gufo:Role carry ~R and +D in the annotations and that a gufo:Kind carry +R. Where the two disagree one of them is wrong, and the disagreement is worth more than either record on its own. gist's own rule for naming instances, that the infix is the most specific rigid class, is the same theory put to a practical end: never name an instance after a role, because the role may end and the name should not.

Diagram

   gUFO type          OntoClean marks        gist example (from gistUFO)    Trail

   gufo:Kind          +R, +O                 gist:Person, gist:Agreement    bs:Person, bs:Bookshop, bs:Work
   gufo:SubKind       +R, +I (inherited)     gist:Contract, gist:Account    bs:Settlement, bs:Translation
   gufo:Role          ~R, +D                 gist:ContractTerm, gist:Message bs:Author, bs:TrailMember
   gufo:Phase         ~R, -D                 gist:Offer, gist:ContingentObligation  (bs:PastEvent, o11)
   gufo:Category      +R, -I (a mixin)       gist:Content, gist:Composite   --

   one IRI, two readings:  bs:Author a owl:Class .   bs:Author a gufo:Role .

What to take away

  • gUFO's Kind, SubKind, Role, Phase and Category are OntoClean's distinctions as a vocabulary you can assert, and gistUFO applies them across the whole of gist.
  • Typing a class with a metaclass is punning, and it stays within OWL 2 DL, as the profile checker confirms.
  • Two records of the same decision, checked against each other, catch more than either alone, and here the gUFO type and the OntoClean marks have to agree.

Measured

Parses66 triples; 14 classes; 6 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; nothing new
Query: Where the gUFO type and the OntoClean marks disagree1 row
  • bs:Publisher gufo:Kind "~R" "+D"

Afterwards, in the SPARQL panel

Where the gUFO type and the OntoClean marks disagree (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
PREFIX gufo: <http://purl.org/nemo/gufo#>
SELECT ?class ?type ?rigidity ?dependence WHERE {
  ?class a ?type ; oc:rigidity ?rigidity ; oc:dependence ?dependence .
  FILTER(?type IN (gufo:Kind, gufo:SubKind, gufo:Role, gufo:Phase))
  FILTER( (?type IN (gufo:Kind, gufo:SubKind) && ?rigidity != "+R")
       || (?type = gufo:Role  && !(?rigidity = "~R" && ?dependence = "+D"))
       || (?type = gufo:Phase && !(?rigidity = "~R" && ?dependence = "-D")) )
}

Sources

O15

An IRI is not a name

The IRI identifies and the label names; a shop can be renamed without becoming a different shop, and an IRI may be readable or opaque as long as nothing depends on reading it.

Corner: symbol · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix gist: <https://w3id.org/semanticarts/ns/ontology/gist/> .
@prefix obo:  <http://purl.obolibrary.org/obo/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .

bt:shop-inkwell
    a               bs:Bookshop ;
    skos:prefLabel  "The Wigtown Inkwell"@en ;
    skos:altLabel   "The Inkwell"@en ;
    skos:definition "The crime fiction shop on Wigtown's main street, opened in 1979. Renamed in 2026; the IRI did not change."@en .

bs:Bookshop
    a               owl:Class ;
    skos:prefLabel  "Bookshop"@en .

# The two styles side by side, as gistBFO has them.
gist:Content
    a                owl:Class ;
    skos:prefLabel   "Content"@en ;
    rdfs:subClassOf  obo:BFO_0000031 .

obo:BFO_0000031
    a               owl:Class ;
    skos:prefLabel  "generically dependent continuant"@en .

Why

The Inkwell might well change its name to The Wigtown Inkwell next year, and if the IRI were the name then every link to the shop would break and every dataset that mentioned it would need editing. Because the IRI is bt:shop-inkwell and the name is a label, the label changes and nothing else does, while the old name stays on as an altLabel so that a search still finds it. This is the practical content of the Web Architecture's URI opacity, which asks that agents infer nothing from the spelling of an IRI, and from which it follows that nothing should depend on the spelling staying true.

People do read IRIs, though, and the two big upper ontologies took opposite views about that. gist writes gist:Content and gist:Organization, readable at a glance, while BFO writes obo:BFO_0000031 for 'generically dependent continuant', a number that will never go stale because it never said anything in the first place; gistBFO, the file that maps the one to the other, has both styles on every line. Neither is wrong. A readable IRI is kinder to whoever is writing the query and a small liability on the day the meaning drifts away from the word, whereas an opaque one puts all the meaning in the label and the definition, where it can be edited. The OBO Foundry requires the opaque form of its members and gist requires the readable one, and both of them forbid changing an IRI once it has been published.

The Trail's IRIs are readable and lower-case with a type prefix, as in shop-inkwell, place-wigtown and author-rhona-blackwood, and o20 compares that with gist's convention. What matters here is only that the reader can tell which of the three things on the screen is which: the IRI identifies, the label names, and the comment carries the meaning.

Diagram

   the shop is renamed                     the IRI                  the label

   before                                  bt:shop-inkwell          "The Inkwell"@en
   after                                   bt:shop-inkwell          "The Wigtown Inkwell"@en
                                                                    altLabel "The Inkwell"@en
   every link still works; the search still finds the old name

   readable                                opaque
   gist:Content                            obo:BFO_0000031
   the meaning is in the IRI and the       the meaning is in the label
   label; drift is a small liability       and definition only; drift is free

What to take away

  • The IRI identifies and the label names, so a rename is a change of label, with the old one kept as an alternative.
  • Opacity is a rule for machines, which should infer nothing from the spelling, and it is also advice for modellers, who should depend on nothing in it.
  • Readable and opaque IRIs are both respectable; pick one convention, and never change an IRI once it is published.

Try it

Untick Show labels beside the subject filter, and Node Labels and Property Labels in the graph toolbar, all three of which are on when the editor opens: the shop is bt:shop-inkwell again. Tick Node Labels and change the prefLabel, press Get All, and the box is renamed while the IRI stays put. Then in the SPARQL panel ask for every label of bt:shop-inkwell, change the prefLabel again, press Get All and ask again: the IRI in the answer has not moved.

Measured

Parses16 triples; 3 classes; 5 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
Query: Every name of the shop, with the IRI beside it2 rows
  • bt:shop-inkwell "The Wigtown Inkwell"
  • bt:shop-inkwell "The Inkwell"

Afterwards, in the SPARQL panel

Every name of the shop, with the IRI beside it (on the model)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?shop ?name WHERE { ?shop skos:prefLabel|skos:altLabel ?name . FILTER(CONTAINS(STR(?shop), "/shop-")) }

Sources

O16

Namespaces: one for the vocabulary, one for the data

The schema lives in a hash namespace and the data in a slash namespace, the ontology IRI is not the namespace IRI, and both are declared on the ontology node.

Corner: symbol · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .
@prefix vann: <http://purl.org/vocab/vann/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .

vann:preferredNamespacePrefix  a owl:AnnotationProperty .
vann:preferredNamespaceUri     a owl:AnnotationProperty .
dct:title                      a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                              owl:Ontology ;
    dct:title                      "The Bookshop Trail schema"@en ;
    vann:preferredNamespacePrefix  "bs" ;
    vann:preferredNamespaceUri     "https://example.org/bookshop-trail/schema#" ;
    skos:editorialNote             "Instance data is minted under https://example.org/bookshop-trail/ with the prefix bt. The two are governed separately."@en .

bs:Bookshop
    a               owl:Class ;
    skos:prefLabel  "Bookshop"@en .

bt:shop-inkwell
    a               bs:Bookshop ;
    skos:prefLabel  "The Inkwell"@en .

Why

A namespace is a prefix that an owner mints names under, and the Trail has two of them: https://example.org/bookshop-trail/schema# for the classes and properties, and https://example.org/bookshop- trail/ for the particulars. gist has three, and its Namespace document explains why, which is that ontology terms, taxonomy terms and instance data change at different rates and are governed by different people, so that a namespace is really a unit of governance.

Hash or slash is the other choice to make. With a hash, everything after the # is a fragment, so a browser asking for bs:Bookshop fetches the whole schema document and finds the term inside it, which suits a vocabulary of a few hundred terms that is read as a whole; with a slash, each IRI can be served on its own, which suits thirty-three shops and will still suit thirty thousand. Cool URIs for the Semantic Web sets the two out and, sensibly enough, declines to pick one for every case.

The ontology's own IRI, https://example.org/bookshop-trail/schema without the hash, names the document rather than the namespace, and the Ontology Quality Suite's QUA-006 fires when the two are conflated, because a term whose IRI is the ontology IRI and nothing more cannot be told apart from the ontology. The vann vocabulary then gives us somewhere to write the preferred prefix and namespace on the ontology node, so that tools and people can agree on 'bs:' without having to be told.

Diagram

   https://example.org/bookshop-trail/schema        the ontology IRI: the document
   https://example.org/bookshop-trail/schema#       the schema namespace   (hash)   bs:
   https://example.org/bookshop-trail/              the data namespace     (slash)  bt:

   bs:Bookshop   = schema# + Bookshop     fetch the document, find the fragment
   bt:shop-inkwell = / + shop-inkwell     fetch the one resource

   gist's three:   .../ns/ontology/gist/   .../ns/taxonomy/gist/   .../ns/data/gist/
   a namespace is a unit of governance

What to take away

  • Keep vocabulary and data in different namespaces, because the two are governed and versioned differently.
  • Hash for a vocabulary that is read as a document, slash for data that is served a resource at a time.
  • The ontology IRI names the document and is not the namespace, so declare the prefix and the namespace on the ontology node with vann.

Measured

Parses17 triples; 1 class; 8 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
Ontology Quality Suite
  • QUA-010 ×1: Term https://example.org/bookshop-trail/schema#Bookshop has no skos:definition.
  • STR-004 ×1: Class https://example.org/bookshop-trail/schema#Bookshop has no formal definition (no owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:un

Sources

O17

Local names

Classes are UpperCamelCase, properties lowerCamelCase, letters and digits only, acronyms camel-cased, no home-made abbreviations; a draft that breaks each rule is corrected.

Corner: symbol · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .

# The draft, kept so the audit can count it.
bs:Book_shop   a owl:Class ;            skos:prefLabel "Book shop"@en ;      skos:editorialNote "Underscore; and the two words are one."@en .
bs:StaffCount  a owl:DatatypeProperty ; skos:prefLabel "staff count"@en ;    skos:editorialNote "A property with a capital."@en .
bs:ISBN13      a owl:DatatypeProperty ; skos:prefLabel "ISBN-13"@en ;        skos:editorialNote "An acronym in capitals; the word boundary is lost."@en .
bs:noOfStaff   a owl:DatatypeProperty ; skos:prefLabel "no. of staff"@en ;   skos:editorialNote "An abbreviation nobody agreed."@en .

# The corrected names: the Trail's own.
bs:Bookshop    a owl:Class ;            skos:prefLabel "Bookshop"@en .
bs:staffCount  a owl:DatatypeProperty ; skos:prefLabel "staff count"@en .
bs:isbn        a owl:DatatypeProperty ; skos:prefLabel "ISBN"@en ;
    skos:editorialNote "The Trail calls it isbn rather than isbn13, since every ISBN in the data is thirteen digits. gist would say Isbn13 for a class."@en .

Why

The gist style guide's orthographic rules fit on a card. Classes start with a capital and properties with a lower-case letter, both of them camel-cased; only letters and digits are allowed, so Isbn10 rather than ISBN-10, because a hyphen ends a prefixed name in Turtle and an underscore is a word boundary nobody ever agreed on; acronyms are camel-cased too, so that word boundaries stay unambiguous, giving UriScheme rather than URIScheme, with ID as the one exception because the dictionary spells it that way; and there are to be no abbreviations of your own devising, so hasUnitOfMeasure rather than hasUoM, since the person reading the query in five years was not in the room when you invented it.

None of this is arbitrary, and none of it is about taste. A convention that every term follows is a convention a tool can check, and a tool that checks it is what stops the one hyphenated name from turning up in production. The Ontology Quality Suite's STY-001 and STY-002 check exactly these two rules, and this file is written to make them fire.

What the file holds is the committee's draft, with four names that break a rule each, set beside the corrected forms. The audit counts three of them, because noOfStaff is perfectly good lowerCamelCase and no pattern can tell an abbreviation from a word; that one is caught by a reader, or by a list of forbidden abbreviations, which is how gist handles it. The corrected names are the Trail's real ones.

Diagram

   rule                                    draft             corrected

   classes UpperCamelCase                  bs:Book_shop      bs:Bookshop
   properties lowerCamelCase               bs:StaffCount     bs:staffCount
   letters and digits only                 bs:ISBN13         bs:isbn13  (and camel-case the acronym)
   no home-made abbreviations              bs:noOfStaff      bs:staffCount
                                           bs:hasUoM         bs:hasUnitOfMeasure

   a convention every term follows is a convention a tool can check

What to take away

  • UpperCamelCase for classes, lowerCamelCase for properties, letters and digits only: four rules, with no exceptions worth having.
  • Camel-case the acronyms too, so that word boundaries stay unambiguous, as in Isbn13 and UriScheme; ID is the one exception the dictionary allows.
  • Never abbreviate on your own account, because the reader in five years was not in the room and no regular expression will catch it for you.

Measured

Parses24 triples; 2 classes; 5 datatype properties; 5 annotation properties
Naming auditbs:Book_shop, bs:ISBN13, bs:StaffCount
Profile (ROBOT)OWL 2 DL: in profile
Ontology Quality Suite
  • QUA-010 ×7: Term https://example.org/bookshop-trail/schema#Book_shop has no skos:definition.
  • STR-003 ×5: Property https://example.org/bookshop-trail/schema#ISBN13 declares neither rdfs:domain/range nor gist-style domainIncludes/rangeIncludes annotations,
  • STR-004 ×2: Class https://example.org/bookshop-trail/schema#Book_shop has no formal definition (no owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:u
  • STY-001 ×1: Class local name 'Book_shop' does not follow UpperCamelCase (PascalCase) naming style.
  • STY-002 ×2: Property local name 'ISBN13' does not follow lowerCamelCase naming style.

Sources

O18

Naming a property so that it reads

Object properties are verb-initial and read as a sentence, datatype properties are nouns, direction is unambiguous and runs up the tree; the Trail's departures from that are listed rather than hidden.

Corner: symbol · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .

bs:locatedIn    a owl:ObjectProperty ; skos:prefLabel "located in"@en ;
    skos:editorialNote "gist style would be isLocatedIn. Kept for the queries that use it."@en .
bs:publishedBy  a owl:ObjectProperty ; skos:prefLabel "published by"@en ;
    skos:editorialNote "gist style would be isPublishedBy."@en .
bs:author       a owl:ObjectProperty ; skos:prefLabel "author"@en ;
    skos:editorialNote "gist style would be hasAuthor: a noun does not read in the middle of a triple."@en .
bs:genre        a owl:ObjectProperty ; skos:prefLabel "genre"@en ;
    skos:editorialNote "gist style would be hasGenre, or isCategorizedBy."@en .
bs:imprintOf    a owl:ObjectProperty ; skos:prefLabel "imprint of"@en ;
    skos:editorialNote "Reads, and runs up the tree: child to parent."@en .
bs:connectsTo   a owl:ObjectProperty ; skos:prefLabel "connects to"@en .
bs:floorArea    a owl:DatatypeProperty ; skos:prefLabel "floor area"@en .
bs:staffCount   a owl:DatatypeProperty ; skos:prefLabel "staff count"@en .

Why

A property name is read in the middle of a triple, so the test is simply whether the triple reads. 'The Inkwell isLocatedIn Wigtown' reads, 'The Inkwell locatedIn Wigtown' very nearly does, and 'The Inkwell location Wigtown' does not. The gist guide's textual standards come down to that test and a handful of tie-breakers: object properties verb-initial, so isAbout rather than about; the 'is' prefix on -ed forms, as in isGovernedBy; an ordinary verb in preference to hasX, so precedes rather than isFollowedBy; datatype properties as nouns, so baseConversionFactor rather than convertToBase; the present tense; and a final preposition where it helps, as in hasJurisdictionOver.

Two of the rules are about direction. Make it unambiguous, so hasBroader rather than broader, which SKOS itself gets wrong by this standard; and where there is a hierarchy, name the property going up it, from child to parent, because that is the direction with fewer results, which gives isPartOf rather than hasPart and hasSuperCategory rather than hasSubCategory. That last rule is what lets gist do without inverses altogether, as o31 comes to.

The Trail's vocabulary was written before these rules were adopted here and departs from them in four places, since locatedIn, publishedBy, author and genre would be isLocatedIn, isPublishedBy, hasAuthor and hasGenre; the departures are recorded on each property as an editorial note. Renaming them would break 145 queries in the SPARQL course for a gain in consistency, and that trade is discussed under pragmatism in o30. The point here is that the departure is written down.

Diagram

   test: does the triple read as a sentence?

   bt:shop-inkwell   bs:locatedIn     bt:place-wigtown     nearly; gist: isLocatedIn
   bt:book-x         bs:publishedBy   bt:pub-bell-rock     nearly; gist: isPublishedBy
   bt:book-x         bs:author        bt:author-y          no;     gist: hasAuthor
   bt:pub-northwind  bs:imprintOf     bt:pub-lodestone     yes, and up the tree
   bt:shop-a         bs:connectsTo    bt:shop-b            yes

   datatype properties are nouns:  bs:floorArea  bs:staffCount  bs:founded

What to take away

  • Read the triple aloud, and if it is not a sentence then rename the property.
  • Name a hierarchical property going up, as in isPartOf, isMemberOf and hasBroader: one direction, fewer results, and no inverse needed.
  • Where an ontology departs from its own rules, say so on the term, because a recorded departure is a decision and a silent one is drift.

Measured

Parses26 triples; 6 object properties; 2 datatype properties; 5 annotation properties
Naming auditevery local name conforms
Profile (ROBOT)OWL 2 DL: in profile
Query: Properties whose name departs from the gist standard4 rows
  • bs:locatedIn "gist style would be isLocatedIn. Kept for the queries that use it."
  • bs:publishedBy "gist style would be isPublishedBy."
  • bs:author "gist style would be hasAuthor: a noun does not read in the middle of a triple."
  • bs:genre "gist style would be hasGenre, or isCategorizedBy."

Afterwards, in the SPARQL panel

Properties whose name departs from the gist standard (on the model)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?property ?note WHERE { ?property skos:editorialNote ?note . FILTER(CONTAINS(?note, "gist style would be")) }

Sources

O19

Labels: one per language, in the right case

Every term has exactly one skos:prefLabel per language, classes in title case and properties in lower case, alternatives in skos:altLabel; the rules are shapes, and they run in the editor.

Corner: symbol · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .

bs:Bookshop    a owl:Class ;            skos:prefLabel "Bookshop"@en ;    skos:altLabel "Shop"@en, "Store"@en .
bs:Settlement  a owl:Class ;            skos:prefLabel "Settlement"@en, "Anheddiad"@cy .
bs:Region      a owl:Class ;            skos:prefLabel "Region"@en, "Rhanbarth"@cy .
bs:floorArea   a owl:DatatypeProperty ; skos:prefLabel "floor area"@en .
bs:isbn        a owl:DatatypeProperty ; skos:prefLabel "ISBN"@en .

# Two labels wrong on purpose, for the shapes to find.
bs:Publisher   a owl:Class ;            skos:prefLabel "publisher"@en .
bs:heldAt      a owl:ObjectProperty ;   skos:prefLabel "Held at"@en .

Why

A preferred label is the one name a term shows in a list, and a list with two names for one thing is a list with a bug in it, which is why SKOS makes it an integrity condition, S14: at most one preferred label per language tag. gist asks for exactly one, and adds cases, with title case for classes, as in Physical Identifiable Item, lower case for properties, as in has unit of measure, and acronyms in capitals in either. Alternative labels are free-form and as plural as you please, which is what they are for.

gist enforces its label rules with SHACL shapes run on every commit, and the escape hatch for a label that is right and non- conforming, a proper noun say, is an annotation, gist:nonConformingLabel true, that the shape reads and respects. The same shapes, simplified, are in this lesson and in the editor: open them in a second tab, pick them in the Shapes dropdown and press Validate. Two of the labels here are wrong on purpose and the report names them. This is the SHACL course's whole method applied to a vocabulary instead of to data, and the file is one you could run against any ontology somebody hands you.

Language tags are the other half of it. The Trail labels places in Welsh and Gaelic where those names exist, and a query for @cy labels is what the Welsh page of the website will run; a label with no tag at all is a label in no language, and STY-003 in the Quality Suite says so, as an Info.

Diagram

   term             prefLabel                          rule

   bs:Bookshop      "Bookshop"@en                      title case, one per language
   bs:Settlement    "Settlement"@en  "Anheddiad"@cy    two languages, one each
   bs:floorArea     "floor area"@en                    lower case
   bs:isbn          "ISBN"@en                          acronym in capitals, allowed

   seeded:  bs:Publisher  "publisher"@en               not title case      -> a violation
            bs:heldAt     "Held at"@en                 not lower case      -> a violation
            bs:Region     "Region"@en  "Rhanbarth"@cy  fine

What to take away

  • One preferred label per language, with altLabels for everything else, and SKOS S14 makes the first of those a hard rule.
  • Title case for classes, lower case for properties, acronyms in capitals, and gist's shapes check it, which the editor will run for you.
  • A label with no language tag is a label in no language, so tag every one of them.

Note

The shapes are in label-shapes.ttl beside this lesson, and Open in the editor opens them in a second tab already selected; press Validate. The SHACL course's s16 covers sh:uniqueLang and s15 sh:pattern.

Measured

Parses23 triples; 4 classes; 1 object property; 2 datatype properties; 5 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
SHACL (5 shapes compiled)does not conform · 2 violations, 0 warnings, 0 info
SeverityFocus nodePathMessage
Violationbs:Publisherskos:prefLabelA class label is in title case: {$value}
Violationbs:heldAtskos:prefLabelA property label is lower case, acronyms excepted: {$value}

Sources

O20

Naming the particulars

Instance IRIs carry a type infix and a slug, never a role, and are minted once; the Trail's shop-inkwell and gist's _Bookshop_The_Inkwell are the same rule in two spellings.

Corner: symbol · Runs in the browser

@prefix bt:    <https://example.org/bookshop-trail/> .
@prefix bs:    <https://example.org/bookshop-trail/schema#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:   <http://www.w3.org/2002/07/owl#> .
@prefix skos:  <http://www.w3.org/2004/02/skos/core#> .
@prefix gistd: <https://w3id.org/semanticarts/ns/data/gist/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .

bs:Person  a owl:Class ; skos:prefLabel "Person"@en .
bs:Author  a owl:Class ; rdfs:subClassOf bs:Person ; skos:prefLabel "Author"@en ;
    skos:editorialNote "Instances are minted as author-<name>, against the rule that an infix is a rigid class. Person exists in the Trail only to hold authors, so the role never ends inside the dataset. Mint under person- the day a non-author person appears."@en .

bt:author-rhona-blackwood  a bs:Author, bs:Person ; skos:prefLabel "Rhona Blackwood"@en .
bt:shop-inkwell            a bs:Bookshop ;          skos:prefLabel "The Inkwell"@en .
bt:place-wigtown           a bs:Settlement ;        skos:prefLabel "Wigtown"@en .
bs:Bookshop    a owl:Class ; skos:prefLabel "Bookshop"@en .
bs:Settlement  a owl:Class ; skos:prefLabel "Settlement"@en .

# The same three, as gist would mint them.
gistd:_Person_Rhona_Blackwood  a bs:Person ;     skos:prefLabel "Rhona Blackwood"@en ; owl:sameAs bt:author-rhona-blackwood .
gistd:_Bookshop_The_Inkwell    a bs:Bookshop ;   skos:prefLabel "The Inkwell"@en ;     owl:sameAs bt:shop-inkwell .
gistd:_Settlement_Wigtown      a bs:Settlement ; skos:prefLabel "Wigtown"@en ;         owl:sameAs bt:place-wigtown .

Why

The Trail names its particulars with a lower-case type and a slug, as in shop-inkwell, place-wigtown, author-rhona-blackwood and event-inkwell-2025-03-08, while gist's convention is a leading underscore, the type, an underscore, and then the name with its case kept, giving _Bookshop_The_Inkwell. The spelling differs and the rule is the same, and it is the rule from module 02: the infix is the most specific rigid class the instance belongs to, and never a role, because the role may end and the IRI may not.

The Trail breaks its own rule exactly once, and the break is worth looking at. Authors are named author-rhona-blackwood, and Author is a role; gist's guide would have _Person_Rhona_Blackwood. The Trail's defence is that its Person class exists only to have authors in it, so that the role never ends within the dataset, which is a pragmatic argument, openly made, and the editorial note records it.

gist's guide allows a fallback as well, which is that when the data being mapped does not say which specific kind a row is, you use a higher class as the infix for the whole table, _Organization_ rather than guessing between corporations and charities. That is the same rule applied with a little humility, and it is better than an infix that is wrong for a third of the rows.

Diagram

   the Trail                        gist                              the rule

   bt:shop-inkwell                  gistd:_Bookshop_The_Inkwell       most specific rigid class + name
   bt:place-wigtown                 gistd:_Settlement_Wigtown
   bt:author-rhona-blackwood        gistd:_Person_Rhona_Blackwood     never a role (Author is one)
   bt:event-inkwell-2025-03-08      gistd:_Event_...                  a slug that will not change

   fallback when the kind is not known from the data: the nearest rigid class

What to take away

  • Infix the most specific rigid class, since it is the one thing about the instance that will not change.
  • Never name an instance after a role; the Trail does exactly that for its authors, and says why on the class.
  • When the source data cannot tell you the kind, use a higher one for the whole table rather than guess row by row.

Measured

Parses31 triples; 4 classes; 5 annotation properties; 6 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 4 inferred
  • bt:author-rhona-blackwood owl:sameAs gistd:_Person_Rhona_Blackwood
  • bt:place-wigtown owl:sameAs gistd:_Settlement_Wigtown
  • bt:shop-inkwell owl:sameAs gistd:_Bookshop_The_Inkwell
  • gistd:_Person_Rhona_Blackwood rdf:type bs:Author
Query: Instances whose IRI names a role1 row
  • bt:author-rhona-blackwood

Afterwards, in the SPARQL panel

Instances whose IRI names a role (on the model)
PREFIX oc: <https://example.org/bookshop-trail/ontoclean#>
SELECT DISTINCT ?instance WHERE { ?instance a ?class . FILTER(CONTAINS(STR(?instance), "/author-")) }

Sources

Module 04

Definitions and controlled vocabularies

This is the thought corner, written down. A definition is the agreement that makes a shared symbol carry a shared meaning, and a controlled vocabulary is a list of terms whose definitions somebody has agreed to look after; so the module covers the form of a good definition, the ways in which one fails, the set of annotations that carry it, and the decision that comes before any taxonomy at all, which is whether this thing is a class or a category.

Sources
O21

A definition is not a label

Every class and property gets a definition in the form 'a genus that differs thus', one sentence, and anything else that was in the comment moves to a scope note.

Corner: thought · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:hiddenLabel    a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:example        a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .

bs:Place
    a                owl:Class ;
    skos:prefLabel   "Place"@en ;
    skos:definition  "A named area or point on the map, at any scale."@en ;
    skos:scopeNote   "Covers settlements, council areas, regions and countries. A shop's premises are not a Place; the shop is located in one."@en ;
    skos:example     "Wigtown; Dumfries and Galloway; Yorkshire; Wales."@en .

bs:Settlement
    a                owl:Class ;
    rdfs:subClassOf  bs:Place ;
    skos:prefLabel   "Settlement"@en ;
    skos:definition  "A Place that is a town or city."@en ;
    skos:scopeNote   "The only kind of Place a bookshop is located in."@en ;
    skos:example     "Wigtown, Hay-on-Wye, Sedbergh; negative examples: Scotland, the Lake District."@en .

bs:TrailSegment
    a                owl:Class ;
    skos:prefLabel   "Trail Segment"@en ;
    skos:definition  "A walking route between two bookshops, with a length."@en ;
    skos:scopeNote   "Asserted once per pair; walkable in either direction."@en .

bs:locatedIn
    a                owl:ObjectProperty ;
    skos:prefLabel   "located in"@en ;
    skos:definition  "Relates a shop or publisher to the settlement it trades in."@en .

Why

A label is a symbol, whereas a definition is the thought written down, and it has a shape that has served since Aristotle: name the nearest kind the thing belongs to, and then say what distinguishes it from the other things of that kind. 'A Settlement is a Place that is a town or city' gives Place as the genus and town or city as the differentia, and somebody who has never seen the data can now decide whether a given thing is one, which is the whole test of a definition. OBO Foundry principle FP-006 asks for exactly this form, and for a definition on most classes as a condition of membership, while gist's guide asks for exactly one, written as a full sentence ending in a period, with the subject optional.

The Trail's shipped comments are a mixture of the two things. 'A town or city' is a definition, whereas 'the only kind of Place a bookshop sits in' is true and useful and is not one, being a note about how the term is used. SKOS gives the two their own properties, skos:definition and skos:scopeNote, and keeping them apart is what stops a definition growing into a paragraph that nobody reads; examples go in skos:example, including the negative ones, as gist's LivingThing does when it offers unicorns and Mickey Mouse as what it is not.

The audit in this lesson is rough, and deliberately so, since it flags a definition whose first sentence contains the term's own name, which catches circularity, as o22 shows, and nothing subtler than that. It finds nothing to complain about here.

Diagram

   "A Settlement is a Place that is a town or city."
                      -----       -----------------
                      genus       differentia

   skos:definition   one sentence, genus and differentia, ends with a period
   skos:scopeNote    how the term is used, what it does not cover
   skos:example      instances, and negative examples
   skos:prefLabel    the symbol; never a substitute for the sentence

What to take away

  • Genus and differentia, which is to say the nearest kind and then what distinguishes it, in one sentence.
  • A definition has to decide membership for somebody who has never seen the data, and if it cannot do that then it is a note rather than a definition.
  • Definition, scope note and example are three properties because they are three different jobs.

Measured

Parses30 triples; 3 classes; 1 object property; 12 annotation properties
Definition auditno circular definition
Profile (ROBOT)OWL 2 DL: in profile

Sources

O22

How a definition fails

Five failing definitions from the draft are named for what they are, circular, too broad, by example, negative and by IRI, and each is rewritten.

Corner: thought · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:hiddenLabel    a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:example        a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .

# The draft, kept so the audit can find its circular members.
bs:Publisher  a owl:Class ; skos:prefLabel "Publisher"@en ;
    skos:definition "An organisation that publishes books."@en ;
    skos:editorialNote "Circular. Repair: an organisation that issues works for sale under its own name."@en .
bs:Event      a owl:Class ; skos:prefLabel "Event"@en ;
    skos:definition "Something that happens at an event venue."@en ;
    skos:editorialNote "Circular and too broad. Repair: an occasion held at a bookshop on a given day, open to the public."@en .
bs:Genre      a owl:Class ; skos:prefLabel "Genre"@en ;
    skos:definition "Crime, poetry, nature writing, history and so on."@en ;
    skos:editorialNote "By example only. Repair: a category of works by subject and manner, used to shelve books and to describe what a shop is known for."@en .
bs:Bookshop   a owl:Class ; skos:prefLabel "Bookshop"@en ;
    skos:definition "A business that is not a publisher."@en ;
    skos:editorialNote "Negative. Repair: a business that sells books to the public from premises."@en .
bs:Source     a owl:Class ; skos:prefLabel "Source"@en ;
    skos:definition "The class bs:Source."@en ;
    skos:editorialNote "By IRI. Repair: a document or organisation that makes claims about shops, such as a guidebook, a survey or a register."@en .

Why

There are five ways a definition goes wrong and the draft has one of each. The circular, 'a publisher is an organisation that publishes', leaves the reader who did not know what publishing was no further on. The too broad, 'an event is something that happens', is true of a sneeze. The by example only, 'genre: crime, poetry, nature writing and so on', offers a list that is not the meaning, and 'and so on' is exactly where the next disagreement will live. The negative, 'a bookshop is a business that is not a publisher', says what the thing is not and leaves everything else in. And the by IRI, 'the class bs:Bookshop', could only be read by somebody who already had the ontology open in front of them.

Each has a repair and it is the same repair in every case: find the genus, find the differentia, write one sentence. A publisher is an organisation that issues works for sale under its own name; an event is an occasion held at a bookshop on a given day, open to the public; a genre is a category of works by their subject and manner, used to shelve books and to describe what a shop is known for.

The audit catches three of the five by looking for the term's own stem in the first sentence, those being the two circular ones and the one by IRI, which is circular in the plainest way since it simply names the term. The other two need a reader, and the SemOps Manual's point about cognitive budget applies here: a definition is read by many people once each, so a minute spent on it is repaid many times over, and a definition that makes the reader do the work is a cost that everybody pays.

Diagram

   failure          draft                                           repair

   circular         a publisher is an organisation that publishes    ... that issues works for sale under its own name
   too broad        an event is something that happens               ... an occasion held at a bookshop on a given day
   by example       genre: crime, poetry, and so on                  ... a category of works by subject and manner
   negative         a bookshop is a business that is not a publisher ... a business that sells books from premises
   by IRI           the class bs:Bookshop                            (the same)

   the audit finds three (circular twice, and by IRI); a reader finds the rest

What to take away

  • The reader who does not already know has to be able to decide membership from the sentence, and every failure here breaks that.
  • Circularity is mechanical to find and it is common; too broad and by example are commoner still, and they need a reader.
  • A definition is read many times by many people, which makes it the cheapest place in the whole vocabulary to spend a careful minute.

Measured

Parses32 triples; 5 classes; 12 annotation properties
Definition auditbs:Event, bs:Publisher, bs:Source repeat the term's own name
Profile (ROBOT)OWL 2 DL: in profile

Sources

O23

The annotation set

Each term carries prefLabel and definition exactly once, scopeNote and example where they help, editorialNote for the maintainers, and never rdfs:comment as a dumping ground.

Corner: thought · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:hiddenLabel    a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:example        a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .

bs:Bookshop
    a                   owl:Class ;
    skos:prefLabel      "Bookshop"@en, "Siop Lyfrau"@cy ;
    skos:altLabel       "Shop"@en, "Store"@en ;
    skos:definition     "A business that sells books to the public from premises."@en ;
    skos:scopeNote      "The business, not the building. A shop that moves premises keeps its identity; a building that changes hands does not become the new tenant."@en ;
    skos:example        "The Inkwell; Marginalia. Negative examples: a publisher's warehouse; an online-only seller."@en ;
    skos:editorialNote  "Definition agreed 12 March 2025. Chains are out of scope; revisit if one joins the trail."@en ;
    rdfs:seeAlso        <https://en.wikipedia.org/wiki/Bookselling> .

# The minimum missing, for the shapes to find.
bs:Series
    a                   owl:Class ;
    skos:prefLabel      "Series"@en .

Why

gist's guide sorts the annotations into required, recommended, use where relevant, use rarely and do not use, and the sorting is more useful than any single rule in it. Required are skos:prefLabel and skos:definition, exactly one of each; recommended are skos:scopeNote and skos:example; where relevant come skos:altLabel and skos:editorialNote, the last of those being for the maintainers rather than the users; rarely, skos:changeNote and skos:historyNote, since the version control system usually holds that; and not at all, rdfs:label and rdfs:comment, because SKOS gives a finer set and a comment becomes the place where everything ends up.

The Trail's shipped vocabulary uses rdfs:label and rdfs:comment, and its comments carry definitions, usage notes and provenance together in one string. That was a reasonable choice for a teaching dataset whose queries wanted a single property to read, and it is also exactly the case the guide warns about. This lesson shows bs:Bookshop with the full set beside a term with the minimum missing, and the shapes find the gap, as does the Quality Suite's QUA-010; its STY-004 fires as well, on the Welsh label, because that check assumes a label matches the local name and has no notion of a second language, which is worth knowing before it goes anywhere near a gate.

Cardinality is the rule that pays for itself. One label per language and one definition per term means a tool can pick the label and print the definition without having to choose between candidates, and a reader never has to wonder which of two sentences is the current one.

Diagram

   required           skos:prefLabel     exactly one per language
                      skos:definition    exactly one
   recommended        skos:scopeNote     usage, boundaries
                      skos:example       instances, negative examples
   where relevant     skos:altLabel      any number
                      skos:editorialNote for the maintainers
   rarely             skos:changeNote, skos:historyNote
   do not use         rdfs:label, rdfs:comment   (the Trail does; the note above says why)

What to take away

  • Two annotations are required and both have a cardinality of one, while everything else is optional and may be as plural as you like.
  • An editorial note is for whoever maintains the term and a scope note is for whoever uses it, which is why they are different properties for different readers.
  • rdfs:comment is where everything ends up when nothing else is provided, so provide something else.

Measured

Parses24 triples; 2 classes; 12 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
Ontology Quality Suite
  • QUA-010 ×1: Term https://example.org/bookshop-trail/schema#Series has no skos:definition.
  • STR-004 ×2: Class https://example.org/bookshop-trail/schema#Bookshop has no formal definition (no owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:un
  • STY-004 ×1: Term https://example.org/bookshop-trail/schema#Bookshop has skos:prefLabel 'Siop Lyfrau' which does not match its local name, suggesting the label and
SHACL (3 shapes compiled)does not conform · 1 violation, 0 warnings, 0 info
SeverityFocus nodePathMessage
Violationbs:Seriesskos:definitionExactly one definition.

Sources

O24

Class or category: when a concept becomes a class

A genre stays a concept, a particular that a work belongs to and a shop specialises in, and is promoted to a class only when something must be said about its members as a set.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:hiddenLabel    a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:example        a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .

skos:Concept    a owl:Class .
skos:broader    a owl:ObjectProperty .

bs:Work         a owl:Class ; skos:prefLabel "Work"@en .
bs:Bookshop     a owl:Class ; skos:prefLabel "Bookshop"@en .
bs:genre        a owl:ObjectProperty ; rdfs:domain bs:Work ;     rdfs:range skos:Concept .
bs:specialises  a owl:ObjectProperty ; rdfs:domain bs:Bookshop ; rdfs:range skos:Concept .

# The genre as a concept: what the Trail does.
bt:genre-fiction        a skos:Concept ; skos:prefLabel "Fiction"@en, "Ffuglen"@cy .
bt:genre-crime-fiction  a skos:Concept ; skos:prefLabel "Crime Fiction"@en ; skos:altLabel "Mystery"@en ;
    skos:broader bt:genre-fiction ;
    skos:scopeNote "Works whose plot turns on a crime and its investigation. Includes cosy crime and tartan noir."@en .

bt:book-the-book-town  a bs:Work ;     skos:prefLabel "The Book Town"@en ; bs:genre bt:genre-crime-fiction .
bt:shop-inkwell        a bs:Bookshop ; skos:prefLabel "The Inkwell"@en ;   bs:specialises bt:genre-crime-fiction .

# The same genre promoted to a class: what you would write if you needed
# to say something about every crime novel. A shop has no way to specialise
# in it, and the query below shows the asymmetry.
bs:CrimeFiction  a owl:Class ; rdfs:subClassOf bs:Work ; skos:prefLabel "Crime Fiction (as a class)"@en ;
    skos:definition "A Work whose plot turns on a crime and its investigation."@en .
bt:book-the-book-town  a bs:CrimeFiction .

Why

Every ontology has things that could be either a class or an individual, and I think this is the most consequential choice it makes. Crime fiction is the Trail's case. As a class, bs:CrimeFiction is a set of works, so that every crime novel is a member and anything true of the set, that its members have a detective in them, say, can be written as an axiom and inherited. As a particular, bt:genre-crime-fiction is a thing in its own right, with a Welsh label, a broader concept, a scope note and, decisively, the property of being what The Inkwell specialises in; and a shop cannot specialise in a set of works by being a member of it.

The test is a set-theoretic one. If you need to say things about the members, as a set, that a reasoner should then apply to each of them, it is a class, and rdfs:subClassOf is subset; if instead you need to say things about the category itself, to point other kinds of thing at it, to arrange it in a hierarchy that curators will rearrange, or to carry a few thousand of them, then it is a concept or a category, and skos:broader is a relation between two particulars that promises nothing whatever about members. gist says it outright in the scope note on gist:Category, which recommends a category if you do not much care about the formal structure of the different types, or if there is a whole hierarchy of them.

The promotion happens when the first axiom about members is needed, and OWL 2 allows both readings at once by punning, with one IRI that is an owl:Class and a skos:Concept and rdfs:subClassOf running in parallel with skos:broader. It costs a second reading of every term, and it is the right answer for a vocabulary that is both a shelf plan and a reasoning target. The Trail needs only the shelf plan, so it stops at the concept, and the query at the end shows what the other choice would cost: as a class, the shop's specialism has nowhere to point.

Diagram

   as a concept (a particular)                 as a class (a set)

   bt:genre-crime-fiction                      bs:CrimeFiction
     a skos:Concept                              a owl:Class ; rdfs:subClassOf bs:Work
     skos:broader bt:genre-fiction               members: every crime novel
     skos:prefLabel "Crime Fiction"@en           axioms about members are inherited
     <- bt:shop-inkwell bs:specialises           <- a shop cannot be a member of it
     <- bt:book-x bs:genre

   promote when the first axiom about the members is needed
   both at once: punning, subClassOf and broader in parallel

What to take away

  • A class is a set you need to say things about and a concept is a thing you need to point at, so ask which of the two you need before you type owl:Class.
  • Promotion is triggered by the first axiom about members, and until then a concept scheme is cheaper, more flexible and truer to what is actually known.
  • Punning lets one IRI be both, and it is worth paying for only when the vocabulary is a shelf plan and a reasoning target at once.

Measured

Parses43 triples; 4 classes; 3 object properties; 12 annotation properties; 2 concepts; 4 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; nothing new
Query: What points at the genre as a concept2 rows
  • bt:book-the-book-town bs:genre
  • bt:shop-inkwell bs:specialises
Query: What is a member of the genre as a class1 row
  • bt:book-the-book-town

Afterwards, in the SPARQL panel

What points at the genre as a concept (on the model)
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?thing ?property WHERE { ?thing ?property bt:genre-crime-fiction }
What is a member of the genre as a class (on the model)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?member WHERE { ?member a bs:CrimeFiction }

Sources

O25

A controlled vocabulary before a taxonomy

The event kinds stop being strings in the data and become a controlled list of seven terms with one preferred label each, synonyms, a scope note and a maintainer.

Corner: thought · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:hiddenLabel    a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:example        a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .

bs:EventKind  a owl:Class ; skos:prefLabel "Event Kind"@en ;
    skos:definition "A kind of event a bookshop holds, from the Trail's controlled list."@en ;
    skos:editorialNote "Maintained by the events secretary. Additions by committee only."@en .
bs:eventKind  a owl:ObjectProperty ; rdfs:range bs:EventKind ; skos:prefLabel "event kind"@en .

bt:kind-reading   a bs:EventKind ; skos:notation "RD" ; skos:prefLabel "Reading"@en ;   skos:altLabel "Author event"@en, "reading"@en ;
    skos:scopeNote "An author reads from a work and takes questions. Not a signing, though one may follow."@en .
bt:kind-signing   a bs:EventKind ; skos:notation "SG" ; skos:prefLabel "Signing"@en ;
    skos:scopeNote "An author signs copies. No reading."@en .
bt:kind-launch    a bs:EventKind ; skos:notation "LN" ; skos:prefLabel "Launch"@en ;
    skos:scopeNote "The first public event for a new work, at or near publication."@en .
bt:kind-workshop  a bs:EventKind ; skos:notation "WS" ; skos:prefLabel "Workshop"@en ;
    skos:scopeNote "Participants write or make something. Ticketed, usually."@en .
bt:kind-panel     a bs:EventKind ; skos:notation "PN" ; skos:prefLabel "Panel"@en ;
    skos:scopeNote "Several speakers in discussion, with a chair."@en .
bt:kind-book-club a bs:EventKind ; skos:notation "BC" ; skos:prefLabel "Book Club"@en ; skos:altLabel "Reading group"@en ;
    skos:scopeNote "A recurring meeting to discuss one work. The author is not usually present."@en .
bt:kind-lecture   a bs:EventKind ; skos:notation "LC" ; skos:prefLabel "Lecture"@en ;  skos:altLabel "Talk"@en ;
    skos:scopeNote "One speaker on a subject, not necessarily an author of a work in stock."@en .

# Three events, migrated.
bt:event-1  a bs:Event ; bs:eventKind bt:kind-reading .
bt:event-2  a bs:Event ; bs:eventKind bt:kind-reading .
bt:event-3  a bs:Event ; bs:eventKind bt:kind-lecture .
bs:Event    a owl:Class .

Why

The Trail's events carry bs:eventKind as a string, with values of Reading, Signing, Launch, Workshop, Panel, Book Club and Lecture, and the vocabulary lists the seven of them in a comment, which is the only thing standing between us and an eighth spelt 'reading'. A string that ought to be a thing is the commonest smell in a dataset, and the cure is the oldest technique in information science, which is a controlled vocabulary. ANSI/NISO Z39.19 sets out what one is: each concept has exactly one preferred term, every synonym is recorded and points at it, each term has a scope note saying what it does and does not cover, and somebody is named as responsible for the list.

None of that needs a hierarchy. A controlled vocabulary is a flat list with discipline, and it is where most vocabularies should start and where a good many of them should stop; the hierarchy, when it is needed, is the next rung of the ladder in module 06. gist has the same shape in its own terms, where a gist:ControlledVocabulary is a collection of gist:Category instances governed by an organisation or a person.

So the seven kinds become seven particulars in this file, each with a notation, a preferred label, its synonyms as alternative labels and a scope note, and 'Talk' and 'Author event' are recorded as synonyms of Lecture and Reading because the spreadsheet had them. The query at the end counts events by kind after the migration, which is what the website will do and what the strings could never have done reliably.

Diagram

   before                                    after

   bs:eventKind  "Reading"                   bs:eventKind  bt:kind-reading
   bs:eventKind  "reading"    (an eighth)    bt:kind-reading  a bs:EventKind ;
   bs:eventKind  "Author event"                 skos:notation "RD" ;
                                                skos:prefLabel "Reading"@en ;
                                                skos:altLabel "Author event"@en, "reading"@en ;
                                                skos:scopeNote "An author reads from a work ..." .

   one preferred term, synonyms recorded, a scope note, a named maintainer

What to take away

  • A string that is one of a fixed set is a thing pretending to be a value, so give it an IRI.
  • A controlled vocabulary is a flat list with four disciplines: one preferred term, recorded synonyms, scope notes, and a named maintainer.
  • Start flat, and add the hierarchy when a question needs it rather than before.

Measured

Parses58 triples; 2 classes; 1 object property; 12 annotation properties; 10 individuals
Profile (ROBOT)OWL 2 DL: in profile
Query: Events by kind, by preferred label7 rows
  • "Reading" "2"
  • "Lecture" "1"
  • "Signing" "0"
  • "Launch" "0"
  • "Workshop" "0"
  • "Panel" "0"
  • "Book Club" "0"

Afterwards, in the SPARQL panel

Events by kind, by preferred label (on the model)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?kind (COUNT(?event) AS ?events) WHERE {
  ?k a bs:EventKind ; skos:prefLabel ?kind .
  OPTIONAL { ?event bs:eventKind ?k }
} GROUP BY ?kind ORDER BY DESC(?events)

Sources

O26

Borrow the term, or borrow the definition

A term is reused by IRI only when its published meaning is the meaning wanted; otherwise the Trail mints its own and cites the source; and every borrowed term is declared.

Corner: symbol and thought · Runs in the browser

@prefix bs:    <https://example.org/bookshop-trail/schema#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:   <http://www.w3.org/2002/07/owl#> .
@prefix xsd:   <http://www.w3.org/2001/XMLSchema#> .
@prefix skos:  <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:   <http://purl.org/dc/terms/> .
@prefix geo:   <http://www.opengis.net/ont/geosparql#> .
@prefix wgs84: <http://www.w3.org/2003/01/geo/wgs84_pos#> .
@prefix gist:  <https://w3id.org/semanticarts/ns/ontology/gist/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:hiddenLabel    a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:example        a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .

# Borrowed by IRI, and declared: the published meaning is the meaning wanted.
skos:Concept       a owl:Class ;          rdfs:isDefinedBy <http://www.w3.org/2004/02/skos/core> .
geo:Feature        a owl:Class ;          rdfs:isDefinedBy <http://www.opengis.net/ont/geosparql> .
geo:hasGeometry    a owl:ObjectProperty ; rdfs:isDefinedBy <http://www.opengis.net/ont/geosparql> .
wgs84:lat          a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:isDefinedBy <http://www.w3.org/2003/01/geo/wgs84_pos> .
dct:title          a owl:AnnotationProperty .
dct:isPartOf       a owl:AnnotationProperty ;
    skos:editorialNote "Defined only in RDFS by DCMI, so declared here as an annotation property to stay in OWL 2 DL."@en .
rdfs:isDefinedBy   a owl:AnnotationProperty .

# Minted, with the definition borrowed and cited.
bs:Person  a owl:Class ; skos:prefLabel "Person"@en ;
    skos:definition "A human being who was or is alive."@en ;
    dct:source      gist:Person ;
    skos:editorialNote "Definition after gist:Person. Minted rather than reused so that the Trail's files stand alone; module 09 adds the bridge."@en .

# Minted, with the search recorded.
bs:TrailSegment  a owl:Class ; skos:prefLabel "Trail Segment"@en ;
    skos:definition "A walking route between two bookshops, with a length."@en ;
    skos:editorialNote "Considered gist:GeoRoute (an ordered set of points; ours has two ends and a length) and gist:NetworkLink. Neither said 'walkable'. Minted."@en .

Why

Reuse is a principle in every guide, since OBO FP-007 requires existing relations to be reused rather than reinvented and gist exists so that person, organisation and agreement need not be defined all over again. The reason goes back to the triangle: two ontologies that share an IRI share a referent, and their data joins without a mapping, whereas two that each mint a Person need a mapping, and a mapping is a place for an argument.

Reusing an IRI means reusing its definition and its semantics, though, all of them. dct:title is safe enough, being an annotation that says nothing a reasoner will act on; schema:Book arrives with a hierarchy under schema:CreativeWork and a loose contract about what a Book is; and dct:isPartOf is defined in RDFS only, which is why the Trail declares it an annotation property in order to stay inside OWL 2 DL. So the rule has three branches. Borrow the IRI when the published definition is the one you want, and declare it in your own file so that the profile checker knows what it is; borrow the definition, citing it with dct:source, when the words are right and the semantics are not; and mint your own when neither fits, saying what you looked at first.

The Trail borrows skos:, geo:, wgs84:, prov: and dct: terms by IRI and declares every one of them, which is what lets a file of its own stand as OWL 2 DL while importing nothing. It mints bs:Person rather than reusing gist:Person or foaf:Person, and module 09 shows what that costs in bridge axioms.

Diagram

   want                                     do                              example

   the published meaning, exactly           reuse the IRI, declare it       skos:prefLabel, geo:hasGeometry
   the words, not the semantics             mint, cite with dct:source      bs:Person (source: gist:Person)
   neither                                  mint, say what you looked at    bs:TrailSegment

   a shared IRI is a shared referent: data joins with no mapping
   a borrowed IRI brings its whole definition, and its hierarchy, and its profile

What to take away

  • Reuse an IRI when you mean exactly what its owner means, and then declare it in your own file so that the checker knows its type.
  • Borrow a definition and cite it where the words fit and the semantics do not.
  • Mint your own when neither fits, and record what you considered, because the note is a good deal cheaper than the second argument.

Measured

Parses34 triples; 4 classes; 1 object property; 1 datatype property; 15 annotation properties
Profile (ROBOT)OWL 2 DL: in profile

Sources

O27

Once published, a term means what it meant

A definition may be clarified but never changed to denote different things; when the meaning must change, a new term is minted and the old one deprecated with a pointer.

Corner: thought · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:hiddenLabel    a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:example        a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:notation       a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .

owl:deprecated  a owl:AnnotationProperty .

bs:Work
    a                 owl:Class ;
    skos:prefLabel    "Work"@en ;
    skos:definition   "A book: what an author wrote, in a particular published form."@en ;
    skos:historyNote  "Defined 2025 as 'A book. Translations are Works in their own right.' Reworded 2026 without change of meaning."@en .

# What the split would look like the day a second edition appears. The old
# term keeps its meaning and its data; the new term carries the new meaning.
bs:PublishedWork
    a                 owl:Class ;
    owl:deprecated    true ;
    skos:prefLabel    "Published Work"@en ;
    skos:definition   "A book with an ISBN, as first modelled."@en ;
    skos:changeNote   "Deprecated 2026: the meaning moved to bs:Edition when works acquired more than one edition. Not deleted; assertions made under it remain true."@en ;
    dct:isReplacedBy  bs:Edition .

bs:Edition
    a                 owl:Class ;
    skos:prefLabel    "Edition"@en ;
    skos:definition   "A printing of a Work by a publisher, with an ISBN if made after 1970."@en .

Why

OBO Foundry principle FP-019 says it in a line, and it is the one most often broken: the definition of a term must always denote the same things. Clarify the wording, correct a typo or add an example and it is still the same term; widen it to cover something it did not cover before, or narrow it, and every dataset that used the old meaning is now silently wrong, with no error raised anywhere.

The Trail's case is bs:Work, which o10 recorded as a work and an edition in one class, and the day a second edition appears that class has to split. The wrong move is to redefine bs:Work as a work independent of edition and start typing editions differently, because every existing bs:Work assertion was made under the old meaning; the right move is a new term for the new meaning, bs:Edition say, with a deprecation on anything whose meaning has moved and a pointer to what replaces it. gist's deprecation policy, which module 11 covers, is the mechanics of that, and this lesson is the principle behind it.

skos:historyNote and skos:changeNote are where the story goes. gist uses them rarely because git holds the history, which is fair enough for an ontology whose users read the repository; for one whose users read the terms, a sentence on the term itself is worth a good deal more.

Diagram

   allowed: same referents, better words          not allowed: new referents, same IRI

   "A book."                                       "A book."   ->   "A work, independent of edition."
     -> "A book: what an author wrote,               every bs:Work assertion made under the
         in a particular published form."            old meaning is now silently wrong

   the move: mint bs:Edition, deprecate what changed, point from old to new
   owl:deprecated true ; dct:isReplacedBy bs:Edition ; skos:changeNote "..."

What to take away

  • Clarify freely, but never change what a term denotes; that is FP-019, and every guide agrees with it.
  • When the meaning has to move, mint a new IRI and deprecate the old one with a pointer, so that the data made under the old meaning stays true.
  • A change note on the term is worth more to a user than a commit message they will never see.

Measured

Parses26 triples; 3 classes; 13 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
Ontology Quality Suite
  • STR-004 ×3: Class https://example.org/bookshop-trail/schema#Edition has no formal definition (no owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:uni
Query: Deprecated terms and what replaces them1 row
  • bs:PublishedWork bs:Edition

Afterwards, in the SPARQL panel

Deprecated terms and what replaces them (on the model)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?old ?new WHERE { ?old owl:deprecated true ; dct:isReplacedBy ?new }

Sources

O28

Minimal: as few terms as will answer the questions

The Trail has twenty-five classes because ten questions need them; a class that no question needs is not added, and gist's ninety-six for a whole enterprise is the standard to be measured against.

Corner: thought · Data: 01-vocabulary.ttl · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Twenty-five classes, thirty-two object properties, forty-two datatype properties, ten annotation properties, for ten competency questions. Compare gist 14: about ninety-six classes for an enterprise."@en .

# One class and its question, side by side.
bs:TrailSegment
    a                owl:Class ;
    skos:prefLabel   "Trail Segment"@en ;
    skos:definition  "A walking route between two bookshops, with a length."@en ;
    skos:scopeNote   "Exists for question 3: what can be walked to from a shop, and how far."@en .

# One that was proposed and refused.
bs:Festival
    a                   owl:Class ;
    owl:deprecated      true ;
    skos:prefLabel      "Festival"@en ;
    skos:editorialNote  "Proposed because Wigtown and Hay have festivals. Refused: no competency question mentions one. Kept as a deprecated stub so the refusal is on record."@en .
owl:deprecated  a owl:AnnotationProperty .

Why

gist describes itself as offering the maximum coverage of typical business ontology concepts with the fewest primitives and the least ambiguity, and it manages that in about a hundred classes, sixty-odd object properties and fifty datatype properties, which is enough for an entire enterprise. The Trail, with ten questions to answer, has twenty-five classes and eighty-four properties, and it is the properties that are worth watching, because every one of them is a column that somebody will have to fill.

A term costs the same whether it is used or not, since it needs a label in each language, a definition, a review, a place in the documentation, a check in the gate, and a decision every time somebody wonders whether to use it or the one next to it. Worse, a term nobody uses is a term defined badly, because nobody ever finds out. The discipline is the one from o07, that a term is admitted when a question needs it, with the question written down beside it.

The same discipline applies to axioms. gist keeps its domains and ranges sparse and declares no inverses at all, not out of laziness but because each of those is a commitment that some future dataset will break, and o31 takes the two of them up. Minimal does not mean small for its own sake; it means the number of terms at which every one of them is still being looked after.

Diagram

                           classes   object props   datatype props   for
   gist 14                    ~96          ~66             ~50       an enterprise
   the Trail                   25           32              42       ten questions

   each term costs: a label per language, a definition, a review,
   a place in the docs, a check in the gate, and a decision every
   time someone wonders whether to use it

What to take away

  • A term is admitted by a question rather than by a noun in the brief, so keep the question beside the term.
  • Properties are the number to watch, since each one is a column somebody fills, and the Trail has nearly as many of them as gist has classes.
  • Minimal means that every term is still being looked after, and that is the size to aim for.

Measured

Parses17 triples; 2 classes; 6 annotation properties (with the data: 522 triples)
Profile (ROBOT)OWL 2 DL: in profile
OQuaRE (scripts/metrics.py)18 classes, 25 object properties, 40 datatype properties, 12 roots, 15 leaves
MetricValueScore 1 to 5
LCOMOnto1.45
WMCOnto3.1115
DITOnto25
NACOnto0.45
NOCOnto15
CBOOnto15
RFCOnto6.1673
NOMOnto1.7225
RROnto72%4
AROnto172%5
INROnto139%5
CROnto0%1
ANOnto217%5
TMOnto15
Query: Every class in the shipped vocabulary, with what it exists for17 rows
  • bs:Author "Someone who wrote at least one work in this dataset."
  • bs:Bookshop "An independent bookshop on the trail."
  • bs:CouncilArea "A local authority area."
  • bs:Country "Scotland, England or Wales."
  • bs:DataIssue "A finding produced by the validation report in q88. Nothing in the shipped data has this type; the CONSTRUCT mints them."
  • bs:Event "Something that happened at a bookshop on a given day."
  • bs:Person "A human being."
  • bs:Place "Anywhere on the map: a settlement, a council area, a region or a country."

Afterwards, in the SPARQL panel

Every class in the shipped vocabulary, with what it exists for (on the data)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?class ?comment WHERE { ?class a owl:Class ; rdfs:comment ?comment . FILTER(STRSTARTS(STR(?class), "https://example.org/bookshop-trail/schema#")) } ORDER BY ?class

Sources

O29

Meaning in the ontology, rules in the shapes

An axiom is written only when it is true of every dataset the ontology could describe; anything that is a rule about this dataset, for this purpose, is a SHACL shape, and bs:founded stays non-functional for exactly that reason.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .

bs:Bookshop  a owl:Class ; skos:prefLabel "Bookshop"@en .

# The axiom that turns a disagreement into a contradiction.
bs:founded
    a                owl:DatatypeProperty, owl:FunctionalProperty ;
    rdfs:domain      bs:Bookshop ;
    skos:prefLabel   "founded"@en ;
    skos:definition  "The year a shop began trading."@en ;
    skos:editorialNote "Declared functional here to show what happens. The shipped vocabulary does not, because sources disagree for four shops."@en .

bt:shop-ex-libris
    a           bs:Bookshop ;
    bs:founded  "1921"^^xsd:integer, "1923"^^xsd:integer .

Why

An OWL axiom is a claim about the world, holding for every dataset that uses the term, for ever, and a reasoner will draw conclusions from it whether or not anybody wanted them. A SHACL shape is a rule about a dataset, saying that this data, for this website, must look like this, and a validator will report wherever it does not. The two look much alike on the page and behave nothing alike, and gist's tenth rule for restrictions is the test that tells them apart: if an instance of X did not conform to Y, would it still be an X? If it would, then Y is a rule rather than meaning, and it belongs in a shape.

The Trail's bs:founded is exactly that case. Every shop has one founding year, which is surely true; but four shops have two of them recorded, because the guidebooks disagree, and the brief asks for both to be kept. Declare the property functional and the reasoner does not flag a disagreement at all: it concludes that 1921 and 1923 are the same value, which for literals is a contradiction, and so the whole dataset becomes inconsistent. This lesson's file does precisely that, and HermiT says so. The shipped vocabulary leaves bs:founded non-functional and says why in its comment, while the SHACL course's s04 carries 'exactly one founding year' as a shape, where a second value is a finding to read rather than a contradiction to resolve.

The general habit worth acquiring is to constrain relationships rather less than feels natural. A domain that is too narrow retypes things, as o38 shows; a cardinality that is too tight merges things or contradicts them, as o47 shows; and a property declared functional because it always is becomes false on the day a second source appears. gist keeps its domains and ranges sparse for this reason and puts data constraints in shapes. Meaning in the ontology, rules in the shapes, and where you are in doubt, a shape.

Diagram

   "every shop has exactly one founding year"

   as an axiom                                    as a shape
   bs:founded a owl:FunctionalProperty            sh:path bs:founded ; sh:maxCount 1
   two values => the reasoner says                two values => the validator reports
   INCONSISTENT, for the whole dataset            one row, naming the shop; severity is yours

   the test (gist rule 10):
   if a shop had two founding years on record, would it still be a shop?  yes  => a shape

What to take away

  • An axiom holds for every dataset for ever, where a shape holds for this dataset for this purpose, so ask which of the two you actually mean.
  • Ask whether it would still be an X, and if it would then you have a rule about data, which belongs in SHACL.
  • Constrain relationships less than feels natural, because every tight constraint is a future contradiction with a date on it.

Note

Open in the editor opens founded-shape.ttl in a second tab, and pressing Validate gives one warning, naming the shop, and reports that the graph does not conform, because under the specification's default a warning breaks conformance. Builds of the engine before 0.3, and pySHACL with --allow-warnings, count violations alone and would say conforms over the same result, so read the counts rather than the verdict, which is what the SHACL course's s03 is about. Either way the shop is named and the graph is not contradicted, whereas the reasoner's verdict on the same two triples is that the whole graph is inconsistent.

Measured

Parses16 triples; 1 class; 1 datatype property; 5 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
HermiTinconsistent
SHACL (2 shapes compiled)does not conform · 0 violations, 1 warning, 0 info
SeverityFocus nodePathMessage
Warningbt:shop-ex-librisbs:foundedmore than one founding year

Sources

O30

Pragmatism and purity

A departure from a principle is allowed when it changes only a name or a convenience, refused when it changes an inference or a query, and written on the term either way.

Corner: thought · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .

bs:Work       a owl:Class ;
    skos:editorialNote "Compromise: work and edition in one class. Changes an identity criterion; allowed because the dataset never needs two editions of one work. Split the day it does."@en .
bs:author     a owl:ObjectProperty ;
    skos:editorialNote "Compromise: gist style would be hasAuthor. Changes a name only; 145 queries use this one."@en .
bs:wrote      a owl:ObjectProperty ; owl:inverseOf bs:author ;
    skos:editorialNote "Compromise: gist declares no inverses. Changes what a reasoner derives; kept on purpose so that module 08 has an inference to show, and the SPARQL course's q44 something to construct."@en .
bs:founded    a owl:DatatypeProperty ;
    skos:editorialNote "Compromise: values are xsd:gYear, outside the OWL 2 datatype map. Changes the profile of the data, not the vocabulary; kept for the SPARQL course's portability lesson. A conformant copy ships beside it."@en .
bs:Author     a owl:Class ;
    skos:editorialNote "Compromise: instances minted as author-, a role name. Changes a name only."@en .

Why

Every ontology that is actually used has compromises in it, and the good ones have them written down. The Trail has five. bs:Work is a work and an edition together; four properties are named against the gist standard; inverses are declared, which gist forbids, so that a reasoning lesson has something to infer; xsd:gYear is kept for years even though it puts the data outside OWL 2 DL, because the portability trap it creates is one of the SPARQL course's best lessons; and authors are minted under a role name. Each of them sits on its term as an editorial note, and this file collects the five together.

The rule that decides is about consequences. A departure that changes only a name or a convenience is cheap, since nothing a reasoner does depends on whether the property is called author or hasAuthor, while 145 queries depend on the first; a departure that changes an inference or a query result is not cheap at all, because it will be paid for by somebody who does not know it is there. Declaring inverses changes what a reasoner derives, and the Trail does it deliberately and says so, which is what makes it a decision rather than an accident.

Purity has its own costs, of course. BFO is philosophically rigorous and its IRIs are numbers; gist chose everyday words and a hundred classes; and both are used by people who had to ship something. Chapter 14 of the SemOps Manual is a ledger of what its toolchain does not do, and that is the model to follow, because an ontology should carry its own ledger. The unrecorded compromise is the only kind that is a mistake.

Diagram

   compromise                          changes           verdict          recorded on

   Work = work + edition               an identity        allowed, noted   bs:Work
   author, not hasAuthor               a name only        allowed, noted   the property
   inverses declared                   an inference       allowed on purpose, noted
   xsd:gYear kept                      a profile          allowed for the lesson, noted
   author- as an instance infix        a name only        allowed, noted   bs:Author

   rule: cheap if it changes a name; dear if it changes an inference or a result; write it down either way

What to take away

  • The unrecorded compromise is the only kind that is a mistake, so write each one on the term it affects.
  • Weigh a departure by what it changes, since a name is cheap and an inference or a query result is dear.
  • Rigour and usefulness are both virtues, and an ontology should carry a ledger of where it traded the one for the other.

Measured

Parses16 triples; 2 classes; 2 object properties; 1 datatype property; 5 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
Query: The ledger: every recorded compromise5 rows
  • bs:Author "Compromise: instances minted as author-, a role name. Changes a name only."
  • bs:Work "Compromise: work and edition in one class. Changes an identity criterion; allowed because the dataset never needs two editions of one work. Split the day it does."
  • bs:author "Compromise: gist style would be hasAuthor. Changes a name only; 145 queries use this one."
  • bs:founded "Compromise: values are xsd:gYear, outside the OWL 2 datatype map. Changes the profile of the data, not the vocabulary; kept for the SPARQL course's portability lesson. A conformant copy ships beside it."
  • bs:wrote "Compromise: gist declares no inverses. Changes what a reasoner derives; kept on purpose so that module 08 has an inference to show, and the SPARQL course's q44 something to construct."

Afterwards, in the SPARQL panel

The ledger: every recorded compromise (on the model)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?term ?note WHERE { ?term skos:editorialNote ?note . FILTER(STRSTARTS(?note, "Compromise:")) } ORDER BY ?term

Sources

O31

Disjoint at the top, sparse in the middle, no inverses

The top-level kinds are declared pairwise disjoint, domains and ranges are declared only where every use will agree, and inverses are not declared unless a lesson needs one.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .

bs:Place a owl:Class . bs:Bookshop a owl:Class . bs:Person a owl:Class .
bs:Publisher a owl:Class . bs:Work a owl:Class . bs:Event a owl:Class .
[] a owl:AllDisjointClasses ; owl:members ( bs:Place bs:Bookshop bs:Person bs:Publisher bs:Work bs:Event ) .

bs:locatedIn
    a            owl:ObjectProperty ;
    rdfs:domain  [ a owl:Class ; owl:unionOf ( bs:Bookshop bs:Publisher ) ] ;
    rdfs:range   bs:Place ;
    skos:scopeNote "Domain is a union: shops and publishers both have one. A domain of Bookshop alone would retype every publisher."@en .

bs:claimedBy
    a            owl:ObjectProperty ;
    rdfs:range   bs:Source ;
    skos:scopeNote "No domain. The subject is a claim about a statement, which no class here describes."@en .
bs:Source  a owl:Class .

bs:imprintOf
    a            owl:ObjectProperty ;
    rdfs:domain  bs:Publisher ;
    rdfs:range   bs:Publisher ;
    skos:scopeNote "Child to parent, the direction with fewer results. No hasImprint declared here; ask for ^bs:imprintOf."@en .

# What the disjointness catches: one thing typed as two kinds.
bt:oddity  a bs:Place, bs:Bookshop .

Why

gist's README lists its structural habits in a single breath: sparse domain and range specifications for broader applicability, no inverse properties at all, and extensive fine-grained disjointness at the highest level. The three go together, and each of them is a bet about the future.

Disjointness at the top is the cheap axiom that catches the expensive mistake, since nothing is both a place and a bookshop, and saying so lets a reasoner notice when a data load types one thing as both. The Trail declares its ten top-level kinds disjoint in one owl:AllDisjointClasses and the four subkinds of Place in another, and then it stops, because Author and TrailMember are roles and a shop can perfectly well be both a member and a shop; disjointness lower down is where the tenth rule from o29 starts to bite.

Sparse domains and ranges are the opposite bet. A domain is an inference rule, saying that whatever has this property is of this class, so that if you state bs:locatedIn's domain as Bookshop then every publisher with an address becomes a bookshop; the Trail states a union instead, and leaves bs:claimedBy with no domain whatever, its subject being a statement. gist goes further still and leaves most domains open, on the principle that a property is more reusable than any class it happened to be used with first.

No inverses is the bet that one direction will do. Every inverse doubles the query paths, the data, the shapes and the reasoner's work, and buys nothing that a SPARQL caret or a SHACL inverse path does not already give you; gist removed all of them in version 12 and picks the direction with fewer results, from child to parent. The Trail keeps two of them on purpose, as o30 explains.

Diagram

   disjoint at the top      [] a owl:AllDisjointClasses ; owl:members ( Place Bookshop Person Publisher Work ... )
                            cheap; catches a thing typed as two kinds at once

   sparse in the middle     bs:locatedIn  rdfs:domain [ unionOf ( Bookshop Publisher ) ]
                            bs:claimedBy  no domain: its subject is a statement
                            a domain is an inference rule, and a narrow one retypes things

   no inverses              isPartOf, not hasPart; ^bs:imprintOf in SPARQL, sh:inversePath in SHACL
                            one direction, fewer results, half the paths

What to take away

  • Declare the top-level kinds disjoint, since it is one axiom and it catches the worst data error there is.
  • A domain is an inference rule, so declare one only where every use of the property will agree with it, and reach for a union where two will not.
  • One direction per relationship, because the query language and the shapes language will both walk it backwards for nothing.

Measured

Parses45 triples; 7 classes; 3 object properties; 5 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
HermiTinconsistent

Sources

O32

One parent, mostly

Every backbone class has one parent; a second parent is allowed only for a rigid subkind that is both things in every circumstance, and the tangledness is measured rather than assumed.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .

bs:Work         a owl:Class .
bs:Translation  a owl:Class ; rdfs:subClassOf bs:Work ;
    skos:scopeNote "One parent. A translation is a work in every circumstance."@en .
bs:Bookshop     a owl:Class .
bs:Publisher    a owl:Class .

# The seeded second parent, for the metric to count.
bs:PublisherBookshop  a owl:Class ; rdfs:subClassOf bs:Bookshop, bs:Publisher ;
    skos:editorialNote "Two parents: a shop that also publishes. Wrong: publishing is a role, and belongs on the instance."@en .

# The alternative: the role on the instance.
bs:publishes  a owl:ObjectProperty ; rdfs:range bs:Work .
bt:shop-marginalia  a bs:Bookshop ; bs:publishes bt:work-pamphlet .
bt:work-pamphlet    a bs:Work .

Why

Multiple inheritance is legal in OWL and is usually a sign that two questions have been answered with one class. A class with two parents has two identity criteria to reconcile and two sets of inherited axioms to keep consistent, and a reader has to hold both of them in mind to know what it means. OQuaRE measures the habit as tangledness, TMOnto, the mean number of parents per class, and gives full marks only at two or fewer.

The legitimate case is a rigid subkind that is both things in every circumstance, and bs:Translation is one, being a Work and nothing else besides. A hypothetical bs:PublisherBookshop, a shop that also publishes, would be tempting and wrong, because it is a Bookshop that plays the publishing role, and the role goes on the instance rather than into the class tree; OntoClean's rule from module 02 sorts these out before the tree is ever drawn.

gist's answer is a different one again. Its classes are mostly single-parented, and where a class has several conditions it is defined by an owl:equivalentClass over an intersection, which the reasoner then classifies rather than a human asserting a second parent. That is not tangledness but the reasoner doing the tangling and showing its working. The metric run on this lesson's file counts the one seeded second parent, and run on gist it finds the classes whose asserted parents number more than one, of which there are very few.

Diagram

   single inheritance            seeded second parent                   the alternative

   Work                          Bookshop   Publisher                   Bookshop
     Translation                     \       /                             ^
                                 PublisherBookshop  (two parents)     bt:shop-x a bs:Bookshop ;
                                 two identity criteria, two axiom          bs:publishes bt:work-y   (a role, on the instance)
                                 sets, one confused reader

   TMOnto: mean parents per class, over classes with a parent. Score 5 at 2 or below.

What to take away

  • One parent per backbone class, and a second parent is usually a role that belongs on the instance instead.
  • Let the reasoner do the tangling by defining a class as an intersection and classifying, rather than asserting a second parent by hand.
  • Measure tangledness rather than guessing at it, since TMOnto is one number and an easy one to read.

Measured

Parses20 triples; 5 classes; 1 object property; 5 annotation properties; 2 individuals
Profile (ROBOT)OWL 2 DL: in profile
OQuaRE (scripts/metrics.py)5 classes, 1 object property, 0 datatype properties, 3 roots, 2 leaves
MetricValueScore 1 to 5
LCOMOnto25
WMCOnto05
DITOnto25
NACOnto1.55
NOCOnto1.55
CBOOnto1.55
RFCOnto1.55
NOMOnto05
RROnto0%1
AROnto0%1
INROnto0%1
CROnto40%2
ANOnto40%2
TMOnto1.55
Query: Classes with more than one asserted parent1 row
  • bs:PublisherBookshop "2"

Afterwards, in the SPARQL panel

Classes with more than one asserted parent (on the model)
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?class (COUNT(?parent) AS ?parents) WHERE { ?class rdfs:subClassOf ?parent . FILTER(isIRI(?parent)) }
GROUP BY ?class HAVING (COUNT(?parent) > 1)

Sources

O33

Built with the people who will read it

Every definition records who agreed it and when, the domain experts review the model in a form they can read, and a disagreement about territory is modelled rather than adjudicated by the modeller.

Corner: thought · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .

dct:contributor  a owl:AnnotationProperty .
dct:created      a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                owl:Ontology ;
    skos:prefLabel   "The Bookshop Trail schema"@en ;
    dct:contributor  bt:committee-2025-03-12, bt:person-events-secretary ;
    skos:editorialNote "Responsible for communication about this vocabulary: the events secretary (FP-011). Reviewed by the committee in the editor's graph view before each release."@en .

bt:committee-2025-03-12   skos:prefLabel "Trail committee, 12 March 2025"@en .
bt:person-events-secretary skos:prefLabel "The events secretary"@en .

# A term with its decision trail.
bs:Bookshop
    a                   owl:Class ;
    skos:prefLabel      "Bookshop"@en ;
    skos:definition     "A business that sells books to the public from premises."@en ;
    skos:editorialNote  "Agreed by the committee, 12 March 2025. The Hay shops asked whether a market stall counts; decided no, and 'from premises' was added."@en ;
    dct:contributor     bt:committee-2025-03-12 .

# A disagreement modelled rather than settled: two meanings of 'in stock'.
bs:stocks
    a                   owl:ObjectProperty ;
    skos:prefLabel      "stocks"@en ;
    skos:definition     "The shop has at least one copy of the work on the shelf now."@en ;
    skos:editorialNote  "The website team and the buyers disagreed: on the shelf now, or on the list to order. Both terms kept; the website uses this one. Decided by the chair, 9 April 2025."@en .
bs:lists
    a                   owl:ObjectProperty ;
    skos:prefLabel      "lists"@en ;
    skos:definition     "The shop lists the work for sale and can order it."@en .

Why

The process of bringing an ontology into an organisation is a social one. People have to get together and work out what kinds of things they have, what words get used for them across the business, and how each one can be defined without going round in a circle; and that process builds a team, though it sometimes exposes a fault line that was there already. The output is a vocabulary in common, and the process is at least half the value of the whole undertaking.

The SemOps Manual names the two ways it goes wrong. The first is that definitions are territory: when two departments disagree about a term, each definition is load-bearing for somebody, and a modeller who picks one without saying so has made a decision they had no authority to make, so the thing to do is model the disagreement, get the adjudication from somebody who can make it stick, and record who decided. The second is the priesthood problem, where only three people can read the model, the domain experts stop correcting it, and what gets recorded is the modeller's understanding rather than theirs. The remedy is not to teach everybody OWL but to put the model in front of them in a form they can check, which is what the editor's graph view is for and why every lesson in this course opens in it.

OBO Foundry principles FP-010 and FP-011 say much the same from the other side, that development is collaborative and that a named person is responsible for communication, and this file puts both of them on the ontology node and the decision trail on the terms.

Diagram

   the workshop                                   the record

   who has what kinds of thing ---.               skos:editorialNote "Agreed by the committee, 12 March 2025 ..."
   what words are used for them ---+--> terms --> dct:contributor  bt:committee-2025-03-12
   how each is defined, non-circularly            a named person responsible (FP-011)

   the graph view: the form a domain expert can check without reading OWL
   territory: model both meanings; someone with authority chooses; write down who

What to take away

  • Building the vocabulary is a social process, and the team it builds is part of the return on it.
  • Model a disagreement about territory rather than settling it, get a decision from somebody who can make it stick, and record who that was.
  • If the experts cannot read the model then they cannot correct it, so show them the graph rather than the axioms.

Measured

Parses26 triples; 1 class; 2 object properties; 7 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
Query: Every decision on record, with who made it2 rows
  • bs:Bookshop "Agreed by the committee, 12 March 2025. The Hay shops asked whether a market stall counts; decided no, and 'from premises' was added."
  • bs:stocks "The website team and the buyers disagreed: on the shelf now, or on the list to order. Both terms kept; the website uses this one. Decided by the chair, 9 April 2025."

Afterwards, in the SPARQL panel

Every decision on record, with who made it (on the model)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?term ?note WHERE { ?term skos:editorialNote ?note . FILTER(CONTAINS(?note, "ecided") || CONTAINS(?note, "Agreed")) }

Sources

O34

Ontologies like code: the loop and the gate

The vocabulary lives in version control, is checked in the editor as it is written and at the gate before it merges, and the checks are asserted by identifier and scoped to the Trail's own namespace.

Corner: thought · Runs in the browser; the gate itself is the Ontology Quality Suite from the command line: pip install ontology-quality-suite

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .

# Borrowed annotation properties, declared so the file is OWL 2 DL on its own.
skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
dct:source          a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a               owl:Ontology ;
    skos:prefLabel  "The Bookshop Trail schema"@en ;
    skos:editorialNote "Checked in the editor as written; gated in CI with the Ontology Quality Suite, scoped to the bs: namespace, failing on Violation."@en .

# A seeded fault for the gate to find: a class disjoint with its own ancestor.
bs:Work         a owl:Class ; skos:prefLabel "Work"@en ; skos:definition "A book."@en .
bs:Translation  a owl:Class ; rdfs:subClassOf bs:Work ; owl:disjointWith bs:Work ;
    skos:prefLabel "Translation"@en ; skos:definition "A work that is a translation of another."@en ;
    skos:editorialNote "Seeded: disjoint with its own parent. LOG-001 must fire, and REA-021 when the reasoner runs."@en .

Why

The SemOps Manual's maturity model has five levels and says that most of the return lies in the step from two to three, which is the step from treating ontologies like code to having the standards enforced without anybody having to remember to enforce them. Level two is really a decision: put the files in git, check them in the editor as you type, and run the checks locally. Level three is a gate, which is the same checks in CI with a non-zero exit code when something is wrong.

Two lessons from the manual's chapter 9 decide whether that gate survives its first month. The first is to scope the findings to your own namespace, because the Trail borrows SKOS, GeoSPARQL and PROV terms, and a gate that reports the W3C's documentation habits alongside the Trail's will have been switched off by the second week; the Quality Suite's --own-namespace flag keeps the imports resolved while filtering the report down to the terms you control. The second is to assert on check identifiers rather than counts, since 'LOG-001 must fire on the fixture with the seeded contradiction' survives a registry upgrade and 'expected 23 findings' does not.

This course is built that way itself. Every lesson is generated from a catalogue, every measured line comes from a run, the checker fails on any expectation that does not hold, and the seeded faults exist so that the checks can be seen to fire; which is the practice the manual describes, applied to a course about ontologies rather than to an ontology.

Diagram

   level 2: ontologies like code            level 3: the gate

   files in git                             the same checks in CI
   check in the editor, in seconds          --fail-on Violation, non-zero exit
   competency questions as .cq.rq tests     --own-namespace: findings about your terms only
   review in the graph view                 assert by check id, never by count

   this lesson's file, through the suite with --own-namespace: the ids below, and only those

What to take away

  • Put the vocabulary in version control and check it as you type, which is level two, and which is a decision rather than a project.
  • The gate reports on your namespace only, and the tests assert check identifiers; both of those are what keep a gate switched on.
  • A check that has never fired is indistinguishable from a clean model, so keep a fixture that makes each one of them fire.

Measured

Parses17 triples; 2 classes; 5 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; unsatisfiable: Translation
Ontology Quality Suite
  • LOG-001 ×1: Class https://example.org/bookshop-trail/schema#Translation is asserted disjoint with its own (transitive) superclass https://example.org/bookshop-tra
  • REA-020 ×1: The ontology (plus data, if included) is logically inconsistent according to the hermit reasoner: no model satisfies every axiom simultaneously.
  • REA-021 ×1: https://example.org/bookshop-trail/schema#Translation is unsatisfiable (equivalent to owl:Nothing) according to the hermit reasoner.

Sources

Module 06

The ladder: RDFS and SKOS

RDFS, SKOS and OWL 2 are one idea at rising degrees of commitment. A list of labelled terms says almost nothing; a concept scheme says how the terms relate without saying what they are; and a class hierarchy says that membership is inherited, at which point a reasoner begins to draw conclusions. This module climbs the first of those rungs with the genre scheme and the place hierarchy, saying at each one what has just been promised.

Sources
O35

A class is a set, in every model

Classes are read as sets and the vocabulary's structural words as set operations: rdf:type is membership, rdfs:subClassOf is subset, equivalence is equality, disjointness is an empty intersection.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

# owl:Thing is the whole domain; every class is a subset of it, and saying
# so is harmless and occasionally clarifying.
bs:Place       a owl:Class ; rdfs:subClassOf owl:Thing ; skos:prefLabel "Place"@en .
bs:Settlement  a owl:Class ; rdfs:subClassOf bs:Place ; skos:prefLabel "Settlement"@en .
bs:Bookshop    a owl:Class ; owl:disjointWith bs:Place ; skos:prefLabel "Bookshop"@en .

bt:place-wigtown  a bs:Settlement ; skos:prefLabel "Wigtown"@en .
bt:shop-inkwell   a bs:Bookshop ;   skos:prefLabel "The Inkwell"@en .

# A concept is a particular. No set is involved.
skos:Concept  a owl:Class .
skos:broader  a owl:ObjectProperty .
bt:genre-crime-fiction  a skos:Concept ; skos:broader bt:genre-fiction .
bt:genre-fiction        a skos:Concept .

Why

OWL 2's Direct Semantics says what an interpretation is: a domain of things, and for every class a subset of that domain, which is called its extension, with an individual in the class when it is in the subset. rdfs:subClassOf then means that the one subset is contained in the other, owl:equivalentClass that they are the same subset, and owl:disjointWith that they share nothing at all; a restriction is a set defined by a condition on its members, such as the things that have some bs:locatedIn value in Settlement. Every construct from here to the top of the ladder is a way of saying something about sets, and reading them that way is what makes RDFS, SKOS and OWL one idea rather than three.

Two cautions keep the picture straight. The first is the open world, since the extension is not the thirty-three shops in the file but a set that the axioms constrain and the data merely samples, so that a shop the file never mentions is in it just the same, and 'every Bookshop is located in some Settlement' is true of the set even where the file records no town. The second is that a class is not its extension: two classes that happen to have the same members in this data are not thereby the same class, because in another interpretation they need not be, and OWL 2's punning lets the same IRI be a class and an individual, read separately, which is how o14 typed bs:Author as a gufo:Role.

SKOS does not use sets at all. A skos:Concept is a particular and skos:broader is a relation between two particulars that promises nothing whatever about members, which is no weakness, and is the reason the concept scheme sits on the rung below the class hierarchy and why o40 has to do some work to climb from the one to the other.

Diagram

   in every interpretation                      set reading

   bs:Bookshop                                   a subset B of the domain
   bt:shop-inkwell a bs:Bookshop                 inkwell is in B
   bs:Settlement rdfs:subClassOf bs:Place        S is a subset of P
   bs:Work owl:equivalentClass bs:Book           W = K
   bs:Place owl:disjointWith bs:Bookshop         P and B share no member
   owl:Thing, owl:Nothing                        the whole domain; the empty set
   [ owl:onProperty bs:locatedIn ;               { x : some y with locatedIn(x, y), y in S }
     owl:someValuesFrom bs:Settlement ]

   cautions: the data samples the set; it does not enumerate it
             a class is not its extension; two with the same members here may differ elsewhere

What to take away

  • Read every class as a set and every structural axiom as a claim about sets, since that is the reading the specification gives and it is what makes the ladder one ladder.
  • Under the open world the set is constrained by the axioms and sampled by the data, so absence from the file is not absence from the set.
  • A concept is a particular and broader is a relation between particulars, with no sets involved anywhere, which is why it is a lower rung.

Try it

Press Show Facts, and among the implicit triples you will find bt:place-wigtown rdf:type bs:Place, which is the set reading computed for you. Nothing is said about the concepts, because broader promises nothing about members.

Measured

Parses23 triples; 4 classes; 1 object property; 5 annotation properties; 2 concepts; 4 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 1 inferred
  • bt:place-wigtown rdf:type bs:Place
OWL 2 RL closure (owlrl; what Show Facts computes)+1 triples about the Trail's terms
  • bt:place-wigtown rdf:type bs:Place
Query: Membership by subset: what is a Place, after the closure1 row
  • bt:place-wigtown

Afterwards, in the SPARQL panel

Membership by subset: what is a Place, after the closure (on the reasoned)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?place WHERE { ?place a bs:Place }

Sources

O36

Rung one, a list; rung two, a concept scheme

The genres are a SKOS concept scheme: particulars with labels, arranged by broader and narrower, promising nothing about the works in them; and the scheme's uneven depth is kept because it is true.

Corner: symbol and thought · Data: 02-genres.ttl · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

skos:Concept        a owl:Class .
skos:ConceptScheme  a owl:Class .
skos:inScheme       a owl:ObjectProperty .
skos:hasTopConcept  a owl:ObjectProperty .
skos:broader        a owl:ObjectProperty .

bt:genre-scheme  a skos:ConceptScheme ; skos:prefLabel "Bookshop Trail genre scheme"@en ; skos:hasTopConcept bt:genre-literature .

bt:genre-literature     a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Literature"@en .
bt:genre-fiction        a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Fiction"@en, "Ffuglen"@cy ; skos:broader bt:genre-literature .
bt:genre-poetry         a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Poetry"@en, "Barddoniaeth"@cy ; skos:altLabel "Verse"@en ; skos:broader bt:genre-literature .
bt:genre-crime-fiction  a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Crime Fiction"@en ; skos:altLabel "Mystery"@en ; skos:broader bt:genre-fiction .
bt:genre-cosy-crime     a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Cosy Crime"@en ; skos:altLabel "Cozy Mystery"@en ; skos:broader bt:genre-crime-fiction .
bt:genre-tartan-noir    a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Tartan Noir"@en ; skos:broader bt:genre-crime-fiction .

Why

The lowest rung is a list of terms with IRIs and labels, which is where module 01 started, and it promises only that the terms exist and are distinct from one another. The next rung up is a concept scheme, where each genre is a skos:Concept in a skos:ConceptScheme, each has one preferred label per language and as many alternatives as you like, and broader and narrower arrange them into a tree, or a lattice, since SKOS is content to let a concept have two broader ones. What has been promised is that these particulars are related in this way, and nothing has been said about any work at all; a reasoner given the scheme will not conclude that a cosy crime novel is a work of fiction, because the scheme has no members to conclude it about.

This is why SKOS is the right rung for a shelf plan. Curators move genres about and broader changes, and no data needs retyping on that account. The Trail's scheme has thirty-one concepts, two to four levels deep, and the depth is uneven because crime fiction has kinds that poetry does not; a model that forced every branch to the same depth would be tidier and false.

That unevenness carries the SPARQL course's best-known lesson, made on the place hierarchy in q28 and on this tree in q33: a fixed chain of two broader hops from Fiction finds some of the genres and silently misses the rest, while skos:broader+ finds them all. skos:broader is deliberately not transitive, skos:broaderTransitive is, and a path operator computes the closure without a reasoner anywhere in sight, which module 08 comes back to as the first of three routes to the same inference.

Diagram

   rung 1  a list         bt:genre-cosy-crime  skos:prefLabel "Cosy Crime"@en          exists, is distinct
   rung 2  a scheme       bt:genre-cosy-crime  skos:broader  bt:genre-crime-fiction   related, as particulars
                          bt:genre-crime-fiction skos:broader bt:genre-fiction
                          bt:genre-poetry      skos:broader  bt:genre-literature       (two levels, not four)

   promised: these particulars relate thus.  not promised: anything about a work.

   q33:  skos:broader/skos:broader  from Fiction   misses the branches of other depths
         skos:broader+              from Fiction   finds every descendant

What to take away

  • A concept scheme relates particulars and promises nothing about members, which is exactly why it can be rearranged without retyping any data.
  • Keep a hierarchy as uneven as the world is, because a fixed depth is a lie that some query will eventually trip over.
  • skos:broader is not transitive on purpose, and a path operator, or broaderTransitive, gives you the closure on the day a question needs it.

Note

The counts in the AFTERWARDS queries are for the full scheme, data/02-genres.ttl; open it in the editor to run them. The two COUNT queries return one row each, and the numbers inside differ: that difference is q33, and q28 is the same lesson on places.

Measured

Parses41 triples; 2 classes; 3 object properties; 5 annotation properties; 6 concepts; 6 individuals (with the data: 225 triples)
Profile (ROBOT)OWL 2 DL: in profile
Query: Two hops down from Fiction, in the full scheme1 row
  • "5"
Query: Every descendant of Fiction, however deep1 row
  • "17"
Query: Depth of every leaf, to see the unevenness22 rows
  • bt:genre-hard-sf "4"
  • bt:genre-space-opera "4"
  • bt:genre-climate-fiction "4"
  • bt:genre-folk-fantasy "4"
  • bt:genre-portal-fantasy "4"
  • bt:genre-tartan-noir "3"
  • bt:genre-cosy-crime "3"
  • bt:genre-police-procedural "3"

Afterwards, in the SPARQL panel

Two hops down from Fiction, in the full scheme (on the data)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT (COUNT(DISTINCT ?g) AS ?found) WHERE { ?g skos:broader/skos:broader bt:genre-fiction }
Every descendant of Fiction, however deep (on the data)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT (COUNT(DISTINCT ?g) AS ?found) WHERE { ?g skos:broader+ bt:genre-fiction }
Depth of every leaf, to see the unevenness (on the data)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?leaf (COUNT(?up) AS ?depth) WHERE {
  ?leaf a skos:Concept ; skos:broader+ ?up .
  FILTER NOT EXISTS { ?below skos:broader ?leaf }
} GROUP BY ?leaf ORDER BY DESC(?depth)

Sources

O37

Rung three, a class hierarchy: membership is inherited

Places are a class hierarchy, not a concept scheme, because a query about Places must find every Settlement without being told; rdfs:subClassOf commits to that and a reasoner delivers it.

Corner: referent · Data: 03-places.ttl · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix geo:  <http://www.opengis.net/ont/geosparql#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

geo:Feature     a owl:Class ; skos:prefLabel "Feature"@en .
bs:Place        a owl:Class ; rdfs:subClassOf geo:Feature ; skos:prefLabel "Place"@en .
bs:Settlement   a owl:Class ; rdfs:subClassOf bs:Place ; skos:prefLabel "Settlement"@en .
bs:CouncilArea  a owl:Class ; rdfs:subClassOf bs:Place ; skos:prefLabel "Council Area"@en .
bs:Region       a owl:Class ; rdfs:subClassOf bs:Place ; skos:prefLabel "Region"@en .
bs:Country      a owl:Class ; rdfs:subClassOf bs:Place ; skos:prefLabel "Country"@en .

bt:place-wigtown            a bs:Settlement ;  skos:prefLabel "Wigtown"@en .
bt:place-dumfries-galloway  a bs:CouncilArea ; skos:prefLabel "Dumfries and Galloway"@en .
bt:place-scotland           a bs:Country ;     skos:prefLabel "Scotland"@en .

Why

rdfs:subClassOf is the first promise about members. Settlement under Place means that every settlement is a place, and a reasoner, any reasoner at all, will type Wigtown as a Place without being told to; RDF Semantics writes the rule down, that if x is in A and A is a subclass of B then x is in B. The editor's Show Facts does it in the browser, and the OWL 2 RL closure this lesson was measured with does the same over the place file, adding the typing for every settlement, council area, region and country in it.

The Trail chose a class hierarchy for places and a concept scheme for genres, and the reason lies in what each of them is asked. A question about places, such as which shops are in Wales, needs every kind of place to be a place and never needs to say which kind on the way down; a question about genres, such as which shops specialise in crime fiction including its kinds, needs the tree walked explicitly and is answered by a path over particulars. So: a class hierarchy where membership must be inherited, and a concept scheme where the tree must be walked.

The subclass rule is transitive as well, so that Settlement under Place under Feature makes every town a feature, and it is depth that makes deep hierarchies risky and shallow ones cheap, which is why the backbone from o12 is only two levels deep.

Diagram

   rung 3  a class hierarchy

   bs:Settlement rdfs:subClassOf bs:Place       every settlement is a place: a promise about members
   bt:place-wigtown a bs:Settlement             ... so the reasoner adds:
                                                bt:place-wigtown a bs:Place

   the rule (RDF Semantics, rdfs9):  x type A . A subClassOf B  =>  x type B

   places: a class hierarchy, membership inherited, 'which shops are in Wales' just works
   genres: a concept scheme, tree walked by a path, 'including its kinds' is explicit

What to take away

  • rdfs:subClassOf is the first promise about members, and the reasoner keeps it, typing every instance of the subclass with the superclass.
  • Use a class hierarchy where membership has to be inherited silently, and a concept scheme where the tree has to be walked explicitly.
  • Subclass is transitive, so depth multiplies inferences, which is a good reason to keep the backbone shallow.

Try it

Load data/03-places.ttl, hide the types and annotations and choose the dot engine, and you are looking at the containment hierarchy. Then press Show Facts and count how many things are now typed bs:Place that the file never said were.

Measured

Parses28 triples; 6 classes; 5 annotation properties; 3 individuals
Profile (ROBOT)OWL 2 DL: in profile
OWL 2 RL closure (owlrl; what Show Facts computes)+10 triples about the Trail's terms
  • bt:place-wigtown rdf:type geo:Feature
  • bs:Country rdfs:subClassOf geo:Feature
  • bt:place-scotland rdf:type geo:Feature
  • bt:place-wigtown rdf:type bs:Place
  • bt:place-dumfries-galloway rdf:type geo:Feature
  • bt:place-scotland rdf:type bs:Place
  • bt:place-dumfries-galloway rdf:type bs:Place
  • bs:Settlement rdfs:subClassOf geo:Feature
  • bs:CouncilArea rdfs:subClassOf geo:Feature
  • bs:Region rdfs:subClassOf geo:Feature
Query: What is a Place, after the closure1 row
  • "3"

Afterwards, in the SPARQL panel

What is a Place, after the closure (on the reasoned)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT (COUNT(DISTINCT ?p) AS ?places) WHERE { ?p a bs:Place }

Sources

O38

Rung four, domain and range: inference rules, not constraints

A domain is declared only when everything that will ever have the property is of that class, because the reasoner will type whatever has it; the Trail's bs:locatedIn gets a union domain for that reason.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Bookshop    a owl:Class ; skos:prefLabel "Bookshop"@en .
bs:Publisher   a owl:Class ; skos:prefLabel "Publisher"@en .
bs:Settlement  a owl:Class ; skos:prefLabel "Settlement"@en .

# The narrow domain, declared to show what it does.
bs:locatedIn
    a            owl:ObjectProperty ;
    rdfs:domain  bs:Bookshop ;
    rdfs:range   bs:Settlement ;
    skos:editorialNote "Domain too narrow, on purpose: the reasoner will type every publisher with an address as a bookshop."@en .

bt:shop-inkwell   a bs:Bookshop ;  bs:locatedIn bt:place-wigtown .
bt:pub-northwind  a bs:Publisher ; bs:locatedIn bt:place-edinburgh .
bt:place-wigtown    skos:prefLabel "Wigtown"@en .
bt:place-edinburgh  skos:prefLabel "Edinburgh"@en .

Why

Every newcomer reads rdfs:domain as a constraint, as though it said that only bookshops may have a location, whereas RDF Semantics reads it as a rule, saying that whatever has a location is a bookshop. The two readings agree on clean data and part company at the first publisher with an address: declare bs:locatedIn's domain as bs:Bookshop, load the people file, and the reasoner types Northwind Press as a bookshop without error or warning. This lesson's file does exactly that, and the closure measured on it contains the triple.

Range is the same rule running in the other direction, so that whatever is the value of bs:locatedIn is a Settlement. That one is usually what you wanted, and it still bites, because a range of xsd:integer on a property whose values are strings does not reject the strings but declares them to be integers.

The habit that follows is the one from o31: declare a domain only where every use will agree with it, and where two classes will share a property, declare the union, which is the next rung's vocabulary; or declare nothing at all, as gist mostly does, and let the shapes say what the data must look like. The retyping is not a bug in RDFS but RDFS doing exactly what it says, and the SHACL course's s59, the inference that hides an error, is the same fact seen from the validator's side of the fence.

Diagram

   rdfs:domain, read as a constraint (wrong)       rdfs:domain, read as a rule (right)

   only Bookshops may be locatedIn                 whatever is locatedIn is a Bookshop
   Northwind Press locatedIn Edinburgh => error    Northwind Press locatedIn Edinburgh
                                                     => Northwind Press a Bookshop   (no error, no warning)

   the rule (rdfs2):  p domain C . x p y  =>  x type C

   the fix: a union domain, or none, and a shape for the constraint

What to take away

  • rdfs:domain and rdfs:range add types, and they never reject anything at all.
  • Declare a domain only where every use of the property will agree with it, since two classes sharing a property want a union, or nothing.
  • The constraint you had in mind is a shape, and the SHACL course's s59 shows the other half of this lesson.

Try it

Press Show Facts and Northwind Press is now a bs:Bookshop. Change the domain to a union of Bookshop and Publisher, press Get All and Show Facts again: the union is a class with no name, so Northwind is typed with that instead, which is true and useless. Then remove the domain altogether.

Measured

Parses21 triples; 3 classes; 1 object property; 5 annotation properties; 2 individuals
Profile (ROBOT)OWL 2 DL: in profile
OWL 2 RL closure (owlrl; what Show Facts computes)+3 triples about the Trail's terms
  • bt:pub-northwind rdf:type bs:Bookshop
  • bt:place-edinburgh rdf:type bs:Settlement
  • bt:place-wigtown rdf:type bs:Settlement
Query: Every bookshop, after the closure2 rows
  • bt:shop-inkwell
  • bt:pub-northwind

Afterwards, in the SPARQL panel

Every bookshop, after the closure (on the reasoned)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?shop WHERE { ?shop a bs:Bookshop }

Sources

O39

SKOS in full: labels, notes, related, mapping, and the integrity conditions

The genre scheme carries one preferred label per language, alternatives, scope notes, associative links kept apart from hierarchy, and mappings to outside schemes by match properties rather than owl:sameAs; and its integrity is checked by shapes.

Corner: all three · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix wd:   <http://www.wikidata.org/entity/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

skos:Concept        a owl:Class .
skos:ConceptScheme  a owl:Class .
skos:Collection     a owl:Class .
skos:inScheme       a owl:ObjectProperty .
skos:broader        a owl:ObjectProperty .
skos:related        a owl:ObjectProperty, owl:SymmetricProperty .
skos:member         a owl:ObjectProperty .
skos:closeMatch     a owl:ObjectProperty .
skos:hiddenLabel    a owl:AnnotationProperty .

bt:genre-scheme         a skos:ConceptScheme ; skos:prefLabel "Bookshop Trail genre scheme"@en .
bt:genre-fiction        a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Fiction"@en .
bt:genre-crime-fiction  a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Crime Fiction"@en ;
    skos:altLabel "Mystery"@en ; skos:hiddenLabel "Crime fition"@en ;
    skos:scopeNote "Works whose plot turns on a crime and its investigation."@en ;
    skos:broader bt:genre-fiction ;
    skos:closeMatch wd:Q5937792 .
bt:genre-tartan-noir    a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Tartan Noir"@en ;
    skos:broader bt:genre-crime-fiction ; skos:related bt:genre-scottish-interest .
bt:genre-scottish-interest a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Scottish Interest"@en ;
    skos:broader bt:genre-literature .
bt:genre-literature     a skos:Concept ; skos:inScheme bt:genre-scheme ; skos:prefLabel "Literature"@en .

bt:shelf-welsh  a skos:Collection ; skos:prefLabel "The Welsh-language shelf"@en ;
    skos:member bt:genre-fiction, bt:genre-crime-fiction .

# Two seeded faults for the shapes: a doubled English label, and a cycle.
bt:genre-cosy-crime  a skos:Concept ; skos:inScheme bt:genre-scheme ;
    skos:prefLabel "Cosy Crime"@en, "Cozy Crime"@en ;
    skos:broader bt:genre-crime-fiction .
bt:genre-fiction     skos:broader bt:genre-cosy-crime .

Why

SKOS is small and complete, and it divides into six kinds of statement. There are labels, prefLabel and altLabel and hiddenLabel for the misspellings, with the condition S14 that a concept has at most one preferred label per language. There are notes, definition and scopeNote and example and historyNote and changeNote and editorialNote, which is the same set gist recommends for ontologies, and that is no coincidence. There is hierarchy, broader and narrower, with transitive versions for queries. There is association, skos:related, for Tartan Noir and Scottish Interest, which are neither above nor below one another, subject to the condition S27 that a concept is not both related and broader to the same thing. There is grouping, skos:Collection, for something like the Welsh-language shelf, a set of concepts that is not itself a concept. And there is mapping, exactMatch and closeMatch and broadMatch, to concepts in other schemes.

The mapping properties are the ones worth getting right. owl:sameAs says that two IRIs have one referent, so that everything true of the one becomes true of the other, in both directions and for ever, whereas skos:exactMatch says only that two concepts in two schemes can be used interchangeably for retrieval, which is a smaller and safer claim about two particulars that remain two. The Trail's crime fiction is a closeMatch to Wikidata's crime fiction, which covers film and theatre as well as books, so that the scopes overlap and differ, which is what closeMatch is for.

The integrity conditions are checkable, and this lesson's shapes check the ones that matter: one preferred label per language, every concept in a scheme, related disjoint from broader, and no concept its own ancestor. Run against the shipped scheme they report conformance, and run against this file, with its seeded cycle and its doubled label, they report four violations: the doubled label once, and the cycle once for each of the three concepts caught in it. Four of the five conditions are SHACL Core, and two of those use sh:disjoint with a predicate for its value, which is the form the specification allows: related apart from broader, and broader apart from narrower. The fifth cannot be written that way, since sh:disjoint takes a predicate and not a path and there is no way in Core to say that a concept is absent from its own broader closure; that one is a sh:sparql constraint, and the SHACL course's s30 does it in the very same way.

Diagram

   labels     prefLabel (one per language, S14)   altLabel   hiddenLabel
   notes      definition  scopeNote  example  historyNote  changeNote  editorialNote
   hierarchy  broader  narrower       (broaderTransitive for the closure)
   association related                 (disjoint from broaderTransitive, S27)
   grouping   Collection, member       (not a concept)
   mapping    exactMatch  closeMatch  broadMatch  narrowMatch  relatedMatch

   owl:sameAs      one referent, two names, everything transfers
   skos:closeMatch two concepts, interchangeable for retrieval, nothing else transfers

What to take away

  • SKOS has six kinds of statement and a property for each: labels, notes, hierarchy, association, grouping and mapping.
  • Map between schemes with the match properties, since owl:sameAs merges the referents where exactMatch leaves them two.
  • The integrity conditions are shapes, so run them in the editor before publishing a scheme, and keep a file that breaks them.

Measured

Parses50 triples; 3 classes; 5 object properties; 6 annotation properties; 6 concepts; 7 individuals
Profile (ROBOT)OWL 2 DL: in profile
SHACL (5 shapes compiled)does not conform · 4 violations, 0 warnings, 0 info
SeverityFocus nodePathMessage
Violationbt:genre-crime-fictionA concept is not its own ancestor.
Violationbt:genre-fictionA concept is not its own ancestor.
Violationbt:genre-cosy-crimeskos:prefLabelS14: at most one preferred label per language.
Violationbt:genre-cosy-crimeA concept is not its own ancestor.
Query: Concepts that are their own ancestor3 rows
  • bt:genre-crime-fiction
  • bt:genre-cosy-crime
  • bt:genre-fiction

Afterwards, in the SPARQL panel

Concepts that are their own ancestor (on the model)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT DISTINCT ?c WHERE { ?c skos:broader+ ?c }

Sources

O40

Climbing a rung: when a concept becomes a class

Where an axiom about members is needed, the concept is punned into a class as well, with rdfs:subClassOf running beside skos:broader, and the reasoner then types works by their genre's ancestors.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

skos:Concept  a owl:Class .
skos:broader  a owl:ObjectProperty .
bs:Work       a owl:Class ; skos:prefLabel "Work"@en .

bt:genre-fiction        a skos:Concept, owl:Class ; skos:prefLabel "Fiction"@en ;
    rdfs:subClassOf bs:Work .
bt:genre-crime-fiction  a skos:Concept, owl:Class ; skos:prefLabel "Crime Fiction"@en ;
    skos:broader bt:genre-fiction ;        rdfs:subClassOf bt:genre-fiction .
bt:genre-cosy-crime     a skos:Concept, owl:Class ; skos:prefLabel "Cosy Crime"@en ;
    skos:broader bt:genre-crime-fiction ;  rdfs:subClassOf bt:genre-crime-fiction .
bt:genre-tartan-noir    a skos:Concept, owl:Class ; skos:prefLabel "Tartan Noir"@en ;
    skos:broader bt:genre-crime-fiction ;  rdfs:subClassOf bt:genre-crime-fiction .

bt:book-the-book-town   a bt:genre-cosy-crime ;  skos:prefLabel "The Book Town"@en .
bt:book-the-clyde-tapes a bt:genre-tartan-noir ; skos:prefLabel "The Clyde Tapes"@en .
bt:book-a-margin        a bt:genre-fiction ;     skos:prefLabel "A Margin of Error"@en .

Why

o24 gave the test, which is to promote a concept to a class when the first axiom about its members is needed. Suppose the website must show every work of fiction on one page and the works are tagged with the leaf genre only: a path over the scheme answers that in SPARQL and nothing more is called for. But suppose instead that a downstream consumer will only ever see the closure, or that a shape must target every work of fiction; then the concept scheme is a rung too low, and the promotion is mechanical. The same IRI becomes an owl:Class as well as a skos:Concept, rdfs:subClassOf is written beside every skos:broader, the works are typed with the class, and the reasoner does the rest. This lesson's file has three works typed with genres, two of them leaves, and the closure types each of them with every ancestor.

This is punning, and OWL 2 DL permits it, since the class reading and the individual reading of bt:genre-crime-fiction are kept apart by the reasoner, so that bs:specialises can still point at the individual while works are members of the class; the profile checker confirms that the file is DL. What it costs is that every genre now has two readings to maintain, and every change to the shelf plan becomes a change to a class hierarchy, with all the retyping that implies. That is the price of the higher rung, and it is why the Trail does not pay it for genres.

The set reading makes the promotion exact: the concept was a particular, the class is the set of works in that genre, broader between particulars becomes subset between sets, and a work typed with the leaf is, by inheritance, in every set above it.

Diagram

   before: a particular                        after: a particular and a set

   bt:genre-cosy-crime a skos:Concept          bt:genre-cosy-crime a skos:Concept, owl:Class ;
     skos:broader bt:genre-crime-fiction           skos:broader     bt:genre-crime-fiction ;
                                                   rdfs:subClassOf  bt:genre-crime-fiction .
   bt:book-x bs:genre bt:genre-cosy-crime      bt:book-x a bt:genre-cosy-crime .
                                                 => bt:book-x a bt:genre-crime-fiction, bt:genre-fiction   (the reasoner)

   one IRI, two readings, kept apart: punning. The shelf plan is now a class hierarchy.

What to take away

  • Promotion is mechanical: add owl:Class, mirror broader with subClassOf, type the members, and the reasoner types them with every ancestor.
  • Punning lets the concept keep its uses while the class gains members, and the profile checker confirms that this stays inside DL.
  • The cost is two readings per term and retyping on every rearrangement, so pay it only where an axiom about members is needed.

Measured

Parses34 triples; 6 classes; 1 object property; 5 annotation properties; 4 concepts; 3 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 7 inferred
  • bt:book-the-clyde-tapes rdf:type bt:genre-crime-fiction
  • bt:book-the-book-town rdf:type bs:Work
  • bt:book-the-clyde-tapes rdf:type bt:genre-fiction
  • bt:book-a-margin rdf:type bs:Work
  • bt:book-the-book-town rdf:type bt:genre-crime-fiction
  • bt:book-the-book-town rdf:type bt:genre-fiction
  • bt:book-the-clyde-tapes rdf:type bs:Work
OWL 2 RL closure (owlrl; what Show Facts computes)+12 triples about the Trail's terms
  • bt:genre-tartan-noir rdfs:subClassOf bs:Work
  • bt:book-the-book-town rdf:type bt:genre-fiction
  • bt:book-the-book-town rdf:type bs:Work
  • bt:book-the-clyde-tapes rdf:type bt:genre-crime-fiction
  • bt:book-the-book-town rdf:type bt:genre-crime-fiction
  • bt:genre-cosy-crime rdfs:subClassOf bt:genre-fiction
  • bt:genre-crime-fiction rdfs:subClassOf bs:Work
  • bt:genre-cosy-crime rdfs:subClassOf bs:Work
  • bt:book-the-clyde-tapes rdf:type bt:genre-fiction
  • bt:book-a-margin rdf:type bs:Work
and 2 more
Query: Every work of fiction, after the closure3 rows
  • bt:book-a-margin
  • bt:book-the-book-town
  • bt:book-the-clyde-tapes

Afterwards, in the SPARQL panel

Every work of fiction, after the closure (on the reasoned)
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?work WHERE { ?work a bt:genre-fiction }

Sources

O41

What RDFS cannot say

Disjointness, cardinality, inverses, defined classes and 'exactly one' are wanted and RDFS has none of them, so the list of what it cannot say becomes the agenda for the top of the ladder.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:altLabel       a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:scopeNote      a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Place       a rdfs:Class ; skos:prefLabel "Place"@en .
bs:Bookshop    a rdfs:Class ; skos:prefLabel "Bookshop"@en .
bs:Settlement  a rdfs:Class ; rdfs:subClassOf bs:Place .

# Both at once. RDFS has no way to object, and the closure types it with everything.
bt:oddity  a bs:Settlement, bs:Bookshop ; skos:prefLabel "A thing that is a town and a shop"@en .

Why

RDFS gives us classes and subclasses, properties and subproperties, domains and ranges, and labels, and with those it can say that every settlement is a place and that whatever is located somewhere is a shop. It cannot say that nothing is both a place and a shop; it cannot say that a shop is in one town, that a segment has two ends, that wrote and author are the same relation read in opposite directions, or that an author is a person who wrote something. Most of all it cannot be contradicted: assert that one thing is a Place and a Bookshop, run the RDFS closure, and it will type the thing with both and with their superclasses and report nothing at all, because in RDFS there is nothing to report.

None of which is a flaw. A language in which you cannot be wrong is a language in which data from anywhere merges without an argument, and that is what RDFS was for; the cost is simply that the errors it cannot see stay in the data until a query finds them, and the SPARQL course's module 13 is the collection of queries that do.

The list of what we wanted and could not say is the agenda for module 07, in order: disjointness first, because it is the cheapest and catches the most; then restrictions, which say what a member must have; then defined classes, which let the reasoner decide membership; then the property characteristics; and last the datatypes and profiles, which are where the language draws its own limits.

Diagram

   wanted                                          RDFS       OWL 2 (module 07)

   nothing is both a Place and a Bookshop          --         owl:disjointWith            o43
   every Bookshop is in some Settlement            --         owl:someValuesFrom          o44
   a Bookshop stocks only Works                    --         owl:allValuesFrom           o45
   an Author is a Person who wrote something       --         owl:equivalentClass         o46
   a segment has exactly two ends                  --         owl:cardinality             o47
   wrote is author, backwards                      --         owl:inverseOf               o49
   within is transitive                            --         owl:TransitiveProperty      o48

   a language in which you cannot be wrong merges anything; the errors wait for a query

What to take away

  • RDFS cannot be contradicted, so a thing typed as two disjoint kinds is typed with both and nothing is reported.
  • That is the design rather than an oversight, since data from anywhere merges; the cost is that the errors wait for a query to find them.
  • The list of what RDFS cannot say is the order to learn OWL in, and disjointness comes first.

Measured

Parses14 triples; 3 classes; 5 annotation properties; 1 individual
OWL 2 RL closure (owlrl; what Show Facts computes)+1 triples about the Trail's terms
  • bt:oddity rdf:type bs:Place
Query: What the oddity is, after the RDFS closure3 rows
  • bs:Bookshop
  • bs:Settlement
  • bs:Place

Afterwards, in the SPARQL panel

What the oddity is, after the RDFS closure (on the reasoned)
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?type WHERE { bt:oddity a ?type }

Sources

Module 07

The ladder: OWL 2

The top rungs, where OWL 2 lets an ontology say enough that a reasoner can contradict it, which is the first point at which the model can be wrong rather than incomplete. Each lesson takes one construct, states what it commits you to, and shows what the reasoner then does with it; and the surprises are the point, whether a maximum cardinality that merges two towns instead of flagging an error, a universal restriction that infers nothing at all, or a transitive property that cannot also be functional.

Sources
O42

owl:Class, owl:Ontology, and the rule that everything is declared

The vocabulary becomes an OWL 2 DL ontology: an owl:Ontology node, every class an owl:Class, every property exactly one of object, datatype or annotation, every borrowed term declared, and the profile checker as referee.

Corner: referent · Runs in the browser; the OWL 2 profile verdicts on the MEASURED line come from ROBOT

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a               owl:Ontology ;
    skos:prefLabel  "The Bookshop Trail schema"@en .

bs:Place       a owl:Class ; skos:prefLabel "Place"@en .
bs:Settlement  a owl:Class ; rdfs:subClassOf bs:Place ; skos:prefLabel "Settlement"@en .
bs:Bookshop    a owl:Class ; skos:prefLabel "Bookshop"@en .

bs:locatedIn   a owl:ObjectProperty ;   rdfs:range bs:Settlement ; skos:prefLabel "located in"@en .
bs:founded     a owl:DatatypeProperty ; rdfs:range xsd:integer ;   skos:prefLabel "founded"@en .
bs:website     a owl:DatatypeProperty ; rdfs:range xsd:anyURI ;    skos:prefLabel "website"@en .
skos:Concept   a owl:Class .

bt:shop-inkwell  a bs:Bookshop ; bs:locatedIn bt:place-wigtown ; bs:founded 1979 .
bt:place-wigtown a bs:Settlement .

Why

The step from RDFS to OWL 2 DL is mostly a matter of declarations. rdfs:Class becomes owl:Class, and rdf:Property becomes one of owl:ObjectProperty, owl:DatatypeProperty or owl:AnnotationProperty, and exactly one of them, because a DL reasoner needs to know whether a property's values are things, values or commentary before it can reason at all. Every term the file mentions is declared, including the borrowed ones, since the profile is checked over the imports closure and the Trail imports nothing; and an owl:Ontology node says what the document is.

The reward for that discipline is that a checker can now tell you whether the file is OWL 2 DL, and this one is. The SPARQL course's README records what it took to get the shipped vocabulary there, and the rules that bit were all of them rules RDFS never mentions: everything must be declared; a property is of one kind only; a transitive property is non-simple and may not be functional; and an RDF 1.2 triple term has no OWL type at all, so the property that points at one has to be an annotation property. Each of those is a lesson in this module.

The set reading from o35 now has teeth, since an owl:Class is a set the reasoner will draw conclusions about, where a class that was rdfs:Class in module 06 was a set nobody could contradict. The profiles table in the measured line shows where this small file sits, which is in DL, and in EL, QL and RL besides, because it says so little.

Diagram

   RDFS                                OWL 2 DL

   bs:Bookshop a rdfs:Class            bs:Bookshop a owl:Class
   bs:locatedIn a rdf:Property         bs:locatedIn a owl:ObjectProperty      (values are things)
   bs:founded a rdf:Property           bs:founded a owl:DatatypeProperty      (values are values)
   rdfs:comment (used, undeclared)     rdfs:comment is built in; skos:definition a owl:AnnotationProperty
   (no header)                         <.../schema> a owl:Ontology

   the referee:  robot validate-profile --profile DL      in profile / NOT in profile, with the reason

What to take away

  • OWL 2 DL is RDFS plus declarations, and it is the declarations that let a reasoner and a checker do their work.
  • A property is object, datatype or annotation, and only one of them; decide by asking whether its values are things, values or commentary.
  • Declare whatever you borrow, since the profile is checked over what is imported and here nothing is.

Try it

Delete the line declaring bs:locatedIn and run python scripts/check_lessons.py o42: the file falls out of DL, and the checker says why: 'use of undeclared annotation property bs:locatedIn'. With no declaration, the OWL API reads the assertion as an annotation, and DL requires annotation properties to be declared too. Deleting bs:website's line changes nothing, because nothing uses it. Put the line back. The editor will not object either way; only the referee does.

Measured

Parses26 triples; 4 classes; 1 object property; 2 datatype properties; 3 annotation properties; 2 individuals
Profile (ROBOT)OWL 2 DL: in profile
OWL 2 EL: in profile
OWL 2 QL: in profile
OWL 2 RL: in profile
HermiTconsistent; 1 inferred
  • bt:place-wigtown rdf:type bs:Place

Sources

O43

Disjointness: now you can be wrong

The top-level kinds are declared pairwise disjoint, so that one thing typed as two of them is a contradiction a reasoner reports, and a query finds in the browser.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Place a owl:Class . bs:Bookshop a owl:Class . bs:Person a owl:Class . bs:Work a owl:Class .
[] a owl:AllDisjointClasses ; owl:members ( bs:Place bs:Bookshop bs:Person bs:Work ) .

# For the query, which reads pairwise disjointness: the same fact stated
# pairwise for the two classes the oddity breaks.
bs:Place  owl:disjointWith  bs:Bookshop .

bt:oddity  a bs:Place, bs:Bookshop ; skos:prefLabel "A row loaded with the wrong kind"@en .

Why

Disjointness comes first among the OWL constructs because it is the first thing that can be contradicted. Place and Bookshop declared disjoint says that the two sets share no member, so that if you type one thing with both there is no interpretation in which the axioms hold, the ontology is inconsistent, and HermiT says so. This is the thing RDFS could not do in o41, and it is cheap, since one owl:AllDisjointClasses over the backbone catches the commonest data-loading error there is, which is a row typed as the wrong kind.

The editor's reasoner is an OWL 2 RL engine and materialises rather than judges, so it will not put up a sign saying inconsistent. The same contradiction is a query, though, and the query runs perfectly well in the browser: find anything typed with two classes that are declared disjoint. That query is the SHACL course's s25, sh:not, seen from the other side, and it is the one to keep in the gate.

Below the backbone, disjointness gets harder to justify, because Author and TrailMember are roles and a shop can be a member as well as a shop, just as a person can be an author and a translator at once. gist declares disjointness extensively at the highest level and sparingly below it, and that is the shape to copy.

Diagram

   bs:Place  owl:disjointWith  bs:Bookshop        the sets share no member

   bt:oddity a bs:Place, bs:Bookshop              no interpretation satisfies this
                                                  HermiT: the ontology is inconsistent

   in the browser, the same fact as a query:
   SELECT ?x WHERE { ?a owl:disjointWith ?b . ?x a ?a, ?b }     -> bt:oddity

What to take away

  • Disjointness is the cheapest axiom there is and the first that can be contradicted, so declare it across the backbone.
  • A DL reasoner reports the contradiction where an RL reasoner materialises straight past it, so keep the query that finds it.
  • Disjointness below the backbone is where the roles start to overlap, so declare it there only with a reason.

Measured

Parses21 triples; 4 classes; 3 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
HermiTinconsistent
OWL 2 RL closure (owlrl; what Show Facts computes)+0 triples about the Trail's terms
Query: Anything typed with two disjoint classes1 row
  • bt:oddity bs:Place bs:Bookshop

Afterwards, in the SPARQL panel

Anything typed with two disjoint classes (on the model)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?x ?a ?b WHERE { ?a owl:disjointWith ?b . ?x a ?a, ?b }

Sources

O44

Existential restrictions: a necessary condition, and the open world

'Every bookshop is located in some settlement' is written as a subclass axiom with owl:someValuesFrom, which constrains the set without producing a triple, and a shop with no recorded town is not an error.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Settlement  a owl:Class .
bs:Bookshop    a owl:Class ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty bs:locatedIn ; owl:someValuesFrom bs:Settlement ] .
bs:locatedIn   a owl:ObjectProperty .

bt:shop-inkwell   a bs:Bookshop ; bs:locatedIn bt:place-wigtown .   # the town's type is not inferred
bt:shop-errata    a bs:Bookshop .                                   # no town recorded: not an error
bt:place-wigtown  skos:prefLabel "Wigtown"@en .

Why

A restriction is a set defined by a condition, in this case the things that have some bs:locatedIn value in bs:Settlement, and writing bs:Bookshop rdfs:subClassOf that set says every bookshop is such a thing. It is a necessary condition, that to be a bookshop you must be located in a settlement, and it is not a sufficient one, since other things are located in settlements too; nor is it a defined class, which o46 is.

What does the reasoner do with it? Rather less than newcomers expect. Given a bookshop with no recorded town it does not complain, because the open world says the town exists and is simply not in the file; given a bookshop located in something it does not type that something as a Settlement, because the axiom says that some value is a settlement rather than that every value is. HermiT reports the file consistent and infers nothing visible, and that is correct: the axiom has narrowed the set of interpretations, which is what an axiom does, and nothing in the data happened to be decided by it.

Which is why gist's rules say to use someValuesFrom in an equivalence when you want to infer into a class, and why the shape that says every shop must have a town in this dataset is SHACL rather than this. The SHACL course's s04 is that shape, and it reports the shop with no town; this axiom never will.

Diagram

   bs:Bookshop rdfs:subClassOf [ owl:onProperty bs:locatedIn ; owl:someValuesFrom bs:Settlement ]

   set reading:  B is a subset of { x : some y, locatedIn(x, y), y in S }

   a shop with no town in the file      consistent: the town exists, unrecorded (open world)
   a shop located in something          the something is not typed: 'some', not 'every'
   HermiT: consistent, nothing new      correct, and the point

   want to find shops with no town?     a shape: sh:path bs:locatedIn ; sh:minCount 1   (SHACL course s04)

What to take away

  • A subclass existential is a necessary condition on the set, and it rejects nothing and types nothing, merely narrowing what the world could be.
  • Absence is not error, because the open world says the missing value exists and is unrecorded.
  • To find the missing value, write a shape; to infer membership from a value, use an equivalence, as o46 does.

Measured

Parses14 triples; 2 classes; 1 object property; 3 annotation properties; 2 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; nothing new
OWL 2 RL closure (owlrl; what Show Facts computes)+0 triples about the Trail's terms
Query: Shops with no recorded town (what a shape would report)1 row
  • bt:shop-errata

Afterwards, in the SPARQL panel

Shops with no recorded town (what a shape would report) (on the model)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?shop WHERE { ?shop a bs:Bookshop . FILTER NOT EXISTS { ?shop bs:locatedIn ?town } }

Sources

O45

Universal restrictions: types the values, and is true of nothing

'A bookshop stocks only works' is written with owl:allValuesFrom, which types every value of bs:stocks on a bookshop as a Work and is trivially satisfied by a shop that stocks nothing.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Work      a owl:Class .
bs:Bookshop  a owl:Class ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty bs:stocks ; owl:allValuesFrom bs:Work ] .
bs:stocks    a owl:ObjectProperty .

bt:shop-inkwell  a bs:Bookshop ; bs:stocks bt:thing .
bt:thing         skos:prefLabel "Something the shop stocks; the axiom will say what"@en .
bt:shop-errata   a bs:Bookshop .

Why

The universal restriction picks out the set of things all of whose bs:stocks values are Works, and that includes the things which have no values at all. Put bs:Bookshop under it and the reasoner types every stocked thing as a Work, which makes this a local range, and both HermiT and the RL closure produce the triple; a shop that stocks nothing satisfies it vacuously, which tends to surprise anyone who reads 'only' as 'at least one, and only'.

gist's fifth rule says what a subclass universal is good for, which is to add information about the class without inferring anything into it, and its first two rules say what an equivalence to a universal is good for, which is inferring into the complement: since nothing can be shown to have only Work values under the open world, no individual is ever classified into the class, while one with a non-Work value can be classified out of it. That is a subtle tool, and o47 uses its cousin.

The everyday use is the range you cannot declare globally. bs:stocks on a bookshop points at works, while the same property on a library, if the vocabulary had one, might point at editions; a global rdfs:range would type both of them, where the local universal types only the bookshop's values.

Diagram

   bs:Bookshop rdfs:subClassOf [ owl:onProperty bs:stocks ; owl:allValuesFrom bs:Work ]

   set reading:  B is a subset of { x : for every y, stocks(x, y) implies y in W }

   bt:shop-inkwell bs:stocks bt:thing     =>  bt:thing a bs:Work         (HermiT and the RL closure)
   bt:shop-errata stocks nothing          =>  satisfied, vacuously

   a local range: types the values of this property on this class only

What to take away

  • allValuesFrom types the values, which makes it a range scoped to one class.
  • 'Only' includes 'none', since a thing with no values at all satisfies every universal restriction going.
  • Under the open world nothing can be proved to have only such values, so an equivalence to a universal never infers into the class, though it can infer out of it.

Try it

Show Facts: bt:thing is a bs:Work. Add a second value that is declared a bs:Person, with Person disjoint from Work, and the RL closure will type it a Work anyway; HermiT will call the file inconsistent. Same axiom, two engines, o43's lesson again.

Measured

Parses14 triples; 2 classes; 1 object property; 3 annotation properties; 2 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 1 inferred
  • bt:thing rdf:type bs:Work
OWL 2 RL closure (owlrl; what Show Facts computes)+1 triples about the Trail's terms
  • bt:thing rdf:type bs:Work

Sources

O46

Defined classes: the reasoner decides membership

Author is defined rather than declared: equivalent to a Person who wrote some Work, so that the reasoner types every such person an Author without being told, and the role from module 02 has its formal definition.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Person  a owl:Class .
bs:Work    a owl:Class .
bs:wrote   a owl:ObjectProperty .

bs:Author
    a                    owl:Class ;
    skos:prefLabel       "Author"@en ;
    skos:definition      "A person who wrote at least one work."@en ;
    owl:equivalentClass  [ a owl:Class ;
                           owl:intersectionOf ( bs:Person
                                                [ a owl:Restriction ; owl:onProperty bs:wrote ; owl:someValuesFrom bs:Work ] ) ] .

bt:author-rhona-blackwood  a bs:Person ; bs:wrote bt:book-the-loom .
bt:book-the-loom           a bs:Work .
bt:person-reader           a bs:Person .    # wrote nothing recorded: not classified

# An enumeration: a closed set, by definition.
bs:Country  a owl:Class ;
    owl:equivalentClass [ a owl:Class ; owl:oneOf ( bt:place-scotland bt:place-england bt:place-wales ) ] .
bt:place-scotland a owl:NamedIndividual . bt:place-england a owl:NamedIndividual . bt:place-wales a owl:NamedIndividual .

# A complement: a Work that is not Fiction. Nothing is ever proved into it.
bs:Fiction     a owl:Class ; rdfs:subClassOf bs:Work .
bs:NonFiction  a owl:Class ;
    owl:equivalentClass [ a owl:Class ; owl:intersectionOf ( bs:Work [ a owl:Class ; owl:complementOf bs:Fiction ] ) ] .
# bt:book-the-loom is a Work; fiction or not, the file does not say, so NonFiction gets no member.

Why

A subclass axiom gives a necessary condition, whereas an equivalence gives necessary and sufficient ones, so that Author is exactly the set of Persons with some bs:wrote value in Work and membership is now decided by the reasoner. Rhona Blackwood is a Person and wrote a Work, so she is an Author, and HermiT and the RL closure both say so from the data alone; this is the construct gist's sixth rule points at, that to infer instances into a class you define it with someValuesFrom, hasValue or a minimum cardinality.

It is also the formalisation of o08, where Author was a role, anti-rigid and dependent on Work: the definition says the dependence exactly, and the anti-rigidity follows, since a person with no works is not classified and stops being classified when the works go. Rigidity is not in the axiom, but the axiom respects it.

The habit that follows is to define the roles and phases with equivalences and leave the kinds as primitive classes, because a Person cannot be defined by any condition a reasoner could check where an Author can. gist's Task is an Event with hasGoal some Intention, its HistoricalEvent is an Event with actualEndDateTime some dateTime, and its Person is primitive; the reasoner classifies the defined ones and leaves the primitive ones to the data, which is just the division of labour a backbone taxonomy wants.

Two further ways of defining a class are in the file for completeness. An enumeration, owl:oneOf, defines bs:Country as exactly Scotland, England and Wales, which is a closed set and the one place OWL lets you close the world, and it is right for a list that will not change. A complement, owl:complementOf, defines bs:NonFiction as a Work that is not Fiction, and under the open world no individual is ever proved into it, because nothing can be proved not to be fiction; gist's second rule says that a complement is for inferring out of a class rather than into one, and the measured line bears that out.

Diagram

   bs:Author owl:equivalentClass [ owl:intersectionOf ( bs:Person
                                    [ owl:onProperty bs:wrote ; owl:someValuesFrom bs:Work ] ) ]

   set reading:  A = P intersect { x : some y, wrote(x, y), y in W }

   bt:author-rhona-blackwood a bs:Person ; bs:wrote bt:book-x .    bt:book-x a bs:Work .
     =>  bt:author-rhona-blackwood a bs:Author            (HermiT; and the RL closure)

   define roles and phases; leave kinds primitive

What to take away

  • An equivalence to an intersection with someValuesFrom lets the reasoner decide membership, where a subclass axiom gives a necessary condition only.
  • Roles and phases are the things to define, since kinds are primitive and no condition a reasoner can check makes something a person.
  • The definition of Author states the dependence from module 02 exactly, and respects the anti-rigidity without having to state it.

Measured

Parses48 triples; 6 classes; 1 object property; 3 annotation properties; 3 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 6 inferred
  • bt:author-rhona-blackwood rdf:type bs:Author
  • bt:place-wales rdf:type bs:Country
  • bs:Author rdfs:subClassOf bs:Person
  • bt:place-scotland rdf:type bs:Country
  • bs:NonFiction rdfs:subClassOf bs:Work
  • bt:place-england rdf:type bs:Country
OWL 2 RL closure (owlrl; what Show Facts computes)+6 triples about the Trail's terms
  • bt:place-scotland rdf:type bs:Country
  • bs:Author rdfs:subClassOf bs:Person
  • bt:place-england rdf:type bs:Country
  • bt:author-rhona-blackwood rdf:type bs:Author
  • bs:NonFiction rdfs:subClassOf bs:Work
  • bt:place-wales rdf:type bs:Country
Query: Who is an Author, after the closure1 row
  • bt:author-rhona-blackwood

Afterwards, in the SPARQL panel

Who is an Author, after the closure (on the reasoned)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?who WHERE { ?who a bs:Author }

Sources

O47

Cardinality: the reasoner merges before it complains

Cardinality restrictions are written for meaning and never for validation, because two values under a maximum of one make the reasoner conclude they are the same thing, not that the data is wrong.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Settlement  a owl:Class .
bs:Bookshop    a owl:Class .
bs:locatedIn   a owl:ObjectProperty, owl:FunctionalProperty ; rdfs:range bs:Settlement .

# The same commitment as restrictions, in the three unqualified forms: a
# bookshop is located in at most one, at least one, and so exactly one.
bs:Bookshop
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty bs:locatedIn ; owl:maxCardinality 1 ] ,
                    [ a owl:Restriction ; owl:onProperty bs:locatedIn ; owl:minCardinality 1 ] ,
                    [ a owl:Restriction ; owl:onProperty bs:locatedIn ; owl:cardinality 1 ] .

bt:shop-x         a bs:Bookshop ; bs:locatedIn bt:place-wigtown, bt:place-hay .
bt:place-wigtown  a bs:Settlement ; skos:prefLabel "Wigtown"@en .
bt:place-hay      a bs:Settlement ; skos:prefLabel "Hay-on-Wye"@en .

# A segment is a pair: qualified cardinality carrying meaning.
bs:TrailSegment  a owl:Class ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty bs:end ; owl:qualifiedCardinality 2 ; owl:onClass bs:Bookshop ] .
bs:end  a owl:ObjectProperty .

Why

'A shop is in exactly one settlement.' Written as a cardinality restriction or as a functional property that means what it says, and what it says has a consequence nobody wants at first: a shop recorded in Wigtown and in Hay-on-Wye is no error at all, and the reasoner reads it as evidence that Wigtown and Hay-on-Wye are one place. The RL closure of this file contains owl:sameAs between the two towns and HermiT agrees that the file is consistent, because OWL makes no unique name assumption and two IRIs may name one thing until something says otherwise. Add owl:differentFrom between the towns and the very same file becomes inconsistent.

That is the whole argument behind gist's rules seven to eleven, and behind o29. Use someValuesFrom rather than a minimum of one; use minimum cardinalities above one where you mean them; never a minimum of zero, which says nothing at all; and express data constraints with SHACL, where more than one town is a row in a report and the two towns stay two.

Qualified cardinality is the useful cousin of all this, since a TrailSegment has exactly two bs:end values that are Bookshops, which carries meaning, a segment being a pair; and the reasoner uses it in just the same way, so that a third end merges with one of the other two.

Diagram

   bs:locatedIn a owl:FunctionalProperty          at most one value, in every interpretation

   bt:shop-x bs:locatedIn bt:place-wigtown, bt:place-hay
     =>  bt:place-wigtown owl:sameAs bt:place-hay      (the RL closure; HermiT: consistent)

   + bt:place-wigtown owl:differentFrom bt:place-hay
     =>  inconsistent

   the shape that was meant:  sh:path bs:locatedIn ; sh:maxCount 1   (SHACL course s04)

What to take away

  • There is no unique name assumption, so two IRIs may turn out to be one thing, and a maximum cardinality merges before ever it contradicts.
  • Cardinality is for meaning: that a segment has two ends is meaning, while a shop row having one town is a shape.
  • differentFrom, AllDifferent and inverse-functional keys are how you tell the reasoner that two things really are two.

Try it

Show Facts: the two towns are owl:sameAs. Add 'bt:place-wigtown owl:differentFrom bt:place-hay .', Get All, and run the checker: HermiT now reports the file inconsistent. The editor's closure will show both triples and say nothing.

Measured

Parses34 triples; 3 classes; 2 object properties; 3 annotation properties; 3 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; nothing new
OWL 2 RL closure (owlrl; what Show Facts computes)+4 triples about the Trail's terms
  • bt:place-wigtown skos:prefLabel "Hay-on-Wye"
  • bt:place-wigtown owl:sameAs bt:place-hay
  • bt:place-hay skos:prefLabel "Wigtown"
  • bt:place-hay owl:sameAs bt:place-wigtown
Query: Things the closure has decided are the same1 row
  • bt:place-hay bt:place-wigtown

Afterwards, in the SPARQL panel

Things the closure has decided are the same (on the reasoned)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?a ?b WHERE { ?a owl:sameAs ?b . FILTER(?a != ?b && STR(?a) < STR(?b)) }

Sources

O48

Property characteristics, and the property that cannot be two things

bs:within is transitive, bs:connectsTo symmetric, bs:imprintOf asymmetric and irreflexive; and because a transitive property is non-simple, bs:within may not also be functional, which the profile checker enforces.

Corner: referent · Runs in the browser; the profile refusal on the MEASURED line comes from ROBOT

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Place       a owl:Class .
bs:Bookshop    a owl:Class .
bs:Publisher   a owl:Class .

bs:connectsTo  a owl:ObjectProperty, owl:SymmetricProperty ;   rdfs:domain bs:Bookshop ;  rdfs:range bs:Bookshop .
bs:imprintOf   a owl:ObjectProperty, owl:AsymmetricProperty, owl:IrreflexiveProperty ; rdfs:domain bs:Publisher ; rdfs:range bs:Publisher .

# The combination OWL 2 DL forbids, declared to show the refusal.
bs:within      a owl:ObjectProperty, owl:TransitiveProperty, owl:FunctionalProperty ; rdfs:domain bs:Place ; rdfs:range bs:Place ;
    skos:editorialNote "Transitive and functional at once: outside OWL 2 DL. The shipped vocabulary drops the functional declaration."@en .

bt:place-wigtown            a bs:Place ; bs:within bt:place-dumfries-galloway .
bt:place-dumfries-galloway  a bs:Place ; bs:within bt:place-scotland .
bt:place-scotland           a bs:Place .
bt:shop-inkwell             a bs:Bookshop ; bs:connectsTo bt:shop-marginalia .
bt:shop-marginalia          a bs:Bookshop .

Why

A property characteristic is an axiom about the relation as a whole. Transitive says that if Wigtown is within Dumfries and Galloway and that is within Scotland then Wigtown is within Scotland, and the reasoner writes the triple; symmetric says that a segment from A to B is a segment from B to A, so that bs:connectsTo asserted once holds both ways; asymmetric and irreflexive say that nothing is an imprint of itself or of its own imprint, and asserting either is a contradiction; functional and inverse-functional say at most one value, or at most one subject, with the merging behaviour of o47. Reflexive is rare, and the Trail has none.

OWL 2 DL has a rule about combining them, and it is the rule that bit the Trail's vocabulary first: a transitive property is non- simple, and a non-simple property may not be functional, inverse- functional, irreflexive, asymmetric, disjoint with another, or used in a cardinality restriction. The reason is decidability, and the practical effect is that bs:within, being transitive, cannot also be declared functional, however natural 'directly within one place' may sound. This lesson's file declares both and the profile checker refuses it, naming the property; the fix is either to drop the functional declaration or to keep a separate non-transitive 'directlyWithin' that is functional beneath a transitive 'within' above it.

An RL engine does not enforce the global restriction, and this file shows why the restriction exists. The closure takes transitivity first, so that Wigtown is within Scotland, and then functionality, so that Wigtown's two within values must be one place, and it concludes that Dumfries and Galloway is Scotland, owl:sameAs, and that Scotland is within itself. The measured line has those triples, and nothing was wrong with the data.

The SPARQL course's module 18 measures what a reasoner does with bs:within across the whole Trail, where sixty-three asserted pairs become a hundred and eighty-six, and module 08 reaches that same number three different ways.

Diagram

   characteristic        axiom                                   consequence

   transitive            bs:within a owl:TransitiveProperty      within chains close: 63 pairs -> 186
   symmetric             bs:connectsTo a owl:SymmetricProperty   A connectsTo B => B connectsTo A
   asymmetric            bs:imprintOf a owl:AsymmetricProperty   A imprintOf B and B imprintOf A: contradiction
   irreflexive           bs:imprintOf a owl:IrreflexiveProperty  A imprintOf A: contradiction
   functional            bs:locatedIn a owl:FunctionalProperty   two values merge (o47)

   the global restriction: transitive => non-simple => not functional, not in a cardinality
   bs:within a owl:TransitiveProperty, owl:FunctionalProperty     NOT in OWL 2 DL
   ... and an RL engine, which does not check, concludes:
   bt:place-dumfries-galloway owl:sameAs bt:place-scotland

What to take away

  • A characteristic is an axiom about the whole relation, and each of them has a consequence the reasoner will act on.
  • Transitive makes a property non-simple, and non-simple properties cannot be functional or counted; the checker enforces that, while an RL engine does not and merges a council area with a country instead.
  • When you want both, split the property into a functional direct relation with a transitive closure above it.

Measured

Parses29 triples; 3 classes; 3 object properties; 3 annotation properties; 5 individuals
Profile (ROBOT)OWL 2 DL: not in profile Use of non-simple property in functional axiom <https://example.org/bookshop-trail/schema#within> [FunctionalObjectProperty(<https://example
OWL 2 RL closure (owlrl; what Show Facts computes)+5 triples about the Trail's terms
  • bt:shop-marginalia bs:connectsTo bt:shop-inkwell
  • bt:place-scotland bs:within bt:place-dumfries-galloway
  • bt:place-scotland owl:sameAs bt:place-dumfries-galloway
  • bt:place-dumfries-galloway owl:sameAs bt:place-scotland
  • bt:place-wigtown bs:within bt:place-scotland
Query: What the closure adds: the transitive and symmetric consequences8 rows
  • bt:shop-inkwell bs:connectsTo bt:shop-marginalia
  • bt:shop-marginalia bs:connectsTo bt:shop-inkwell
  • bt:place-dumfries-galloway bs:within bt:place-scotland
  • bt:place-dumfries-galloway bs:within bt:place-dumfries-galloway
  • bt:place-scotland bs:within bt:place-scotland
  • bt:place-scotland bs:within bt:place-dumfries-galloway
  • bt:place-wigtown bs:within bt:place-dumfries-galloway
  • bt:place-wigtown bs:within bt:place-scotland

Afterwards, in the SPARQL panel

What the closure adds: the transitive and symmetric consequences (on the reasoned)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?s ?p ?o WHERE { ?s ?p ?o . FILTER(?p IN (bs:within, bs:connectsTo)) } ORDER BY ?p ?s

Sources

O49

Inverses, chains and keys

bs:wrote is declared the inverse of bs:author so that one assertion yields both directions; a property chain gives an event its town; and a key on ISBN says what identifies a work, without leaving OWL 2 DL.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Work      a owl:Class .
bs:Person    a owl:Class .
bs:Event     a owl:Class .
bs:Bookshop  a owl:Class .

bs:author    a owl:ObjectProperty ; rdfs:subPropertyOf bs:hasContributor .
bs:wrote     a owl:ObjectProperty ; owl:inverseOf bs:author .
bs:translatedBy   a owl:ObjectProperty ; rdfs:subPropertyOf bs:hasContributor .
bs:hasContributor a owl:ObjectProperty .   # anyone who contributed in any capacity: a sub-property hierarchy

bs:heldAt     a owl:ObjectProperty .
bs:locatedIn  a owl:ObjectProperty .
bs:eventTown  a owl:ObjectProperty ; owl:propertyChainAxiom ( bs:heldAt bs:locatedIn ) .

bs:isbn  a owl:DatatypeProperty .
bs:Work  owl:hasKey ( bs:isbn ) .

# Inverse-functional on an object property is inside DL: a website resource
# identifies its shop. On a datatype property it is not, which is why the
# ISBN is a key instead.
bs:hasWebsite  a owl:ObjectProperty, owl:InverseFunctionalProperty ; rdfs:domain bs:Bookshop .

bt:book-the-loom  a bs:Work ; bs:author bt:author-rhona-blackwood ; bs:isbn "9787063088596" .
bt:book-duplicate a bs:Work ; bs:isbn "9787063088596" .
bt:author-rhona-blackwood  a bs:Person .
bt:event-1        a bs:Event ; bs:heldAt bt:shop-inkwell .
bt:shop-inkwell   a bs:Bookshop ; bs:locatedIn bt:place-wigtown .

Why

owl:inverseOf says that two properties are one relation read in opposite directions, so that every bs:author triple yields a bs:wrote triple and the other way about, and the reasoner materialises whichever half was not asserted. That is what the SPARQL course's q44 constructs by hand, and what gist declines to do at all on the grounds that a caret in a query costs nothing, as o31 explains; the Trail declares two inverses deliberately so that this lesson and module 08 have something to show.

A property chain is an inverse's larger sibling, since bs:heldAt followed by bs:locatedIn implies bs:eventTown, so that an event held at a shop in Wigtown is an event in Wigtown and the reasoner adds the triple; the RL closure supports chains and so does the editor, and the global restriction from o48 applies here too, a property defined by a chain being non-simple. rdfs:subPropertyOf is the property hierarchy's own inheritance, and since bs:author and bs:translatedBy both sit under bs:hasContributor, a query for everyone who contributed to a work in any capacity is a single pattern once the closure has run.

A key is the DL-safe way of saying that a value identifies a thing. An inverse-functional datatype property would say it too, that no two works share an ISBN, and it falls outside OWL 2 DL for datatype properties; owl:hasKey says it from inside DL, so that two Works with the same bs:isbn are one Work, with the merging behaviour of o47. That is meaning, an ISBN really does identify an edition, and the shape that says no two rows share an ISBN is the SHACL course's s39, which checks the check digit into the bargain.

Diagram

   inverse     bs:wrote owl:inverseOf bs:author
               bt:book-x bs:author bt:person-y   =>  bt:person-y bs:wrote bt:book-x

   chain       bs:heldAt o bs:locatedIn  =>  bs:eventTown
               bt:event-1 bs:heldAt bt:shop-inkwell . bt:shop-inkwell bs:locatedIn bt:place-wigtown .
                 =>  bt:event-1 bs:eventTown bt:place-wigtown

   key         bs:Work owl:hasKey ( bs:isbn )
               two Works, one ISBN  =>  one Work (sameAs)        inside OWL 2 DL
               bs:isbn a owl:InverseFunctionalProperty           outside OWL 2 DL for a datatype property

What to take away

  • An inverse is one relation read both ways, so declare it where a reasoner will be there to use it, and otherwise let the query language walk it backwards for nothing.
  • A property chain derives a relation from a path, and the RL closure and the editor both compute it.
  • owl:hasKey is how you say that a value identifies a thing while staying inside DL, and like every identity axiom it merges.

Measured

Parses39 triples; 4 classes; 8 object properties; 1 datatype property; 3 annotation properties; 5 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 7 inferred
  • bt:book-duplicate bs:author bt:author-rhona-blackwood
  • bt:author-rhona-blackwood bs:wrote bt:book-the-loom
  • bs:author owl:inverseOf bs:wrote
  • bt:event-1 bs:eventTown bt:place-wigtown
  • bt:book-duplicate bs:hasContributor bt:author-rhona-blackwood
  • bt:book-the-loom bs:hasContributor bt:author-rhona-blackwood
  • bt:author-rhona-blackwood bs:wrote bt:book-duplicate
OWL 2 RL closure (owlrl; what Show Facts computes)+8 triples about the Trail's terms
  • bt:event-1 bs:eventTown bt:place-wigtown
  • bt:book-duplicate bs:hasContributor bt:author-rhona-blackwood
  • bt:author-rhona-blackwood bs:wrote bt:book-duplicate
  • bt:book-the-loom owl:sameAs bt:book-duplicate
  • bt:book-duplicate bs:author bt:author-rhona-blackwood
  • bt:book-duplicate owl:sameAs bt:book-the-loom
  • bt:book-the-loom bs:hasContributor bt:author-rhona-blackwood
  • bt:author-rhona-blackwood bs:wrote bt:book-the-loom
Query: The inferred inverse, the chain, and the key's merge5 rows
  • bt:book-the-loom owl:sameAs bt:book-duplicate
  • bt:book-duplicate owl:sameAs bt:book-the-loom
  • bt:event-1 bs:eventTown bt:place-wigtown
  • bt:author-rhona-blackwood bs:wrote bt:book-duplicate
  • bt:author-rhona-blackwood bs:wrote bt:book-the-loom

Afterwards, in the SPARQL panel

The inferred inverse, the chain, and the key's merge (on the reasoned)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?s ?p ?o WHERE { ?s ?p ?o . FILTER(?p IN (bs:wrote, bs:eventTown, owl:sameAs) && ?s != ?o) } ORDER BY ?p

Sources

O50

Datatypes and profiles: where the language draws its lines

Values are typed only with datatypes in the OWL 2 map, which excludes xsd:gYear and xsd:date; the year properties are left without a range for that reason; and the profile a consumer needs is checked, not assumed.

Corner: referent · Runs in the browser; the profile verdicts come from ROBOT; the editor parses the file without complaint

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Bookshop  a owl:Class .

# Outside the map: the range the shipped vocabulary declines to state.
bs:founded  a owl:DatatypeProperty ; rdfs:domain bs:Bookshop ; rdfs:range xsd:gYear ;
    skos:editorialNote "xsd:gYear is not in the OWL 2 datatype map. The profile checker refuses this file for it."@en .

# Inside the map, with a restriction: a staff count is an integer of at least zero.
bs:staffCount  a owl:DatatypeProperty ; rdfs:domain bs:Bookshop ;
    rdfs:range [ a rdfs:Datatype ; owl:onDatatype xsd:integer ; owl:withRestrictions ( [ xsd:minInclusive 0 ] ) ] .

bt:shop-inkwell  a bs:Bookshop ; bs:founded "1979"^^xsd:gYear ; bs:staffCount 4 .

Why

OWL 2 has a datatype map, and it is a good deal shorter than XML Schema's. xsd:integer, xsd:decimal, xsd:string, xsd:boolean, xsd:dateTime and xsd:anyURI are on it; xsd:gYear and xsd:date are not. A range of xsd:gYear on bs:founded therefore puts the vocabulary outside OWL 2 DL, and declaring xsd:gYear as an rdfs:Datatype makes matters worse rather than better, since it becomes a user-defined datatype and using one in an assertion is a second violation. So the shipped vocabulary declares no range at all on its four year properties and its two date properties and says why in each comment, while the SPARQL course keeps the gYear values in the data because the portability lesson they teach is worth more than the conformance. This file states the range and takes the consequence, which the profile checker duly reports.

Datatype restrictions are the useful part of the map, since a staff count is an integer of at least zero, written as a restriction on xsd:integer with xsd:minInclusive, and that is meaning, which a reasoner will use.

The profiles are three smaller languages inside DL, each with a reasoning guarantee of its own: EL for large class hierarchies with existentials, QL for querying data through the ontology, and RL for rule engines, which is what the editor's reasoner and the owlrl closure both are. Each of them drops something, so that EL has no universals or inverses, QL no existentials on the right, and RL no existentials on the right either and no class as a superclass expression with a someValuesFrom in it. The measured line says which profiles this file is in, and the shipped vocabulary is in DL and in none of the three, EL and QL refusing its functional object properties and RL the union domain of bs:locatedIn. Pick the profile that the consumer's reasoner runs, and then check it.

Diagram

   in the OWL 2 datatype map              not in it
   xsd:integer xsd:decimal xsd:string     xsd:gYear   xsd:date   xsd:gMonth ...
   xsd:boolean xsd:dateTime xsd:anyURI    (a range of one: not DL; declaring it: worse)

   a datatype restriction:  bs:staffCount rdfs:range [ owl:onDatatype xsd:integer ; owl:withRestrictions ( [ xsd:minInclusive 0 ] ) ]

   profile   guarantee                          drops
   EL        polynomial classification          universals, inverses, disjunction
   QL        query rewriting over data          existentials on the right, chains
   RL        rule-based closure (the editor)    existentials on the right; someValuesFrom in a superclass

What to take away

  • The datatype map is the list, and xsd:gYear and xsd:date are not on it, so a range naming them leaves DL, while declaring them makes it worse.
  • A datatype restriction is meaning about values and it is cheap, whereas a missing range is often the right choice.
  • Choose a profile by the reasoner that will actually run, and then check it; the editor's is RL.

Measured

Parses20 triples; 1 class; 2 datatype properties; 3 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: not in profile Use of undeclared datatype: xsd:gYear [DataPropertyRange(<https://example.org/bookshop-trail/schema#founded> xsd:gYear) in OntologyID(Anonym
OWL 2 EL: not in profile Use of undeclared datatype: xsd:gYear [DataPropertyRange(<https://example.org/bookshop-trail/schema#founded> xsd:gYear) in OntologyID(Anonym
OWL 2 QL: not in profile Use of undeclared datatype: xsd:gYear [DataPropertyRange(<https://example.org/bookshop-trail/schema#founded> xsd:gYear) in OntologyID(Anonym
OWL 2 RL: not in profile Use of undeclared datatype: xsd:gYear [DataPropertyRange(<https://example.org/bookshop-trail/schema#founded> xsd:gYear) in OntologyID(Anonym

Sources

Module 08

Reasoning

What follows from what you said, which covers consistency, unsatisfiable classes, the inferred hierarchy, an explanation traced back to the axioms that caused it, and the difference between an OWL 2 RL closure like the editor's and a description-logic reasoner like HermiT. The module ends by reasoning over the whole Trail, where it finds the reason one property was left non-functional.

Sources
O51

What follows from what you said

An inference is a statement true in every interpretation that satisfies the ontology; a reasoner finds them; adding axioms never removes one; and 'not said' is not 'false'.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Place       a owl:Class ; skos:prefLabel "Place"@en .
bs:Settlement  a owl:Class ; rdfs:subClassOf bs:Place ; skos:prefLabel "Settlement"@en .
bs:Bookshop    a owl:Class .
bs:website     a owl:DatatypeProperty .

bt:place-wigtown  a bs:Settlement .
bt:shop-marginalia a bs:Bookshop .     # no website recorded: the reasoner concludes nothing

Why

Every settlement is a place, and Wigtown is a settlement, so Wigtown is a place. Nobody wrote that third sentence and it is true anyway, because there is no way for the first two to hold while the third fails, and that is entailment, which is the whole of what a reasoner does: it finds the statements that hold in every interpretation the axioms allow. The set reading makes it plain enough, since S is inside P and w is in S, so w is in P, and there is nothing left to decide.

Three kinds of thing can follow. There is a new triple, which a materialising reasoner writes into the graph, as the editor's Show Facts does; there is a classification, that one class lies inside another though nobody said so; and there is a contradiction, that no interpretation exists at all. RDFS can give you the first of those, and OWL 2 gives all three.

Two properties of the logic decide how it feels to use. It is monotonic, so that adding an axiom can add conclusions and can never take one away, and a conclusion drawn today stays drawn; monotonicity is what makes materialisation safe, and it is why a SHACL rule with a negation in it, which the SHACL course's s52 shows going stale, is a quite different kind of thing. And it is open-world, so that a statement not in the graph is not false but unknown, and absence entails nothing whatever. The reasoner will never tell you that a shop has no website; a query will, and that difference is the subject of the next lesson.

Diagram

   axioms                                     what follows

   bs:Settlement rdfs:subClassOf bs:Place     w in S, S inside P  =>  w in P
   bt:place-wigtown a bs:Settlement            a new triple:      bt:place-wigtown a bs:Place

   three kinds of consequence:  a triple  |  a classification  |  a contradiction
   monotonic:  more axioms, never fewer conclusions
   open world: not said is not false; absence entails nothing

What to take away

  • Entailment is what holds in every interpretation the axioms allow, so a reasoner finds that while a query finds what is written.
  • The logic is monotonic, and nothing you add takes a conclusion away, which is why materialising is safe and why rules with negation in them are not the same kind of thing.
  • The world is open, so the reasoner never concludes anything from absence; for 'has no website', ask a query.

Measured

Parses12 triples; 3 classes; 1 datatype property; 3 annotation properties; 2 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 1 inferred
  • bt:place-wigtown rdf:type bs:Place
OWL 2 RL closure (owlrl; what Show Facts computes)+1 triples about the Trail's terms
  • bt:place-wigtown rdf:type bs:Place
Query: What the reasoner cannot say: shops with no website1 row
  • bt:shop-marginalia

Afterwards, in the SPARQL panel

What the reasoner cannot say: shops with no website (on the model)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?shop WHERE { ?shop a bs:Bookshop . FILTER NOT EXISTS { ?shop bs:website ?w } }

Sources

O52

Three routes to one inference

The transitive closure of bs:within, sixty-three pairs becoming a hundred and eighty-six, is reached by an OWL reasoner, by a SPARQL property path, and by a SHACL rule iterated to a fixpoint; each is measured, and the choice between them is a matter of when and where the work is done.

Corner: referent · Data: 03-places.ttl · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Place   a owl:Class ; skos:prefLabel "Place"@en .
bs:within  a owl:ObjectProperty, owl:TransitiveProperty ;
    rdfs:domain bs:Place ; rdfs:range bs:Place ;
    skos:definition "Directly inside another place. Transitive, so a reasoner closes the chain; bs:within+ does the same at query time."@en .

Why

Wigtown is within Dumfries and Galloway, which is within Scotland, which is within Great Britain; the data gives us the first three links and question 1 in the brief wants the fourth. There are three ways to get it, and all three give the same answer.

An OWL reasoner, given bs:within a owl:TransitiveProperty, writes the missing pairs into the graph. The editor's Show Facts does this in the browser, and the RL closure that this lesson was measured with adds the 123 missing pairs among the Trail's own places, with HermiT agreeing. Both of them add rather more than that, because the place file links each town to its DBpedia page with owl:sameAs and a reasoner copies every pair onto the alias, so the queries count the Trail's own IRIs, and o04 said why sameAs behaves like that. The work is done once, up front, and the result is stored, which means it goes stale the day a place moves and a reasoner has to run again.

A SPARQL property path, bs:within+, computes the same closure at query time instead. Nothing is stored and nothing goes stale, and it runs on any SPARQL 1.1 engine with no reasoner at all, which is what the SPARQL course's module 05 is built on; the cost is paid on every query, and for a few hundred places that is nothing, while for a few million it becomes a design decision.

A SHACL rule, which is a CONSTRUCT that adds one hop, iterated until nothing new appears, materialises that same closure, and the editor's Inference dropdown will run it. The SHACL course's s51 is that rule, and the SemOps Manual's chapter 11 calls this the third way to derive a triple and says when it is the right one: when the rule is a governed artefact that somebody owns, and when the closure has to be visible to a validator that has no reasoner of its own. This lesson's companion file holds the rule, and beside it a second shape that reports every bs:within pair at Info severity, because a validator's only output is its report and that is the only way to see what a rule did; run with the Inference dropdown at none it reports the 63 asserted pairs, at rules the 123 of one pass, and at rules-iterated all 186.

So: the same 186 pairs, three engines, and three different moments at which to pay for them. The SPARQL course's lab set the reasoner and the path side by side, and the table in the diagram adds the rule.

Diagram

                     OWL reasoner              SPARQL path             SHACL rule (iterated)
   what you write    owl:TransitiveProperty    bs:within+              CONSTRUCT one hop; iterate
   when it runs      once, before queries      every query             once, before validation
   what is stored    186 triples               nothing                 186 triples
   goes stale?       yes; re-run               never                   yes; re-run
   how you see it    Show Facts lists them     the answer is the rows  a shape reports each: 63 -> 123 -> 186
   needs             a reasoner (Show Facts)   any SPARQL 1.1 engine   a SHACL-AF engine (Inference: rules, iterated)
   explains itself?  a DL reasoner can (o56)   the path is the reason  the rule is the reason

   measured on data/03-places.ttl:  63 asserted  ->  186  by all three routes

What to take away

  • Materialise, by reasoner or by rule, when many queries will read the closure and the data changes slowly; query with a path when they will not.
  • A path needs no reasoner and never goes stale, which makes it the portable route and the first one to reach for.
  • A rule is a governed artefact, with somebody owning it and a validator able to see its output, and it is the route to take when the consumer has no reasoner.

Note

Open in the editor opens this file with the rule in a second tab and Inference already at 'rules, iterated'; to see the closure on the whole place hierarchy, load data/03-places.ttl in the first tab instead and validate, or press Show Facts for the reasoner's route. The AFTERWARDS queries run on the place file.

Measured

Parses10 triples; 1 class; 1 object property; 3 annotation properties (with the data: 804 triples)
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 353 inferred
  • <http://dbpedia.org/resource/Hay-on-Wye> rdf:type bs:Place
  • <http://dbpedia.org/resource/Ludlow> bs:within bt:place-gb
  • <http://dbpedia.org/resource/Keswick,_Cumbria> rdf:type geo:Feature
  • <http://dbpedia.org/resource/Kendal> owl:sameAs bt:place-kendal
  • bt:place-oxford bs:within bt:place-gb
  • bt:place-sedbergh bs:within bt:place-gb
  • <http://dbpedia.org/resource/Oxford> bs:within bt:place-england
  • bt:place-county-durham bs:within bt:place-england
  • bt:place-keswick bs:within bt:place-england
  • <http://dbpedia.org/resource/Keswick,_Cumbria> owl:sameAs bt:place-keswick
and 343 more
OWL 2 RL closure (owlrl; what Show Facts computes)+532 triples about the Trail's terms
  • <http://dbpedia.org/resource/Hay-on-Wye> rdf:type bs:Place
  • <http://dbpedia.org/resource/Keswick,_Cumbria> rdf:type geo:Feature
  • <http://dbpedia.org/resource/Berwick-upon-Tweed> bs:isBookTown "false"
  • bt:place-oxford bs:within bt:place-gb
  • <http://dbpedia.org/resource/Oxford> bs:within bt:place-england
  • <http://dbpedia.org/resource/Sedbergh> geo:hasGeometry bt:geom-sedbergh
  • <http://dbpedia.org/resource/Fort_William,_Highland> rdf:type bs:Settlement
  • bt:place-greater-london bs:within bt:place-england
  • <http://dbpedia.org/resource/Edinburgh> geo:hasDefaultGeometry bt:geom-edinburgh
  • <http://dbpedia.org/resource/Exeter> rdf:type bs:Place
and 522 more
SHACL-AF rule (the editor's Inference dropdown)One Info result per derived triple, counted at each mode:
asserted only63 reported
rules, one pass123 reported
rules, iterated to a fixpoint186 reported
Query: Route 2, the path: every Trail place and everything it is within186 rows
  • bt:place-scotland bt:place-gb
  • bt:place-england bt:place-gb
  • bt:place-wales bt:place-gb
  • bt:place-north-east-england bt:place-england
  • bt:place-north-east-england bt:place-gb
  • bt:place-north-west-england bt:place-england
  • bt:place-north-west-england bt:place-gb
  • bt:place-yorkshire bt:place-england
Query: Route 1, the reasoner: the same pairs, asserted after the closure186 rows
  • bt:place-inverness bt:place-highland
  • bt:place-fort-william bt:place-highland
  • bt:place-portree bt:place-highland
  • bt:place-south-west-england bt:place-england
  • bt:place-east-of-england bt:place-england
  • bt:place-south-east-england bt:place-england
  • bt:place-west-midlands bt:place-england
  • bt:place-north-east-england bt:place-england
Query: Question 1: which settlements are in Scotland, by the path8 rows
  • bt:place-edinburgh
  • bt:place-fort-william
  • bt:place-glasgow
  • bt:place-inverness
  • bt:place-perth
  • bt:place-portree
  • bt:place-st-andrews
  • bt:place-wigtown

Afterwards, in the SPARQL panel

Route 2, the path: every Trail place and everything it is within (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?a ?b WHERE { ?a bs:within+ ?b . FILTER(STRSTARTS(STR(?a), "https://example.org/bookshop-trail/") && STRSTARTS(STR(?b), "https://example.org/bookshop-trail/")) }
Route 1, the reasoner: the same pairs, asserted after the closure (on the reasoned)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?a ?b WHERE { ?a bs:within ?b . FILTER(STRSTARTS(STR(?a), "https://example.org/bookshop-trail/") && STRSTARTS(STR(?b), "https://example.org/bookshop-trail/")) }
Question 1: which settlements are in Scotland, by the path (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?place WHERE { ?place a bs:Settlement ; bs:within+ bt:place-scotland } ORDER BY ?place

Sources

O53

Inconsistency: the three usual causes

A contradiction is traced to one of three habits: a thing typed with two disjoint kinds, a functional property with two values declared different, or a value that its datatype cannot hold; each is seeded, and the reasoner names the axioms responsible.

Corner: referent · Runs in the browser; HermiT's verdict and the explanation come from ROBOT

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Place a owl:Class . bs:Bookshop a owl:Class . bs:Settlement a owl:Class .
bs:Place  owl:disjointWith  bs:Bookshop .
bs:locatedIn   a owl:ObjectProperty, owl:FunctionalProperty ; rdfs:range bs:Settlement .
bs:staffCount  a owl:DatatypeProperty ;
    rdfs:range [ a rdfs:Datatype ; owl:onDatatype xsd:integer ; owl:withRestrictions ( [ xsd:minInclusive 0 ] ) ] .

# 1. two disjoint kinds
bt:oddity  a bs:Place, bs:Bookshop .

# 2. functional, two values, declared different
bt:shop-x         a bs:Bookshop ; bs:locatedIn bt:place-wigtown, bt:place-hay .
bt:place-wigtown  a bs:Settlement ; owl:differentFrom bt:place-hay .
bt:place-hay      a bs:Settlement .
# The plural form, for a whole list of things that are pairwise distinct:
[] a owl:AllDifferent ; owl:members ( bt:place-wigtown bt:place-hay bt:place-sedbergh ) .
bt:place-sedbergh a bs:Settlement .

# 3. a value outside its datatype
bt:shop-y  a bs:Bookshop ; bs:staffCount -3 .

Why

An ontology is inconsistent when no interpretation satisfies it, which is one verdict for the whole graph, and the first thing to know about it is that it is rarely subtle. Almost every inconsistency I have met in practice has one of three shapes: a thing typed with two classes declared disjoint, which is usually a data load with the wrong kind in it, as o43 describes; a functional property with two values that are declared different or can be proved so, whether two towns for one shop with owl:differentFrom between them or two integers, since literals differ when they differ, as o47 describes; or a value outside its datatype, such as a staff count of minus three where the range is an integer of at least zero. A value that is not even a well- formed literal, 'four' as an xsd:integer, never reaches the reasoner at all, since the OWL API refuses to load the file, which is a stricter kind of no.

This file seeds all three so that HermiT's verdict is unmistakable and the reasoner can be asked to explain itself. ROBOT's explain command in inconsistency mode returns the smallest sets of axioms that produce the contradiction, and asked for all of them it finds four here, not three. The second cause is seeded twice, once with owl:differentFrom and once with owl:AllDifferent, and each closes the contradiction on its own, so it has two sets, and deleting either declaration leaves the ontology as inconsistent as before. ROBOT's default is one explanation, whichever the reasoner happens to reach first, which is why --max matters. Explain is the tool to reach for when a large ontology turns inconsistent after a change, since the question to ask is not what the ontology says but which few axioms are to blame.

The editor's RL closure will show you the sameAs and the types and say nothing at all about a contradiction, so for the browser each cause becomes a query, and the three queries here find the three seeds.

Diagram

   cause                                   seed                                               query
   two disjoint kinds                      bt:oddity a bs:Place, bs:Bookshop                  ?x a ?a, ?b . ?a owl:disjointWith ?b
   functional, two values, different       bt:shop-x locatedIn wigtown, hay ; wigtown differentFrom hay
                                                                                              ?x ?p ?a, ?b . ?p a owl:FunctionalProperty . ?a owl:differentFrom ?b
   a value outside its datatype            bt:shop-y bs:staffCount -3 ; range: integer >= 0   FILTER(?v < 0) on the restricted property

   HermiT: inconsistent.  robot explain --mode inconsistency --max 10: four sets of axioms, two of them for cause 2

What to take away

  • Inconsistency is one verdict for the whole graph, and it almost always turns out to have one of three shapes.
  • Ask the reasoner to explain itself, because the smallest set of axioms behind a contradiction is a few lines however large the ontology may be.
  • The browser cannot report a contradiction, but each cause is a query, and those queries belong in the gate.

Measured

Parses37 triples; 3 classes; 1 object property; 1 datatype property; 3 annotation properties; 6 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTinconsistent
Explanation (ROBOT)
  • \1 \1 \1
  • \1 \1 \1
  • Functional: \1
  • DifferentIndividuals: \1, \1, \1
  • \1 Type \1
  • \1 DisjointWith \1
  • \1 Type \1
  • \1 \1 -3
Query: Cause 1: a thing with two disjoint types1 row
  • bt:oddity
Query: Cause 2: a functional property with two values known to differ1 row
  • bt:shop-x bs:locatedIn
Query: Cause 3: a staff count below zero1 row
  • bt:shop-y "-3"

Afterwards, in the SPARQL panel

Cause 1: a thing with two disjoint types (on the model)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?x WHERE { ?a owl:disjointWith ?b . ?x a ?a, ?b }
Cause 2: a functional property with two values known to differ (on the model)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?x ?p WHERE { ?p a owl:FunctionalProperty . ?x ?p ?a, ?b . ?a owl:differentFrom ?b }
Cause 3: a staff count below zero (on the model)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?x ?v WHERE { ?x bs:staffCount ?v . FILTER(?v < 0) }

Sources

O54

Unsatisfiable classes: consistent, and dead

A class that can have no instances is found by the reasoner before any data arrives; the ontology stays consistent, and the class is fixed rather than shipped.

Corner: referent · Runs in the browser; the unsatisfiable classes on the MEASURED line come from HermiT through ROBOT, and REA-021 from the Quality Suite

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Work a owl:Class . bs:Person a owl:Class . bs:Settlement a owl:Class . bs:Publisher a owl:Class .
bs:Work  owl:disjointWith  bs:Person .
bs:locatedIn  a owl:ObjectProperty .

# Under two disjoint parents.
bs:Memoir  a owl:Class ; rdfs:subClassOf bs:Work, bs:Person ;
    skos:editorialNote "A memoir is a work about a person; someone made it both. Unsatisfiable."@en .

# The trivial case: a class declared empty outright.
bs:Ghost  a owl:Class ; rdfs:subClassOf owl:Nothing .

# Restrictions that contradict.
bs:Settlement  owl:disjointWith  bs:Publisher .
bs:OddShop  a owl:Class ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty bs:locatedIn ; owl:someValuesFrom bs:Settlement ] ,
                    [ a owl:Restriction ; owl:onProperty bs:locatedIn ; owl:allValuesFrom  bs:Publisher ] .

Why

An unsatisfiable class is one whose extension is empty in every interpretation, so that no possible thing could be a member of it. The ontology remains consistent, since nothing has been asserted to be in the class, and that is exactly what makes the fault a quiet one: it sits there until the first individual is typed with it, and then the whole graph is inconsistent and the cause is a class somebody defined months ago.

The usual shapes are a class under two disjoint parents, like the Translation that o34 seeded disjoint from Work; a class whose restrictions contradict one another, located in some Settlement and located in only Publishers; and a class defined as the intersection of two disjoint classes. All of them are found by classification with no data at all, and ROBOT's reason command lists them and then refuses to write the reasoned file until they are fixed, which is the right default for a gate. The Quality Suite reports each of them as REA-021, and for this file it also reports REA-020, inconsistent, where ROBOT says consistent with two unsatisfiable classes: two tools and two verdicts on one file, and the difference is precisely this lesson, since unsatisfiable is about a class and inconsistent is about the whole graph, and a checker that folds the one into the other is not wrong so much as coarser. LOG-001, the suite's reasoner-free check, is narrower still, catching a class disjoint with its own ancestor, which neither of these is.

In set terms an unsatisfiable class is a subset of the empty set, owl:Nothing is the empty set's name, and 'equivalent to owl:Nothing' is what a reasoner says when it finds one.

Diagram

   bs:Work owl:disjointWith bs:Person
   bs:Memoir rdfs:subClassOf bs:Work, bs:Person          M inside W and inside P, and W and P share nothing
                                                          => M is empty in every interpretation
                                                          => M is equivalent to owl:Nothing

   consistent (nobody is a Memoir yet); unsatisfiable (nobody ever can be)
   the first bt:book-x a bs:Memoir makes the whole graph inconsistent

What to take away

  • An unsatisfiable class is a fault with no data in it yet, so classify before you load and fix whatever the reasoner names.
  • The gate should refuse an ontology that has one; ROBOT does so by default, and the Quality Suite reports REA-021.
  • Under two disjoint parents, or with two restrictions that contradict: those are the shapes to look for when a class comes back empty.

Measured

Parses25 triples; 7 classes; 1 object property; 3 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; unsatisfiable: OddShop, Ghost, Memoir
Ontology Quality Suite
  • REA-020 ×1: The ontology (plus data, if included) is logically inconsistent according to the hermit reasoner: no model satisfies every axiom simultaneously.
  • REA-021 ×3: https://example.org/bookshop-trail/schema#OddShop is unsatisfiable (equivalent to owl:Nothing) according to the hermit reasoner.

Sources

O55

Classification: the hierarchy the reasoner infers

Subclass relations that follow from definitions are left for the reasoner to infer rather than asserted, and the inferred hierarchy is compared with the asserted one as a check on both.

Corner: referent · Runs in the browser; the inferred hierarchy comes from HermiT through ROBOT; Show Facts will not derive it

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Person a owl:Class . bs:Work a owl:Class . bs:Publisher a owl:Class . bs:Settlement a owl:Class .
bs:wrote     a owl:ObjectProperty .
bs:locatedIn a owl:ObjectProperty .

bs:Author  a owl:Class ;
    owl:equivalentClass [ owl:intersectionOf ( bs:Person [ a owl:Restriction ; owl:onProperty bs:wrote ; owl:someValuesFrom bs:Work ] ) ] .

bs:LocalPublisher  a owl:Class ;
    owl:equivalentClass [ owl:intersectionOf ( bs:Publisher [ a owl:Restriction ; owl:onProperty bs:locatedIn ; owl:someValuesFrom bs:Settlement ] ) ] .

bs:WigtownPublisher  a owl:Class ;
    owl:equivalentClass [ owl:intersectionOf ( bs:Publisher [ a owl:Restriction ; owl:onProperty bs:locatedIn ; owl:hasValue bt:place-wigtown ] ) ] .

bt:place-wigtown  a bs:Settlement .

Why

Classification is the reasoner working out the subclass relations between every pair of classes, asserted or otherwise. Define Author as a Person who wrote some Work and Author is under Person without anybody having written rdfs:subClassOf; define LocalPublisher as a Publisher located in some Settlement, and PublisherInWigtown as a Publisher located in Wigtown, and the reasoner places the second under the first, because a value is a special case of some. Nobody said so, and it follows all the same.

That is the reason to define rather than declare wherever a definition is possible. An asserted hierarchy records what somebody believed, an inferred one records what the definitions entail, and the two can be compared; where they differ one of them is wrong, and in my experience it is usually the assertion. gist's habit of defining classes by equivalence to an intersection, which o32 describes, is this same habit at scale.

ROBOT's reason command writes the inferred axioms into the ontology and the measured line counts them. The editor's RL closure does not classify, since deriving a subclass axiom from restrictions lies outside RL; it types individuals instead, which is the other half of the same work, and o56 sets the two side by side.

Diagram

   asserted                                              inferred (HermiT)

   bs:Author = Person and (wrote some Work)              bs:Author rdfs:subClassOf bs:Person
   bs:LocalPublisher = Publisher and (locatedIn some Settlement)
   bs:WigtownPublisher = Publisher and (locatedIn value bt:place-wigtown)
                                                          bs:WigtownPublisher rdfs:subClassOf bs:LocalPublisher
   (bt:place-wigtown a bs:Settlement)                     because 'value w' is a case of 'some S' when w is in S

   define where you can; assert where you must; compare the two

What to take away

  • Classification infers the subclass relations that the definitions entail, so define rather than assert, and let it.
  • Compare the inferred hierarchy with the asserted one, because a difference means that a belief and a definition disagree.
  • The RL closure types individuals and does not classify classes, and that is the line between the editor's reasoner and a DL one.

Measured

Parses40 triples; 7 classes; 2 object properties; 3 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 3 inferred
  • bs:Author rdfs:subClassOf bs:Person
  • bs:WigtownPublisher rdfs:subClassOf bs:LocalPublisher
  • bs:LocalPublisher rdfs:subClassOf bs:Publisher
OWL 2 RL closure (owlrl; what Show Facts computes)+3 triples about the Trail's terms
  • bs:LocalPublisher rdfs:subClassOf bs:Publisher
  • bs:WigtownPublisher rdfs:subClassOf bs:Publisher
  • bs:Author rdfs:subClassOf bs:Person

Sources

O56

Two reasoners: the rule engine and the description logic

The editor's OWL 2 RL closure is used for what it does well, typing individuals and closing properties, and a description-logic reasoner for what only it does, which is to classify, to find the dead classes, and to explain.

Corner: referent · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

bs:Work  a owl:Class .
bs:translationOf  a owl:ObjectProperty .

bs:Translation  a owl:Class ;
    owl:equivalentClass [ owl:intersectionOf ( bs:Work [ a owl:Restriction ; owl:onProperty bs:translationOf ; owl:someValuesFrom bs:Work ] ) ] .

bs:EnglishWork  a owl:Class ; rdfs:subClassOf bs:Work .
bs:EnglishTranslation  a owl:Class ;
    owl:equivalentClass [ owl:intersectionOf ( bs:Work [ a owl:Restriction ; owl:onProperty bs:translationOf ; owl:someValuesFrom bs:EnglishWork ] ) ] .

bt:book-x  a bs:Work ; bs:translationOf bt:book-w .
bt:book-w  a bs:Work .

Why

There are two kinds of reasoner about, and it pays to know which one you are holding. An OWL 2 RL engine, which is what the editor's HyLAR and the owlrl closure this course measures with both are, applies rules to triples until nothing new appears: subclass typing, domain and range, transitive and symmetric closure, inverses, chains, sameAs from functional properties, allValuesFrom typing, and membership of defined classes wherever the fillers are named. It is fast, it scales, it runs in a browser, and it never says no.

A description-logic reasoner, HermiT in this course, decides entailment by searching for a model, and so it classifies classes, finds the unsatisfiable ones, reports inconsistency, and explains itself, which is to say it can tell you which few axioms make an inference true. It handles existentials in superclass position and everything else DL allows, and in return it is slower, it does not stream, and it wants the whole ontology in memory.

This file shows the line between them on two examples. Translation is a Work that is a translation of some Work, EnglishTranslation is a Work that is a translation of some EnglishWork, and EnglishWork is a kind of Work. Both engines type bt:book-x as a Translation, its fillers being named, and both derive that Translation is under Work, since RL has a rule that an intersection lies inside each of its parts; only HermiT derives that EnglishTranslation is under Translation, because that needs the two definitions compared as wholes and RL has no rule for it. The measured line carries both results. Use the RL engine for the data and the DL engine for the ontology, and run the DL engine in the gate, where somebody will read what it says.

Diagram

                            OWL 2 RL (editor, owlrl)      DL (HermiT)
   types individuals        yes                            yes
   closes properties        yes                            yes
   sameAs from functional   yes                            yes
   classifies classes       no                             yes
   finds unsatisfiable      no                             yes
   reports inconsistency    no (materialises past it)      yes
   explains                 no                             yes (robot explain)
   runs in the browser      yes                            no
   cost                     linear-ish, streams            exponential worst case, whole ontology

   measured here:  bt:book-x a bs:Translation                        both
                   bs:Translation subClassOf bs:Work                 both (RL: an intersection is inside each part)
                   bs:EnglishTranslation subClassOf bs:Translation   HermiT only

What to take away

  • RL is rules over triples, fast and browser-sized and never saying no; DL is models, and it classifies, finds the dead classes and explains.
  • RL has schema rules too and derives more of the hierarchy than people expect; what it cannot do is compare two definitions as wholes.
  • Use RL for the data and DL for the ontology, and run DL in the gate.
  • When a book tells you that the reasoner infers something, ask which kind of reasoner, because the answer changes what you can rely on.

Measured

Parses30 triples; 4 classes; 1 object property; 3 annotation properties; 2 individuals
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 3 inferred
  • bs:EnglishTranslation rdfs:subClassOf bs:Translation
  • bs:Translation rdfs:subClassOf bs:Work
  • bt:book-x rdf:type bs:Translation
OWL 2 RL closure (owlrl; what Show Facts computes)+3 triples about the Trail's terms
  • bs:Translation rdfs:subClassOf bs:Work
  • bs:EnglishTranslation rdfs:subClassOf bs:Work
  • bt:book-x rdf:type bs:Translation

Sources

O57

Reasoning over the whole Trail

The shipped vocabulary and the whole dataset go through a description-logic reasoner together: consistent, one hundred and eighty-six within pairs, the reversed segments and inverses materialised, and not one new type, because the data types everything already.

Corner: referent · Data: bookshop-trail-owl-dl.ttl · Runs in the browser; the reasoning over the whole dataset is HermiT through ROBOT; in the browser, load the file and press Show Facts for the RL closure

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

# This lesson's own model is the ontology header; the checker merges the
# whole OWL 2 DL copy of the dataset before reasoning.
<https://example.org/bookshop-trail/schema>
    a               owl:Ontology ;
    skos:prefLabel  "The Bookshop Trail schema"@en .

Why

Everything in this module so far has been done on a few triples, whereas the shipped vocabulary with the whole dataset comes to four thousand eight hundred, and HermiT classifies and realises it in about two seconds, which is the number to have in hand before anybody tells you that a DL reasoner is too slow for real data. The OWL 2 DL copy of the dataset is the one used, so that the profile verdict and the reasoning are about a single file; run on the copy that keeps its xsd:gYear values, HermiT reports consistent as well, treating the unknown datatype as opaque.

What it adds is exactly what the axioms promise and nothing besides. bs:within is transitive, so sixty-three pairs become a hundred and eighty-six; bs:connectsTo is symmetric, so thirty- three segments gain their reverse; bs:hasImprint is the inverse of bs:imprintOf and is asserted nowhere, so nine of them appear; bs:wrote is the inverse of bs:author, so six appear where the data used author. And not one new rdf:type on a Trail term, because the data types every individual already and the vocabulary defines no class by equivalence. The SPARQL course's module 18 measured the same thing and drew the same conclusion, that the closure is real and a property path gets most of it without a reasoner at all.

Consistent, too, which is the quiet confirmation that a vocabulary written with the habits of module 05, sparse domains, no functional property where the sources disagree, and disjointness only at the top, does not fight its own data. Change one of those habits, as o29 did, and the same run comes back inconsistent.

Diagram

   data/bookshop-trail-owl-dl.ttl: 4,796 triples, through HermiT, about two seconds

   inferred                         count   from
   bs:within                        +123    owl:TransitiveProperty      63 -> 186
   bs:connectsTo                    +33     owl:SymmetricProperty
   bs:hasImprint                    +9      owl:inverseOf bs:imprintOf  (asserted nowhere in the data)
   bs:wrote                         +6      owl:inverseOf bs:author
   rdf:type on a Trail class        0       everything is typed already

   verdict: consistent

What to take away

  • A DL reasoner on a few thousand triples takes seconds, so measure it before assuming that it will not scale.
  • What it adds is what the axioms promise, which is closures and inverses; where the data is typed already, it adds no types.
  • Consistency over the whole dataset is the cheapest end-to-end test an ontology has, so run it in the gate.

Measured

Parses5 triples; 3 annotation properties (with the data: 4799 triples)
HermiTconsistent; 173 inferred
  • bt:place-greater-london bs:within bt:place-england
  • bt:place-norwich bs:within bt:place-east-of-england
  • bt:shop-borderprint bs:connectsTo bt:shop-gutter-gilt
  • bt:shop-harbour-page bs:connectsTo bt:shop-bookwyrm
  • bt:author-bram-tillotson bs:wrote bt:book-the-tarn-gd
  • bt:place-norwich bs:within bt:place-england
  • bt:place-powys bs:within bt:place-gb
  • bt:place-city-of-cardiff bs:within bt:place-gb
  • bt:place-kendal bs:within bt:place-gb
  • bt:place-yorkshire bs:within bt:place-gb
and 163 more
OWL 2 RL closure (owlrl; what Show Facts computes)+184 triples about the Trail's terms
  • bt:place-norwich bs:within bt:place-east-of-england
  • bt:shop-borderprint bs:connectsTo bt:shop-gutter-gilt
  • bt:author-bram-tillotson bs:wrote bt:book-the-tarn-gd
  • bt:place-powys bs:within bt:place-gb
  • bt:place-city-of-cardiff bs:within bt:place-gb
  • bt:place-kendal bs:within bt:place-gb
  • bt:place-yorkshire bs:within bt:place-gb
  • bs:translationOf rdfs:domain bs:Work
  • bt:shop-cliff-road bs:connectsTo bt:shop-clock-tower
  • bt:place-glasgow-city bs:within bt:place-gb
and 174 more
Query: Places within Scotland, after the closure1 row
  • "14"

Afterwards, in the SPARQL panel

Places within Scotland, after the closure (on the reasoned)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT (COUNT(*) AS ?n) WHERE { ?p bs:within bt:place-scotland }

Sources

Module 09

Standing on gist

An upper ontology is a vocabulary in common, so this module hooks the Trail's classes onto gist, decides which of gist's patterns are worth adopting, and reads two published alignments, gistBFO and gistUFO, as worked examples of how a class comes to be placed under a foundational one and why. Two of the placements disagree with one another, which is instructive in itself.

Sources
O58

What an upper ontology is for

The Trail stands on gist rather than defining person, organisation, event and place again, because an upper ontology is a vocabulary in common, and gist is the smallest one that covers a business.

Corner: thought · Runs in the browser; the counts on the MEASURED line are computed from gistCore.ttl, fetched by scripts/setup-tools.ps1

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix gist: <https://w3id.org/semanticarts/ns/ontology/gist/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Stands on gist 14.1.0 (https://w3id.org/semanticarts/ontology/gistCore), CC-BY-4.0, Semantic Arts. The bridge axioms are in o59."@en .

gist:Person        a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Organization  a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .

Why

An upper ontology is a set of classes general enough that every domain ontology needs them and few define them well, among them person, organisation, event, place, agreement, content, and a quantity with a unit. Its value is the one o26 gave for reuse, at the level where reuse pays best: two ontologies that both put their people under gist:Person can be joined without a mapping, and two teams that have both learned gist's hundred classes share a vocabulary before they ever meet.

An upper ontology is for that, and the paper this lesson cites, Is an upper ontology useful?, sets out the whole list: a starter palette of classes and properties, commonality between domain ontologies, logical consistency done once at the top, a common vocabulary, a tool for thought, integrated data, and data that can be partitioned again afterwards. Its answer to its own title is yes, and the point about a common vocabulary is the one this course keeps coming back to, that the ontology is part of the vocabulary of the organisation, so you should choose one whose class names can enter everyday speech; continuant fiat boundary, as the paper observes, might not be for everyone.

gist is built to be that vocabulary for enterprises, and its design choices are all in the service of being learnable: about a hundred classes, everyday names, sparse domains and ranges, no inverses, disjointness at the top, everything defined in SKOS annotations, a CC-BY licence, and a release with a version number and a migration guide. The paper puts the Goldilocks level of abstraction at a human scale, about as many classes as there were people in your year at school, and the measured line has version 14's counts, taken from the file itself; BFO, at the other end of the spectrum, has thirty-six classes and a philosophy, and this module reads both.

What standing on an upper ontology costs is that its commitments come along too. Put bs:Bookshop under gist:Organization and gist's disjointness axioms now apply to bookshops, so that a bookshop cannot also be a gist:Building, however much it may be a shop with a door; which is the right consequence, and o59 finds it.

Diagram

   domain ontologies                        the upper ontology

   the Trail       bs:Person   bs:Bookshop  \
   a supplier      sup:Staff   sup:Supplier   +---->  gist:Person   gist:Organization   gist:Event ...
   a library       lib:Reader  lib:Branch   /        one vocabulary in common; joins with no mapping

   gist 14, measured from the file:  about a hundred classes, sixty-odd object properties,
   fifty datatype properties, everyday names, no inverses, CC-BY

What to take away

  • An upper ontology is a vocabulary in common, and its value is that two teams, or two datasets, meet already agreed on what a person, an organisation or an event is.
  • gist is built to be learned, with a hundred classes under ordinary names, and every design choice in module 05 comes from it.
  • Standing on an upper ontology imports its commitments, which is the point of it and also the cost.

Measured

Parses11 triples; 2 classes; 4 annotation properties (with the data: 2317 triples)
Profile (ROBOT)OWL 2 DL: in profile
OQuaRE (scripts/metrics.py)96 classes, 66 object properties, 50 datatype properties, 62 roots, 81 leaves
MetricValueScore 1 to 5
LCOMOnto1.4575
WMCOnto0.4275
DITOnto34
NACOnto0.4575
NOCOnto15
CBOOnto15
RFCOnto1.8825
NOMOnto0.3125
RROnto28%2
AROnto31%2
INROnto12%1
CROnto8%1
ANOnto366%5
TMOnto15

Sources

O59

Hooking the Trail onto gist

Each Trail kind is placed under its most specific gist class with a note saying why, two are left unplaced because nothing fits, and the whole is reasoned over with gist imported to see what gist's own axioms make of it.

Corner: referent · Runs in the browser; consistency with gist imported is HermiT through ROBOT; gistCore.ttl is fetched by scripts/setup-tools.ps1

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix gist: <https://w3id.org/semanticarts/ns/ontology/gist/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

gist:Organization        a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Person              a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Content             a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Event               a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GeoRegion           a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GovernedGeoRegion   a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:OrderedCollection   a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GeoRoute            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Category            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Magnitude           a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Aspect              a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:UnitOfMeasure       a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Building            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:isCategorizedBy     a owl:ObjectProperty .
gist:hasMagnitude        a owl:ObjectProperty .
gist:hasAspect           a owl:ObjectProperty .
gist:hasUnitOfMeasure    a owl:ObjectProperty .
gist:numericValue        a owl:DatatypeProperty .

bs:Person        a owl:Class ; rdfs:subClassOf gist:Person .
bs:Bookshop      a owl:Class ; rdfs:subClassOf gist:Organization ;
    skos:editorialNote "Under Organization, not Building: the shop is the business. gist declares the two disjoint, so a shop that is both would be a contradiction."@en .
bs:Publisher     a owl:Class ; rdfs:subClassOf gist:Organization .
bs:Work          a owl:Class ; rdfs:subClassOf gist:Content ;
    skos:editorialNote "gist:Content's example is a literary work independent of edition. bs:Work is also an edition (o10); the compromise stands."@en .
bs:Event         a owl:Class ; rdfs:subClassOf gist:Event .
bs:Series        a owl:Class ; rdfs:subClassOf gist:OrderedCollection .
bs:Place         a owl:Class ; rdfs:subClassOf gist:GovernedGeoRegion ;
    skos:editorialNote "Every Trail place is governed by the council area or country it is within."@en .
bs:StockRecord   a owl:Class ;
    skos:editorialNote "Not placed. Neither gist:Magnitude, gist:Offer nor gist:Collection is true of every stock record."@en .
bs:TrailSegment  a owl:Class ;
    skos:editorialNote "Not placed. gist:GeoRoute is an ordered set of points; a segment is two shops and a length."@en .

skos:Concept  a owl:Class .
bt:genre-crime-fiction  a skos:Concept, gist:Category ; skos:prefLabel "Crime Fiction"@en .

bt:shop-inkwell  a bs:Bookshop ; skos:prefLabel "The Inkwell"@en .

Why

Alignment is placing each of your classes under the most specific class of the upper ontology that is true of every instance, and then writing down the argument. gistBFO's README states the standard well: a mapping that puts everything under the top class is not an interesting mapping, so aim low and be able to say why. The bridge for the Trail comes to seven subclass axioms, one typing, and two refusals.

Most of them are easy enough. bs:Person goes under gist:Person, whose definition, a human being who was or is alive, is the Trail's own; bs:Publisher and bs:Bookshop go under gist:Organization, a structured entity formed to achieve goals; bs:Event under gist:Event and bs:Series under gist:OrderedCollection; and the genres, being particulars, are gist:Category, which gist defines as a concept used to categorise without formal semantics, which is the very call o24 made. Some of them take an argument, though. bs:Bookshop is the business rather than the premises, so it goes under Organization and not Building, the identity criterion from o10 deciding it, and gist's disjointness would object if it went under both; bs:Work is a work and an edition at once, and gist's own example for Content is Huckleberry Finn independent of any edition, so bs:Work goes under gist:Content with the compromise noted; and places go under gist:GovernedGeoRegion, since every one of them is governed by exactly the council or country it lies within.

Two are refused outright. bs:StockRecord, being a count of copies at a price, is not a Magnitude, an Offer or a Collection in gist's sense, and a forced placement would be worse than none at all; and bs:TrailSegment comes close to gist:GeoRoute without quite fitting, a route being an ordered set of points where a segment is a pair of shops with a length. Both refusals are noted, in the way the SemOps ledger marks a gap grey rather than painting it green, and HermiT, with gistCore imported, finds the bridge consistent.

Diagram

   Trail                    gist                          why
   bs:Person                gist:Person                   same definition
   bs:Bookshop              gist:Organization             the business, not the building (identity, o10)
   bs:Publisher             gist:Organization
   bs:Work                  gist:Content                  gist's own example: a literary work, independent of edition
   bs:Event                 gist:Event
   bs:Series                gist:OrderedCollection
   bs:Place                 gist:GovernedGeoRegion        each is governed by the area or country it is within
   bt:genre-*               gist:Category  (individuals)  a concept used to categorise, no formal semantics
   bs:StockRecord           --                            nothing fits; noted, not forced
   bs:TrailSegment          --                            GeoRoute is an ordered set of points; noted

   measured: consistent, with gistCore imported

What to take away

  • Place each class under the most specific upper class that is true of every instance, and write the argument on the axiom itself.
  • Identity decides the hard ones, since a bookshop is the business and the building is a different referent altogether.
  • Refuse a placement rather than force one, because a gap written down is a decision where a wrong placement is a contradiction waiting for data.

Try it

Add 'bs:Bookshop rdfs:subClassOf gist:Building .' and run the checker: gist declares Organization and Building disjoint, so bs:Bookshop becomes unsatisfiable, and The Inkwell makes the whole graph inconsistent. Identity, enforced by someone else's axiom.

Measured

Parses62 triples; 23 classes; 4 object properties; 1 datatype property; 4 annotation properties; 1 concept; 2 individuals (with the data: 2352 triples)
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 825 inferred
  • gist:isTriggeredBy skos:definition "Relates a contingency, such as an event or obligation, to the event that gives rise to it."
  • gist:RenderedContent skos:prefLabel "Rendered Content"
  • gist:rangeIncludes skos:scopeNote "This property is used to guide the ontology user; like all annotation properties, it does not play a role in inference."
  • gist:GeoVolume skos:example "An oil reservoir, the body of a lake, or an airspace."
  • gist:hasAddress skos:prefLabel "has address"
  • gist:hasBroader skos:prefLabel "has broader"
  • gist:SchemaMetaData skos:definition "Superclass for all types of metadata."
  • gist:goesToAgent skos:prefLabel "goes to agent"
  • gist:isBasedOn skos:definition "The object is a foundation for, a starting point for, gave rise to, or justifies the subject."
  • gist:actualStartYear skos:prefLabel "actual start year"
and 815 more
Query: The bridge, and what each placement says7 rows
  • bs:Bookshop gist:Organization "Under Organization, not Building: the shop is the business. gist declares the two disjoint, so a shop that is both would be a contradiction."
  • bs:Event gist:Event
  • bs:Person gist:Person
  • bs:Place gist:GovernedGeoRegion "Every Trail place is governed by the council area or country it is within."
  • bs:Publisher gist:Organization
  • bs:Series gist:OrderedCollection
  • bs:Work gist:Content "gist:Content's example is a literary work independent of edition. bs:Work is also an edition (o10); the compromise stands."

Afterwards, in the SPARQL panel

The bridge, and what each placement says (on the model)
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?trail ?upper ?why WHERE {
  ?trail rdfs:subClassOf ?upper .
  FILTER(STRSTARTS(STR(?trail), "https://example.org/bookshop-trail/schema#") && STRSTARTS(STR(?upper), "https://w3id.org/semanticarts/"))
  OPTIONAL { ?trail skos:editorialNote ?why }
} ORDER BY ?trail

Sources

O60

gist's patterns, and when to keep your own

Floor area is modelled gist's way, as a Magnitude with an Aspect and a Unit, beside the Trail's way, a datatype property with the unit in its name; the Trail keeps the simple form and records what it gives up.

Corner: referent · Runs in the browser

@prefix bt:    <https://example.org/bookshop-trail/> .
@prefix bs:    <https://example.org/bookshop-trail/schema#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:   <http://www.w3.org/2002/07/owl#> .
@prefix xsd:   <http://www.w3.org/2001/XMLSchema#> .
@prefix skos:  <http://www.w3.org/2004/02/skos/core#> .
@prefix gist:  <https://w3id.org/semanticarts/ns/ontology/gist/> .
@prefix gistd: <https://w3id.org/semanticarts/ns/data/gist/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

gist:Organization        a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Person              a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Content             a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Event               a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GeoRegion           a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GovernedGeoRegion   a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:OrderedCollection   a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GeoRoute            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Category            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Magnitude           a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Aspect              a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:UnitOfMeasure       a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Building            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:isCategorizedBy     a owl:ObjectProperty .
gist:hasMagnitude        a owl:ObjectProperty .
gist:hasAspect           a owl:ObjectProperty .
gist:hasUnitOfMeasure    a owl:ObjectProperty .
gist:numericValue        a owl:DatatypeProperty .

bs:Bookshop  a owl:Class ; rdfs:subClassOf gist:Organization .

# The Trail's way.
bs:floorArea  a owl:DatatypeProperty ; rdfs:domain bs:Bookshop ; rdfs:range xsd:decimal ;
    skos:definition "Selling floor in square metres."@en ;
    skos:editorialNote "Compromise: the unit is in the definition, not in the data. gist's Magnitude pattern is the alternative; adopt it if a second unit is ever needed."@en .
bt:shop-inkwell  a bs:Bookshop ; bs:floorArea 140.0 .

# gist's way, for the same fact.
gistd:_Aspect_area     a gist:Aspect ;        skos:prefLabel "area"@en .
bt:unit-square-metre   a gist:UnitOfMeasure ; skos:prefLabel "square metre"@en .
bt:mag-inkwell-area    a gist:Magnitude ;
    gist:numericValue      140.0 ;
    gist:hasAspect         gistd:_Aspect_area ;
    gist:hasUnitOfMeasure  bt:unit-square-metre .
bt:shop-inkwell  gist:hasMagnitude bt:mag-inkwell-area .

# The cheap pattern: a controlled list as categories.
bs:Event  a owl:Class ; rdfs:subClassOf gist:Event .
bt:kind-reading  a gist:Category ; skos:prefLabel "Reading"@en .
bt:event-1       a bs:Event ; gist:isCategorizedBy bt:kind-reading .

Why

gist has a pattern for measured quantities that is worth understanding even where you do not adopt it. A gist:Magnitude is a node with a numeric value, a gist:Aspect saying what was measured, area in this case, and a gist:UnitOfMeasure saying in what, square metres, so that every shop's floor area becomes four triples where it was one. In return the unit is data rather than a hint buried in a property name, a query can convert between units, and a floor area and a plot area become the same aspect measured on different things; gist uses the same pattern for money, durations and probabilities, and defines several classes by it, since a GeoRegion is a location with a magnitude whose aspect is area.

The Trail says bs:floorArea 140.0 and puts 'square metres' in the definition, which is the pragmatic form: one triple, a column in the spreadsheet, and a number a query can compare. It gives up the unit as data and the possibility of a second unit, and o30's rule applies, that a departure changing only convenience is allowed so long as the note says what was traded away. On the day the website needs square feet, that note is the migration plan.

gist:isCategorizedBy is the other pattern the Trail could borrow, since the event kinds of o25 are gist:Category instances and an event isCategorizedBy one of them. That costs nothing at all and gains a shared property, and the shipped vocabulary should probably adopt it.

Diagram

   the Trail's way                        gist's way

   bt:shop-inkwell bs:floorArea 140.0     bt:shop-inkwell gist:hasMagnitude bt:mag-inkwell-area .
   (unit: in the definition)              bt:mag-inkwell-area a gist:Magnitude ;
                                              gist:numericValue 140.0 ;
                                              gist:hasAspect gistd:_Aspect_area ;
                                              gist:hasUnitOfMeasure bt:unit-square-metre .

   one triple, unit as a hint             four triples, unit as data, convertible, shareable
   the note records the trade

What to take away

  • gist's Magnitude pattern makes the unit into data, at a cost of three triples a value, and it pays whenever units vary or values are shared.
  • A datatype property with the unit in its definition is the pragmatic form, so record what it gives up.
  • Borrow the cheap patterns wholesale, since gist:isCategorizedBy for a controlled list costs nothing.

Measured

Parses59 triples; 15 classes; 4 object properties; 2 datatype properties; 4 annotation properties; 6 individuals (with the data: 2348 triples)
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 826 inferred
  • gist:isTriggeredBy skos:definition "Relates a contingency, such as an event or obligation, to the event that gives rise to it."
  • gist:RenderedContent skos:prefLabel "Rendered Content"
  • gist:rangeIncludes skos:scopeNote "This property is used to guide the ontology user; like all annotation properties, it does not play a role in inference."
  • gist:GeoVolume skos:example "An oil reservoir, the body of a lake, or an airspace."
  • gist:hasAddress skos:prefLabel "has address"
  • gist:hasBroader skos:prefLabel "has broader"
  • gist:SchemaMetaData skos:definition "Superclass for all types of metadata."
  • gist:goesToAgent skos:prefLabel "goes to agent"
  • gist:isBasedOn skos:definition "The object is a foundation for, a starting point for, gave rise to, or justifies the subject."
  • gist:NetworkLink skos:scopeNote "Each network link is connected to a network node via the property gist:links or one of its subproperties."
and 816 more

Sources

O61

gistBFO: the same classes under a foundational ontology

The Trail reaches BFO through gist's published bridge, reads each mapping note as a worked example of alignment reasoning, and reasons over the three ontologies together.

Corner: referent · Runs in the browser; the four-ontology consistency check is HermiT through ROBOT; gistBFO and BFO are fetched by scripts/setup-tools.ps1

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix gist: <https://w3id.org/semanticarts/ns/ontology/gist/> .
@prefix obo:  <http://purl.obolibrary.org/obo/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

gist:Organization        a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Person              a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Content             a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Event               a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GeoRegion           a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GovernedGeoRegion   a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:OrderedCollection   a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:GeoRoute            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Category            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Magnitude           a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Aspect              a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:UnitOfMeasure       a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:Building            a owl:Class ; rdfs:isDefinedBy <https://w3id.org/semanticarts/ontology/gistCore> .
gist:isCategorizedBy     a owl:ObjectProperty .
gist:hasMagnitude        a owl:ObjectProperty .
gist:hasAspect           a owl:ObjectProperty .
gist:hasUnitOfMeasure    a owl:ObjectProperty .
gist:numericValue        a owl:DatatypeProperty .

obo:BFO_0000001  a owl:Class ; skos:prefLabel "entity"@en .
obo:BFO_0000002  a owl:Class ; skos:prefLabel "continuant"@en .
obo:BFO_0000003  a owl:Class ; skos:prefLabel "occurrent"@en .
obo:BFO_0000015  a owl:Class ; skos:prefLabel "process"@en .
obo:BFO_0000027  a owl:Class ; skos:prefLabel "object aggregate"@en .
obo:BFO_0000030  a owl:Class ; skos:prefLabel "object"@en .
obo:BFO_0000031  a owl:Class ; skos:prefLabel "generically dependent continuant"@en .
obo:BFO_0000040  a owl:Class ; skos:prefLabel "material entity"@en .

bs:Bookshop  a owl:Class ; rdfs:subClassOf gist:Organization .
bs:Work      a owl:Class ; rdfs:subClassOf gist:Content .
bs:Event     a owl:Class ; rdfs:subClassOf gist:Event .
bs:Person    a owl:Class ; rdfs:subClassOf gist:Person .

# The gistBFO axioms these inherit, restated here with labels so the file
# reads on its own; the checker imports the real gistBFO and BFO.
gist:Organization  rdfs:subClassOf obo:BFO_0000027 .
gist:Content       rdfs:subClassOf obo:BFO_0000031 .
gist:Category      rdfs:subClassOf obo:BFO_0000031 .
gist:Event         rdfs:subClassOf [ a owl:Class ; owl:unionOf ( obo:BFO_0000031 obo:BFO_0000015 ) ] .

bt:shop-inkwell  a bs:Bookshop .

Why

BFO is a foundational ontology of quite a different temperament: thirty-six classes, every IRI a number, and a philosophy, in which everything is either a continuant, which persists through time, or an occurrent, which unfolds in it, and a continuant is either independent, a material object say, or dependent on one, being a quality or a piece of information. The OBO Foundry builds its biomedical ontologies on it and a great deal of careful thought has gone into it. gist chose everyday names and no philosophy at all, and gistBFO is the published bridge between the two, thirty subclass axioms from gist classes into their most specific BFO superclass, nineteen of them carrying a mapping note that says why.

The notes are the useful part, because they show the reasoning that an alignment needs. gist:Organization goes under object aggregate, partly in order to agree with the Common Core Ontologies; gist:Content goes under generically dependent continuant, since a work is information that any copy can carry; gist:Category goes under the same, with a note explaining why not a quality, a category being a bucket that things fall into rather than a redness that a car has. And then there is gist:Event, which BFO splits, since a scheduled event is a plan, a generically dependent continuant, while one that has happened is a process, so gist:Event goes under the union of the two, which is a mapping that admits to being two things.

The Trail inherits the whole bridge, so that bs:Bookshop under gist:Organization is under object aggregate, bs:Work under gist:Content is a generically dependent continuant, and bs:Event is whatever gist:Event is. HermiT, given the Trail's bridge with gistCore, gistBFO and BFO 2020 core together, reports the four of them consistent in a few seconds, which is the measured line. Whether the Trail wants BFO's commitments is a separate question, and the plain answer is that a bookshop website does not; the value here lies in seeing an alignment done well.

Diagram

   Trail             gist                  BFO (gistBFO)                                   note
   bs:Bookshop  ->   gist:Organization ->  obo:BFO_0000027  object aggregate               partly to agree with CCO
   bs:Work      ->   gist:Content      ->  obo:BFO_0000031  generically dependent continuant  carried by any copy
   bt:genre-*   ->   gist:Category     ->  obo:BFO_0000031                                 a bucket, not a quality
   bs:Event     ->   gist:Event        ->  GDC or process                                  a plan, or a happening
   bs:Person    ->   gist:Person       ->  (via LivingThing) material entity

   measured: the Trail + gistCore + gistBFO + BFO 2020 core, consistent under HermiT

What to take away

  • A foundational ontology brings a philosophy with it, so read the bridge's notes to see what each placement commits you to.
  • The hard mappings are the candid ones, like gist:Event under a union, because in BFO's terms it really is two kinds of thing.
  • Inheriting a bridge is cheap and reasoning over it is quick, and deciding whether you want its commitments is the real work.

Measured

Parses70 triples; 25 classes; 4 object properties; 1 datatype property; 4 annotation properties; 1 individual (with the data: 3412 triples)
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 900 inferred
  • gist:isTriggeredBy skos:definition "Relates a contingency, such as an event or obligation, to the event that gives rise to it."
  • gist:RenderedContent skos:prefLabel "Rendered Content"
  • gist:rangeIncludes skos:scopeNote "This property is used to guide the ontology user; like all annotation properties, it does not play a role in inference."
  • gist:GeoVolume skos:example "An oil reservoir, the body of a lake, or an airspace."
  • gist:hasAddress skos:prefLabel "has address"
  • gistd:_Aspect_mass rdf:type obo:BFO_0000001
  • gist:hasBroader skos:prefLabel "has broader"
  • gist:SchemaMetaData skos:definition "Superclass for all types of metadata."
  • gist:goesToAgent skos:prefLabel "goes to agent"
  • gist:isBasedOn skos:definition "The object is a foundation for, a starting point for, gave rise to, or justifies the subject."
and 890 more
OWL 2 RL closure (owlrl; what Show Facts computes)+867 triples about the Trail's terms
  • gist:ContingentObligation rdfs:subClassOf gist:Commitment
  • obo:BFO_0000199 rdfs:range obo:BFO_0000003
  • obo:BFO_0000194 rdfs:range obo:BFO_0000002
  • gist:hasPhysicalLocation rdfs:range obo:BFO_0000140
  • obo:BFO_0000222 rdfs:range obo:BFO_0000001
  • gist:Text rdfs:subClassOf obo:BFO_0000031
  • gist:IntergovernmentalOrganization rdfs:subClassOf obo:BFO_0000040
  • gist:isAllocatedBy skos:scopeNote gist:ID
  • gist:IntergovernmentalOrganization rdfs:subClassOf obo:BFO_0000004
  • gist:hasAspect rdfs:range obo:BFO_0000016
and 857 more
Query: What The Inkwell is, all the way up32 rows
  • bs:Bookshop
  • gist:Organization
  • _:n6bdba
  • _:n6bdba
  • _:n6bdba
  • _:n6bdba
  • owl:Thing
  • _:n6bdba

Afterwards, in the SPARQL panel

What The Inkwell is, all the way up (on the reasoned)
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?type WHERE { bt:shop-inkwell a ?type }

Sources

O62

Two alignments, one ontology: gistUFO beside gistBFO

The Trail's classes are typed the gUFO way, as gistUFO types gist's, and set beside their BFO placements; where the two published alignments disagree about gist:Event, the disagreement is kept as instruction.

Corner: referent · Runs in the browser; consistency with gUFO and gist imported is HermiT through ROBOT

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix gist: <https://w3id.org/semanticarts/ns/ontology/gist/> .
@prefix gufo: <http://purl.org/nemo/gufo#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
rdfs:isDefinedBy    a owl:AnnotationProperty .

# gUFO's types, declared so the file stands alone; the checker imports gUFO.
gufo:Kind     a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .
gufo:SubKind  a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .
gufo:Role     a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .
gufo:EventType a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .
gufo:AbstractIndividualType a owl:Class ; rdfs:isDefinedBy <http://purl.org/nemo/gufo> .
gist:Organization  a owl:Class .  gist:Person a owl:Class .  gist:Event a owl:Class .  gist:Category a owl:Class .

bs:Person       a owl:Class, gufo:Kind ;      rdfs:subClassOf gist:Person .
bs:Bookshop     a owl:Class, gufo:Kind ;      rdfs:subClassOf gist:Organization .
bs:Settlement   a owl:Class, gufo:SubKind ;   rdfs:subClassOf bs:Place .
bs:Place        a owl:Class, gufo:Kind .
bs:Author       a owl:Class, gufo:Role ;      rdfs:subClassOf bs:Person .
bs:Event        a owl:Class, gufo:EventType ; rdfs:subClassOf gist:Event .
bt:genre-crime-fiction  a gist:Category ;
    skos:editorialNote "gistUFO types gist:Category as a gufo:AbstractIndividualType, and gistBFO places it under a generically dependent continuant."@en .

Why

gistUFO is the second published alignment of gist, this time to gUFO, and it works rather differently from gistBFO, in that it does not put gist classes under gUFO classes so much as type them: gist:Person is a gufo:Kind, gist:Offer a gufo:Phase, gist:ContractTerm a gufo:Role, and gist:Category a gufo:AbstractIndividualType. That is module 02's analysis applied by other people to a real ontology and published for the rest of us, and o14 used it. A second file then places gist classes under gUFO's individuals hierarchy, with gist:Person under gufo:FunctionalComplex, gist:Agreement under gufo:Relator and gist:Event under gufo:Event.

Set the two alignments side by side and they agree on most things and disagree on one, instructively. gistBFO says that a gist:Event is a generically dependent continuant or a process, because a scheduled event is a plan; gistUFO says that a gist:Event is a gufo:Event, a perdurant, something that happens, and types gist:ScheduledEvent as an intrinsic mode of whatever is doing the planning. Neither of them is wrong, and they draw the line between the plan and the happening in different places, so that an ontology importing both would have to choose. An alignment is exactly that, a set of decisions each with a note attached, and two sets of decisions made by careful people will not always coincide.

The Trail types its classes the gUFO way here, with gUFO imported, and the profile checker and HermiT both accept the punning; the query at the end lists every class with its gUFO type and, through gist, its BFO superclass, which is about as much foundational commitment as a bookshop trail could want.

Diagram

   class            gUFO (gistUFO)                     BFO (gistBFO)                          agree?
   gist:Person      Kind; FunctionalComplex            material entity, via LivingThing       yes
   gist:Organization Kind; FunctionalComplex           object aggregate                       yes
   gist:Content     Category; Object                   generically dependent continuant       roughly
   gist:Category    AbstractIndividualType; QualityValue  generically dependent continuant    roughly
   gist:Event       EventType; gufo:Event (a perdurant) GDC or process                        no: the plan vs the happening
   gist:Offer       Phase (of a Commitment)            (a commitment: relational quality)     different questions

   an alignment is a set of decisions with notes; two careful sets will not always coincide

What to take away

  • gistUFO types classes by what kind of class they are and gistBFO places them under what kind of thing they are, and both of those are alignment.
  • Two published alignments of one ontology disagree about events, so keep the disagreement, because it shows where the line between plan and happening has been drawn.
  • The Trail can carry both typings and the checker accepts it, though whether to adopt either is a scope decision.

Measured

Parses37 triples; 15 classes; 4 annotation properties; 1 individual (with the data: 3086 triples)
Profile (ROBOT)OWL 2 DL: in profile
HermiTconsistent; 852 inferred
  • gist:isTriggeredBy skos:definition "Relates a contingency, such as an event or obligation, to the event that gives rise to it."
  • gist:RenderedContent skos:prefLabel "Rendered Content"
  • gist:rangeIncludes skos:scopeNote "This property is used to guide the ontology user; like all annotation properties, it does not play a role in inference."
  • gist:GeoVolume skos:example "An oil reservoir, the body of a lake, or an airspace."
  • gist:hasAddress skos:prefLabel "has address"
  • gist:hasBroader skos:prefLabel "has broader"
  • gist:SchemaMetaData skos:definition "Superclass for all types of metadata."
  • gist:goesToAgent skos:prefLabel "goes to agent"
  • gist:isBasedOn skos:definition "The object is a foundation for, a starting point for, gave rise to, or justifies the subject."
  • gist:NetworkLink skos:scopeNote "Each network link is connected to a network node via the property gist:links or one of its subproperties."
and 842 more
Query: Every class, its gUFO type, and its gist parent6 rows
  • bs:Author gufo:Role
  • bs:Bookshop gufo:Kind gist:Organization
  • bs:Event gufo:EventType gist:Event
  • bs:Person gufo:Kind gist:Person
  • bs:Place gufo:Kind
  • bs:Settlement gufo:SubKind

Afterwards, in the SPARQL panel

Every class, its gUFO type, and its gist parent (on the model)
PREFIX gufo: <http://purl.org/nemo/gufo#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?class ?gufoType ?gistParent WHERE {
  ?class a ?gufoType . FILTER(STRSTARTS(STR(?gufoType), STR(gufo:)))
  OPTIONAL { ?class rdfs:subClassOf ?gistParent . FILTER(STRSTARTS(STR(?gistParent), "https://w3id.org/semanticarts/")) }
} ORDER BY ?class

Sources

Module 10

Quality

Measuring what has been built, and knowing what the measurements cannot see: OQuaRE's metrics scored on the Trail and on gist, the Ontology Quality Suite's checks read by identifier, style shapes like the ones gist runs against itself, and competency questions used as tests. Then the limits of all that, since an ontology can score five out of five and still be wrong, and no metric sees rigidity.

Sources
O63

OQuaRE in a page

Quality is assessed with a published model rather than a feeling: OQuaRE's characteristics, sub-characteristics and metrics, each metric scored one to five against the paper's thresholds, and the scores read as a map of strengths rather than a mark.

Corner: thought · Data: 01-vocabulary.ttl · Runs in the browser; the scores come from scripts/metrics.py on data/01-vocabulary.ttl

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Assessed with OQuaRE (Duque-Ramos et al., 2011). Scores are in the course document beside this lesson; the script that computed them is scripts/metrics.py."@en .

Why

OQuaRE takes the software quality standard SQuaRE, ISO/IEC 25000, and adapts it to ontologies, in three layers. At the top are the characteristics, which are structural, functional adequacy, reliability, operability, maintainability, compatibility, transferability and quality in use. Under each of those come sub- characteristics, so that structural has cohesion, tangledness, redundancy and consistency, maintainability has modularity, reusability, analysability and changeability, and functional adequacy has the intended uses of an ontology, from controlled vocabulary through to inferencing. And at the bottom come the metrics, fourteen of them, most adapted from object-oriented software metrics: depth of inheritance, number of children, coupling, cohesion, and the richness of attributes, relations and annotations.

Each metric is scored from one to five, where one is not acceptable, three is minimally acceptable and five exceeds requirements, against thresholds the paper proposes; a sub- characteristic's score is the mean of its metrics and a characteristic's is the mean of its sub-characteristics. The authors are careful to say that the result is meant to show strengths and weaknesses rather than to rank ontologies, and that the thresholds are proposals drawn from software practice and awaiting validation, and both of those cautions are worth keeping in mind.

The metrics are computed in this course by scripts/metrics.py, which follows the paper's formulas and states its own choices wherever the paper is silent. The measured line has the Trail's scores, o64 and o65 read them, and o68 says what they miss.

Diagram

   SQuaRE (ISO/IEC 25000)  ->  OQuaRE

   characteristic      sub-characteristics                         metrics (examples)
   structural          formalisation, cohesion, tangledness, ...   LCOMOnto, TMOnto, DITOnto
   functional adequacy controlled vocabulary, reasoning, ...       RROnto, AROnto, INROnto, ANOnto, CROnto
   maintainability     modularity, reusability, analysability, ... WMCOnto, DITOnto, NOCOnto, RFCOnto, NOMOnto, CBOOnto
   reliability, operability, compatibility, transferability, quality in use

   score 1..5 per metric, by threshold; means upward; strengths and weaknesses, not a rank

What to take away

  • A quality model comes in three layers, characteristics and sub-characteristics and metrics, and the metrics are the only part a script can compute.
  • Scores run from one to five against proposed thresholds, so read them as a map rather than as a mark.
  • The authors' own caveat is that the thresholds came from software practice and are awaiting validation, and it is worth keeping.

Measured

Parses6 triples; 3 annotation properties (with the data: 513 triples)
OQuaRE (scripts/metrics.py)17 classes, 25 object properties, 40 datatype properties, 11 roots, 14 leaves
MetricValueScore 1 to 5
LCOMOnto1.4295
WMCOnto3.2945
DITOnto25
NACOnto0.4295
NOCOnto15
CBOOnto15
RFCOnto6.1673
NOMOnto1.8245
RROnto74%4
AROnto182%5
INROnto147%5
CROnto0%1
ANOnto200%5
TMOnto15

Sources

O64

Structure: depth, breadth, tangledness, cohesion

The Trail's hierarchy is two deep, one wide on average, untangled and cohesive, which the structural metrics confirm; and gist, measured the same way, shows sixty-two roots because its structure lives in definitions the metrics do not read.

Corner: referent · Data: 01-vocabulary.ttl · Runs in the browser; the metrics come from scripts/metrics.py; the gist column from the same script on gistCore.ttl

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Structural metrics: see the MEASURED line, computed over the shipped vocabulary. gist's, for comparison, are in this lesson's diagram."@en .

Why

Five metrics describe the shape of the class tree. DITOnto is the longest path from the root to a leaf, and the Trail's is 2, Place and then Settlement, which scores 5, where gist's is 3; NOCOnto is the mean number of children per non-root class, which is 1 for both; NACOnto is the mean number of ancestors per leaf, under half for both, because most classes are roots; TMOnto is the mean number of parents per class that has one, which is 1 for both, meaning no multiple inheritance anywhere, and o32 said why; and LCOMOnto, the paper's cohesion measure, is the mean path length from leaf to root, about 1.4 for both.

The number that stands out is gist's sixty-two root classes out of ninety-six. gist is not flat, it is defined: gist:Contract is an Agreement that isUnderJurisdictionOf some GovernmentOrganization, said with owl:equivalentClass, and a reasoner places it under Agreement, while the metric reads rdfs:subClassOf and sees a root. Run the metrics over the reasoned ontology and the tree appears; run them over the asserted one and gist looks like a list. That is the first thing to know about structural metrics, that they measure what was written, so that an ontology which lets the reasoner do the tangling, as o32 and o55 describe, will score as though it had no structure at all.

The scores themselves are on the measured line for the Trail and in the diagram for both of them, and on structure both ontologies score 5 on nearly everything, which says less than it appears to, since a small shallow single-parented hierarchy is the easy case and OQuaRE's thresholds were set for ontologies with thousands of classes in them.

Diagram

   metric      what                                  Trail        gist        threshold for 5
   DITOnto     longest root-to-leaf path             2  (5)       3  (4)      at most 2
   NOCOnto     mean children per non-root class      1  (5)       1  (5)      at most 3
   NACOnto     mean ancestors per leaf               0.43 (5)     0.46 (5)    at most 2
   TMOnto      mean parents per class with a parent  1  (5)       1  (5)      at most 2
   LCOMOnto    mean leaf-to-root path length         1.43 (5)     1.46 (5)    at most 2

   gist: 62 roots of 96 classes, asserted. Reasoned, most of them have a parent.
   the metrics read rdfs:subClassOf; a defined hierarchy is invisible to them

What to take away

  • Depth, breadth, ancestors, tangledness and cohesion give you five numbers, and for a small ontology all five of them come out well.
  • Structural metrics read what is asserted, so an ontology that defines its classes and lets the reasoner classify will look flat to them.
  • Measure the reasoned ontology as well as the written one wherever the structure lives in the definitions.

Measured

Parses6 triples; 3 annotation properties (with the data: 513 triples)
OQuaRE (scripts/metrics.py)17 classes, 25 object properties, 40 datatype properties, 11 roots, 14 leaves
MetricValueScore 1 to 5
LCOMOnto1.4295
WMCOnto3.2945
DITOnto25
NACOnto0.4295
NOCOnto15
CBOOnto15
RFCOnto6.1673
NOMOnto1.8245
RROnto74%4
AROnto182%5
INROnto147%5
CROnto0%1
ANOnto200%5
TMOnto15
Query: Depth of every class in the shipped vocabulary17 rows
  • bs:Author "1"
  • bs:CouncilArea "1"
  • bs:Country "1"
  • bs:Region "1"
  • bs:Settlement "1"
  • bs:Translation "1"
  • bs:Bookshop "0"
  • bs:DataIssue "0"

Afterwards, in the SPARQL panel

Depth of every class in the shipped vocabulary (on the data)
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?class (COUNT(?up) AS ?depth) WHERE {
  ?class a owl:Class . FILTER(STRSTARTS(STR(?class), "https://example.org/bookshop-trail/schema#"))
  OPTIONAL { ?class rdfs:subClassOf+ ?up . FILTER(isIRI(?up)) }
} GROUP BY ?class ORDER BY DESC(?depth) ?class

Sources

O65

Richness and coupling: what the metrics count, and what a style choice does to them

The richness metrics are read with their small print: they count properties by declared domain, so an ontology that keeps domains sparse on principle scores low on richness, and gist does.

Corner: referent · Runs in the browser; the gist metrics on the MEASURED line come from scripts/metrics.py over gistCore.ttl

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Richness and coupling metrics: MEASURED line for the Trail; gist in the diagram. Properties are assigned to classes by rdfs:domain, each member of a union counted; that is scripts/metrics.py's reading of a formula the paper leaves open."@en .

Why

AROnto is attributes per class, INROnto is relationships per class, RROnto is the share of properties that are attributes, ANOnto is annotations per class, and CROnto is individuals per class. The paper does not say how a property is to be assigned to a class, and the only workable reading is by rdfs:domain, so that a property belongs to the class it is declared on; scripts/metrics.py does that, counting each member of a union domain.

The consequence is laid out in the diagram. The Trail declares a domain on nearly every property and so scores 5 on attribute and relationship richness, while gist declares domains sparingly as a matter of principle, set out in its README and in o31, and so scores 1 and 2 on the same metrics with sixty-six object properties and fifty datatype properties sitting in the file. Nothing about gist is the poorer for it; a design choice the metric cannot see has simply been counted as an absence. That is the general lesson about richness metrics, and it applies to CROnto as well, which scores every pure vocabulary 1 because a schema has no individuals in it.

ANOnto is the one to watch, since it measures the thing most ontologies neglect. The Trail has a label and a comment on every class and so scores 5, while gist has a preferred label, a definition and often an example and a scope note besides, and scores higher still. The coupling metrics, CBOOnto and RFCOnto and WMCOnto and NOMOnto, are the software ones with the least ontological content in them, adding up superclasses and properties per class, and for both ontologies they come out low, which the thresholds read as good.

Diagram

   metric      what                                    Trail         gist         note
   AROnto      datatype properties per class           1.82 (5)      0.31 (2)     by declared domain
   INROnto     object properties per class             1.47 (5)      0.12 (1)     gist keeps domains sparse, on purpose
   RROnto      attributes among all properties         74% (4)       28% (2)
   ANOnto      annotations per class                   2.0  (5)      3.6  (5)     the one that measures care
   CROnto      individuals per class                   0    (1)      0.08 (1)     a schema has no individuals
   RFCOnto     properties + superclasses per class     6.2  (3)      1.9  (5)
   WMCOnto     properties and relationships per class  3.3  (5)      0.4  (5)

   a metric counts what it can see; a principle it cannot see is counted as an absence

What to take away

  • Richness metrics assign properties to classes by domain, so read their small print before you read their scores.
  • gist scores low on richness because it keeps its domains sparse on principle, which means the metric measured the principle rather than the quality.
  • ANOnto is the metric that tracks care, and it is the one to put in a gate.

Measured

Parses6 triples; 3 annotation properties (with the data: 2314 triples)
OQuaRE (scripts/metrics.py)96 classes, 66 object properties, 50 datatype properties, 62 roots, 81 leaves
MetricValueScore 1 to 5
LCOMOnto1.4575
WMCOnto0.4275
DITOnto34
NACOnto0.4575
NOCOnto15
CBOOnto15
RFCOnto1.8825
NOMOnto0.3125
RROnto28%2
AROnto31%2
INROnto12%1
CROnto8%1
ANOnto364%5
TMOnto15

Sources

O66

The checks, by identifier

The shipped vocabulary goes through the Ontology Quality Suite scoped to its own namespace, and the findings are read by identifier: a check that fires on every term is a policy not adopted, not a hundred mistakes.

Corner: thought · Data: 01-vocabulary.ttl · Runs in the browser; the identifiers on the MEASURED line come from the Ontology Quality Suite: pip install ontology-quality-suite

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Checked with the Ontology Quality Suite, scoped to this namespace. QUA-009 and QUA-010 fire on most terms because the vocabulary uses rdfs:label and rdfs:comment; adopting SKOS annotations is one pass of work and is recorded as a decision to take."@en .

Why

The Ontology Quality Suite runs sixty-one checks across ten categories, each with an identifier, a severity, a message and a remediation: STR for structure, LOG for logic, QUA for documentation and metadata, STY for naming style, REA for whatever a reasoner finds, and so on. The SemOps Manual's chapter 9 is about running it as a gate, and its two rules are the ones o34 gave, which are to scope the findings to your own namespace and to assert on identifiers.

Run over the shipped vocabulary with --own-namespace set to bs:, it reports the small set of identifiers on the measured line, of which two fire on nearly every term: QUA-009, no skos:prefLabel per language, and QUA-010, no skos:definition. The Trail uses rdfs:label and rdfs:comment, the very pair gist's guide says not to use, so from the registry's point of view it has no labels and no definitions at all. That is the manual's point exactly, that a check which fires on every term is telling you about a policy you have not adopted: either adopt SKOS annotations, which is one pass of work, or drop the two checks from the registry with a note saying why. Either of those is a decision, whereas leaving a hundred warnings in the report is not.

The other findings are worth reading one at a time, with their remediation text, which is where the suite's knowledge actually lives. STR-003, a property with neither domain nor range, fires on bs:statedOn, whose subject is a reifier and whose value is a date the datatype map cannot hold, so that o31 and o50 left both of them open deliberately; the right response is an editorial note the check can be pointed at rather than a domain. A gate reports, and a person decides.

Diagram

   python -m ontology_suite checks --ontology data/01-vocabulary.ttl --own-namespace https://example.org/bookshop-trail/schema# --engine sparql

   identifier   fires on               reading
   QUA-009      nearly every term      a policy not adopted: skos:prefLabel per language
   QUA-010      nearly every term      the same: skos:definition
   STR-003      bs:statedOn            no domain or range, on purpose (o31, o50); note it, do not fix it
   STR-004      every class            no formal definition; most kinds are primitive, and should be (o46)

   assert on identifiers, never counts; scope to your namespace; read the remediation

What to take away

  • Read a report by identifier, since each of them has a remediation and the remediation is where the knowledge is.
  • A check that fires on every term is a policy question, so adopt the policy or drop the check, and write down which of the two you did.
  • Some findings are correct and wanted, as STR-003 is on a property left open on purpose, and the answer there is a note rather than a change.

Measured

Parses6 triples; 3 annotation properties (with the data: 513 triples)
Ontology Quality Suite
  • QUA-009 ×83: Term https://example.org/bookshop-trail/schema#Author has no skos:prefLabel.
  • QUA-010 ×83: Term https://example.org/bookshop-trail/schema#Author has no skos:definition.
  • STR-003 ×1: Property https://example.org/bookshop-trail/schema#statedOn declares neither rdfs:domain/range nor gist-style domainIncludes/rangeIncludes annotations
  • STR-004 ×11: Class https://example.org/bookshop-trail/schema#Bookshop has no formal definition (no owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:un

Sources

O67

Style shapes: the vocabulary validated like data

The house style is written as SHACL shapes that target owl:Class and the property types, as gist does for itself, and the shipped vocabulary is validated against them in the editor.

Corner: symbol and thought · Data: 01-vocabulary.ttl · Runs in the browser

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Validated against style-shapes.ttl beside this lesson. The vocabulary passes every Violation-level constraint and collects a warning per class for the missing skos:definition."@en .

Why

An ontology is an RDF graph, so a shape can target it. gist's repository has validation/shapes/ontologyShapes.ttl, run on every commit, with a class shape requiring title-case preferred labels and a definition, a property shape requiring lower-case labels, and an instance shape requiring every rdf:type value to be a declared class. The targets there are SPARQL-based, selecting terms in the gist namespace, and the version here uses a SPARQL target scoped to the Trail's namespace in just the same way, which the editor's engine runs happily.

That scoping matters. The first draft of this lesson targeted every owl:Class and twelve violations came back, all of them borrowed classes such as geo:Geometry and prov:Entity, declared here so that the file is DL and documented upstream where they belong; findings about somebody else's terms are the SemOps Manual's chapter 9 in miniature.

On the shipped vocabulary the shapes report what o66's checks reported, in the vocabulary of the SHACL course: every class has a label and a comment, so the two Violation-level constraints pass; no class has a skos:definition, so the Warning fires seventeen times, once per Trail class; and every type in the file is a declared class, so the instance shape stays quiet. The report is the same fact as QUA-010 arrived at by a different tool, and the SemOps Manual's advice is to write a check both ways, as a query and as a shape, so that two engines corroborate one another rather than agreeing by construction. This is the SHACL course's whole method turned on the vocabulary, a shapes graph and a report and a severity chosen by whoever owns the policy, and it will run against any vocabulary somebody hands you, which is how to form a first opinion of one.

Diagram

   shape                  targets                   checks                          on the Trail
   ClassStyleShape        owl:Class in bs:          rdfs:label (Violation)          17 pass
                                                    rdfs:comment (Violation)        17 pass
                                                    skos:definition (Warning)       17 warnings: the policy not adopted
   PropertyStyleShape     properties in bs:         rdfs:label (Warning)            all pass
   DeclaredTypeShape      every rdf:type value      is a declared class             quiet

   the editor: vocabulary in one tab, shapes in another, Validate

What to take away

  • A vocabulary is a graph and shapes can target it, which is how gist validates its own style on every commit.
  • Write a check as a shape and as a query, because two engines that corroborate are worth more than one that agrees with itself.
  • Run style shapes over any vocabulary you are handed, since it is the fastest first opinion there is.

Note

Open in the editor opens this lesson with the shapes in a second tab; to validate the whole vocabulary, load data/01-vocabulary.ttl in the first tab instead, keep the shapes tab, and press Validate. The seventeen warnings are the ones in the measured line below, and the report says the vocabulary does not conform although nothing here is a violation, because a warning breaks conformance too. o29 is the lesson on that, and the rule it gives holds here, which is to read the counts.

Measured

Parses6 triples; 3 annotation properties (with the data: 513 triples)
SHACL (9 shapes compiled)does not conform · 0 violations, 17 warnings, 0 info
SeverityFocus nodePathMessage
Warningbs:DataIssueskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:StockRecordskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:Bookshopskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:Eventskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:Personskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:Settlementskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:Sourceskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:TrailSegmentskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:Placeskos:definitiongist style: a skos:definition rather than rdfs:comment.
Warningbs:Authorskos:definitiongist style: a skos:definition rather than rdfs:comment.
and 7 more

Sources

O68

What the metrics cannot see

Metrics and checks are one part of a gate that also runs the reasoner, the OntoClean queries, the style shapes and the competency questions, because a five-out-of-five ontology can still be wrong and no metric sees rigidity or meaning.

Corner: thought · Data: bookshop-trail-1.1.ttl · Runs in the browser

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "The gate: HermiT over the whole dataset; the OntoClean queries; the definition audit; style-shapes.ttl; the Quality Suite scoped to bs:; and the competency questions as queries. Three of the questions run below."@en .

Why

OQuaRE scores the Trail 5 on almost everything, and the Quality Suite's violations come out at zero once the policy questions have been decided. Neither of them knows whether Author is a role, whether Work is one class or two, whether the definitions are circular, whether bs:founded ought to be functional, or whether the ontology can answer question 7 in the brief. Metrics count what is written and checks pattern-match it, while meaning is not in the graph at all unless somebody has put it there, and the parts of this course that put it there, modules 02 and 04, are precisely the parts no metric reads.

So a gate wants more than one instrument. There is the reasoner, asking whether the thing is consistent and free of unsatisfiable classes with the whole dataset loaded, as o57 does; the OntoClean queries over the rigidity annotations from o13; the definition audit from o22; the style shapes of o67 and the checks by identifier of o66; and the competency questions, run as queries against the data, which are the only instrument that tests whether the ontology does its job at all. The SemOps Manual calls those the closest thing ontology work has to a requirement and makes them executable as .cq.rq tests; this lesson runs three of the brief's questions against the Trail's data as its measured line, and module 12 runs more of them.

The critical literature on OQuaRE says much the same from the other side, that the thresholds were proposed rather than validated, that the metrics overlap, and that several of them measure a style choice as though it were a defect, which o65 showed happening to gist. None of that makes the metrics useless; it makes them one instrument among six, and the one to trust least on its own.

Diagram

   instrument                    sees                                  blind to
   OQuaRE metrics                shape and counts                      meaning, rigidity, style choices it counts as absences
   OQS checks                    patterns, by identifier               whether the pattern is wanted
   style shapes                  the house style                       whether a definition is true
   the reasoner                  contradiction, dead classes           errors that are consistent
   OntoClean queries             rigidity, identity, dependence        anything not annotated
   competency questions          whether the questions get answered    questions nobody wrote down

   a gate is all six; the last is the one that tests the job

What to take away

  • An ontology that scores five out of five can still be wrong, because metrics count and do not read.
  • There are six instruments in the gate, and the competency questions are the one that tests whether the ontology does its job.
  • Trust the metrics least of all on their own, which is what their authors said first.

Measured

Parses6 triples; 3 annotation properties (with the data: 4830 triples)
Query: Question 2: towns on the trail with no bookshop4 rows
  • bt:place-durham
  • bt:place-fort-william
  • bt:place-perth
  • bt:place-truro
Query: Question 1: shops in Wales, by the path4 rows
  • bt:shop-castle-steps
  • bt:shop-cliff-road
  • bt:shop-clock-tower
  • bt:shop-taff-margin
Query: Question 7: authors influenced, at any remove, by Rhona Blackwood23 rows
  • bt:author-agnes-varden
  • bt:author-bram-tillotson
  • bt:author-callum-strachan
  • bt:author-delia-stannard
  • bt:author-dermot-lisle
  • bt:author-dilys-tremain
  • bt:author-fenella-drew
  • bt:author-hal-morrow

Afterwards, in the SPARQL panel

Question 2: towns on the trail with no bookshop (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT ?town WHERE { ?town a bs:Settlement . FILTER NOT EXISTS { ?shop bs:locatedIn ?town } } ORDER BY ?town
Question 1: shops in Wales, by the path (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?shop WHERE { ?shop a bs:Bookshop ; bs:locatedIn ?town . ?town bs:within+ bt:place-wales } ORDER BY ?shop
Question 7: authors influenced, at any remove, by Rhona Blackwood (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?author WHERE { ?author bs:influencedBy+ bt:author-rhona-blackwood } ORDER BY ?author

Sources

O69

The header: what an ontology says about itself

The ontology node carries a version IRI, a version number, its prior version, a title, a description, a creator, dates, a licence, its preferred prefix and namespace, and a named point of contact; the Quality Suite's metadata checks confirm nothing is missing.

Corner: symbol and thought · Runs in the browser

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .
@prefix vann: <http://purl.org/vocab/vann/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
dct:title           a owl:AnnotationProperty .
dct:description     a owl:AnnotationProperty .
dct:creator         a owl:AnnotationProperty .
dct:contributor     a owl:AnnotationProperty .
dct:created         a owl:AnnotationProperty .
dct:modified        a owl:AnnotationProperty .
dct:license         a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .
vann:preferredNamespacePrefix  a owl:AnnotationProperty .
vann:preferredNamespaceUri     a owl:AnnotationProperty .
owl:deprecated      a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                              owl:Ontology ;
    owl:versionIRI                 <https://example.org/bookshop-trail/schema/2.0.0> ;
    owl:versionInfo                "2.0.0" ;
    owl:priorVersion               <https://example.org/bookshop-trail/schema/1.1.0> ;
    dct:title                      "The Bookshop Trail schema"@en ;
    skos:prefLabel                 "The Bookshop Trail schema"@en ;
    dct:description                "A vocabulary for a fictional network of independent bookshops at real British locations: shops, places, works, people, events, trail segments, stock and sources. OWL 2 DL."@en ;
    dct:creator                    <https://github.com/pwin> ;
    dct:created                    "2025-09-09"^^xsd:date ;
    dct:modified                   "2026-09-21"^^xsd:date ;
    dct:license                    <https://opensource.org/license/mit> ;
    vann:preferredNamespacePrefix  "bs" ;
    vann:preferredNamespaceUri     "https://example.org/bookshop-trail/schema#" ;
    rdfs:seeAlso                   <https://www.semantechs.co.uk/Ontology_Course/> ;
    skos:editorialNote             "Responsible for this vocabulary and for answering questions about it: the Trail committee, via the repository's issue tracker."@en .

bs:Bookshop  a owl:Class ; skos:prefLabel "Bookshop"@en ; skos:definition "A business that sells books to the public from premises."@en .

Why

An ontology's header is the part read by the most tools and written with the least care. owl:versionIRI names this release, and OWL 2 says how it relates to the ontology IRI, the ontology IRI being the thing and the version IRI one edition of it, with an importer free to ask for either; owl:versionInfo carries the number for people to read and owl:priorVersion the one before it. The Dublin Core terms say what it is, who made it, when, and under what licence, which is OBO principle FP-001, openly available and stated so; vann gives the preferred prefix so that nobody has to guess; and a point of contact answers FP-011, because an ontology with nobody to ask is an ontology with a clock running on it.

gist's header is the model to copy, with a version IRI that has the number in it, a definition, a history note listing every release and its date, and a licence IRI. Nothing in it is decorative, and each line answers a question that somebody will ask. The Quality Suite has five checks for the header alone, QUA-002 and QUA-005 through QUA-008, covering no versioning metadata, no identifying IRI, an ontology IRI reused as a namespace, no version IRI and a non-https IRI; run over this file not one of them fires, which is the measured line.

The version IRI pattern matters rather more than it looks. gist uses gistCore14.1.0 as its version IRI with gist/ as the term namespace, and the Trail uses schema/2.0.0 with schema#, and both of them keep the term IRIs stable across versions, which is what lets data written against 1.1.0 stay true under 2.0.0, and is the principle o27 gave.

Diagram

   <.../schema>  a owl:Ontology ;                   the ontology: the thing
     owl:versionIRI <.../schema/2.0.0> ;            this edition; importable by name
     owl:versionInfo "2.0.0" ;                      the number, for people
     owl:priorVersion <.../schema/1.1.0> ;          the one before
     dct:title, dct:description                     what it is
     dct:creator, dct:created, dct:modified         who, when
     dct:license                                    FP-001: open, and stated
     vann:preferredNamespacePrefix "bs"             so nobody guesses
     skos:editorialNote "Responsible: ..."          FP-011: someone to ask

   QUA-002, QUA-005, QUA-006, QUA-007, QUA-008: the checks; none fires here

What to take away

  • The ontology IRI is the thing and the version IRI is an edition of it, while the term IRIs stay the same across editions.
  • Title, description, creator, dates, licence, prefix and contact: each of those lines answers a question somebody will ask.
  • Five checks cover the header, so run them, since a header is the easiest thing in an ontology to get right and the commonest to leave out.

Measured

Parses34 triples; 1 class; 16 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
Ontology Quality Suite
  • STR-004 ×1: Class https://example.org/bookshop-trail/schema#Bookshop has no formal definition (no owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:un

Sources

O70

Deprecate; do not delete

A term whose meaning has moved is marked deprecated, kept for a full major version, given a change note and a replacement, and the migration is asserted from the retiring IRI so that tooling can find it.

Corner: symbol · Runs in the browser; QUA-003 on the MEASURED line comes from the Ontology Quality Suite, scoped to the whole Trail namespace

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .
@prefix vann: <http://purl.org/vocab/vann/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
dct:title           a owl:AnnotationProperty .
dct:description     a owl:AnnotationProperty .
dct:creator         a owl:AnnotationProperty .
dct:contributor     a owl:AnnotationProperty .
dct:created         a owl:AnnotationProperty .
dct:modified        a owl:AnnotationProperty .
dct:license         a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .
vann:preferredNamespacePrefix  a owl:AnnotationProperty .
vann:preferredNamespaceUri     a owl:AnnotationProperty .
owl:deprecated      a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                              owl:Ontology ;
    owl:versionIRI                 <https://example.org/bookshop-trail/schema/2.0.0> ;
    owl:versionInfo                "2.0.0" ;
    owl:priorVersion               <https://example.org/bookshop-trail/schema/1.1.0> ;
    dct:title                      "The Bookshop Trail schema"@en ;
    skos:prefLabel                 "The Bookshop Trail schema"@en ;
    dct:description                "A vocabulary for a fictional network of independent bookshops at real British locations: shops, places, works, people, events, trail segments, stock and sources. OWL 2 DL."@en ;
    dct:creator                    <https://github.com/pwin> ;
    dct:created                    "2025-09-09"^^xsd:date ;
    dct:modified                   "2026-09-21"^^xsd:date ;
    dct:license                    <https://opensource.org/license/mit> ;
    vann:preferredNamespacePrefix  "bs" ;
    vann:preferredNamespaceUri     "https://example.org/bookshop-trail/schema#" ;
    rdfs:seeAlso                   <https://www.semantechs.co.uk/Ontology_Course/> ;
    skos:editorialNote             "Responsible for this vocabulary and for answering questions about it: the Trail committee, via the repository's issue tracker."@en .

bs:Work  a owl:Class ; skos:prefLabel "Work"@en .

bs:PublishedWork
    a                    owl:Class ;
    owl:deprecated       true ;
    skos:prefLabel       "Published Work"@en ;
    skos:changeNote      "Deprecated in 2.0.0: works acquired more than one edition, and the meaning moved to bs:Edition. Retained through 2.x; to be deleted in 3.0.0."@en ;
    dct:isReplacedBy     bs:Edition ;
    owl:equivalentClass  bs:Edition .

bs:Edition
    a                owl:Class ;
    skos:prefLabel   "Edition"@en ;
    skos:definition  "A printing of a Work by a publisher, with an ISBN if made after 1970."@en .

# Still in use, for QUA-003 to find.
bt:book-old  a bs:PublishedWork .

Why

gist's deprecation and deletion policy is short and worth copying whole. Deprecation is a minor change, so a term that is to go is marked owl:deprecated true in the next minor release, with a note saying why and what to use instead, and it stays available through the rest of that major version, so that nobody's data breaks between one release and the next. A major release then contains no deprecations at all, since every term deprecated during the previous major version is deleted in it and the release notes say so; and renaming a term counts as a deletion plus an addition, which makes it major too. The Trail's bs:PublishedWork in o27 followed the first half of that, and this lesson adds the mechanics.

The migration annotation is the part the tooling reads. The SemOps Manual's chapter 12 found that the Quality Suite's rename detection recognises owl:equivalentClass asserted from the old IRI to the new one and not the other way about, and that writing it backwards, which reads more naturally, silently drops a hundred- percent-confidence repair down to a guess. owl:equivalentClass is symmetric to a reasoner and directional to a tool, so the rule is to assert it from the retiring IRI; gist adds gist:isSupersededBy for the same purpose, and dct:isReplacedBy says the same thing to a person.

A deprecated term still in use is a finding, QUA-003, and the measured line has it, since this file types a work with the deprecated class so that the check can be seen to fire. It did not fire at first, and the reason is worth a sentence: the finding's focus node is the work, which lives in the data namespace, while the gate was scoped to the schema namespace, so the finding was filtered out as somebody else's. Scope the gate to the whole Trail for this check, or it will never see a deprecated term in use, which is the check's whole purpose. That is the finding to watch for in the release before a deletion, and if it fires then the deletion waits.

Diagram

   release N          bs:PublishedWork in use
   release N+1        bs:PublishedWork owl:deprecated true ;
                        skos:changeNote "..." ; dct:isReplacedBy bs:Edition ;
                        owl:equivalentClass bs:Edition .        <- from the retiring IRI, for the tooling
                      bs:Edition a owl:Class ...
   release N+1 .. M   both present; QUA-003 reports any use of the old one
   release M+1        bs:PublishedWork deleted; the release notes say so

   gist: deprecation is a minor change; a major release deletes every deprecated term and adds none

What to take away

  • Deprecate in a minor release, keep the term for the rest of the major version, delete at the next major one, and say so in the notes.
  • Assert the migration from the retiring IRI, old owl:equivalentClass new, because to a reasoner it is symmetric and to the tooling it is not.
  • QUA-003, a deprecated term still in use, is the check that decides whether a deletion can go ahead.

Note

Scoped to the whole Trail namespace, the suite also reports the undocumented individual and the references it cannot resolve in a file this small (QUA-004, STR-006, DAT-002). The wider scope costs you those, which is the reason to scope narrowly by default and to widen only for the checks that need it. QUA-003 is the one this lesson is about.

Measured

Parses43 triples; 3 classes; 16 annotation properties; 1 individual
Profile (ROBOT)OWL 2 DL: in profile
Ontology Quality Suite
  • DAT-002 ×3: Reference target https://github.com/pwin (object of https://example.org/bookshop-trail/schema http://purl.org/dc/terms/creator) is never used as a sub
  • QUA-003 ×1: Deprecated term https://example.org/bookshop-trail/schema#PublishedWork is still actively used on https://example.org/bookshop-trail/book-old; depreca
  • QUA-004 ×2: Resource https://example.org/bookshop-trail/book-old has no skos:prefLabel or rdfs:label.
  • QUA-010 ×2: Term https://example.org/bookshop-trail/schema#PublishedWork has no skos:definition.
  • STR-004 ×2: Class https://example.org/bookshop-trail/schema#Edition has no formal definition (no owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:uni
  • STR-006 ×2: Object https://opensource.org/license/mit of triple (https://example.org/bookshop-trail/schema http://purl.org/dc/terms/license) is never given an rdf
Query: Deprecated terms still in use1 row
  • bs:PublishedWork "1"

Afterwards, in the SPARQL panel

Deprecated terms still in use (on the model)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?term (COUNT(?x) AS ?uses) WHERE { ?term owl:deprecated true . ?x a ?term } GROUP BY ?term

Sources

O71

The version number decided by evidence

A release's version number is derived from what happened to the axioms, by diffing the old and new files, rather than from what the author believed the change was; a removed class makes it major whatever else was added.

Corner: thought · Runs in the browser; the verdict on the MEASURED line is the Ontology Quality Suite's version-diff over the two companion files beside this lesson

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .
@prefix vann: <http://purl.org/vocab/vann/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
dct:title           a owl:AnnotationProperty .
dct:description     a owl:AnnotationProperty .
dct:creator         a owl:AnnotationProperty .
dct:contributor     a owl:AnnotationProperty .
dct:created         a owl:AnnotationProperty .
dct:modified        a owl:AnnotationProperty .
dct:license         a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .
vann:preferredNamespacePrefix  a owl:AnnotationProperty .
vann:preferredNamespaceUri     a owl:AnnotationProperty .
owl:deprecated      a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                              owl:Ontology ;
    owl:versionIRI                 <https://example.org/bookshop-trail/schema/2.0.0> ;
    owl:versionInfo                "2.0.0" ;
    owl:priorVersion               <https://example.org/bookshop-trail/schema/1.1.0> ;
    dct:title                      "The Bookshop Trail schema"@en ;
    skos:prefLabel                 "The Bookshop Trail schema"@en ;
    dct:description                "A vocabulary for a fictional network of independent bookshops at real British locations: shops, places, works, people, events, trail segments, stock and sources. OWL 2 DL."@en ;
    dct:creator                    <https://github.com/pwin> ;
    dct:created                    "2025-09-09"^^xsd:date ;
    dct:modified                   "2026-09-21"^^xsd:date ;
    dct:license                    <https://opensource.org/license/mit> ;
    vann:preferredNamespacePrefix  "bs" ;
    vann:preferredNamespaceUri     "https://example.org/bookshop-trail/schema#" ;
    rdfs:seeAlso                   <https://www.semantechs.co.uk/Ontology_Course/> ;
    skos:editorialNote             "Responsible for this vocabulary and for answering questions about it: the Trail committee, via the repository's issue tracker."@en .

Why

Semantic versioning for an ontology follows the software rule with the terms translated: a change that can break a consumer is major, an addition is minor, and a wording change is a patch. The hard part is knowing which of the three a given release is, and the SemOps Manual's answer is not to ask the author. The Quality Suite's version-diff compares two files axiom by axiom and reports what was removed, what was added, and the bump that follows, so that one removed class makes the release MAJOR regardless of the seven additive changes around it, and regardless of anybody's view that nobody was using it.

This lesson's two companion files are version 1.1.0 and 2.0.0 of a small vocabulary. Between them bs:PublishedWork has gone, bs:Edition and bs:website have arrived, and bs:isbn's domain has moved; the diff on the measured line says MAJOR, and the lines it prints are the evidence a steward signs off on. --fail-on major turns that into a gate, so that a pull request removing a class cannot merge without a named reviewer, and the argument in the meeting becomes 'the diff says major, so who signs off', which is a better argument than 'do you think this is breaking'.

OBO Foundry FP-004 asks for documented versioning procedures, and gist has release notes for every version and a migration guide for the major ones. The procedure that costs least, though, is the one where the number is computed rather than argued over.

Diagram

   1.1.0                                   2.0.0                              version-diff
   bs:PublishedWork                        (gone)                             Removed classes [MAJOR]
   bs:isbn domain PublishedWork            bs:isbn domain Edition             changed
   --                                      bs:Edition                         Added classes [minor]
   --                                      bs:website                         Added properties [minor]

   Suggested version bump: MAJOR           one removal outweighs any number of additions
   --fail-on major: the gate; a named reviewer signs off

What to take away

  • Major if a consumer can break, minor if there are only additions, patch if only the wording moved, and derive it from the diff rather than from the author.
  • One removal makes a release major whatever else it contains, and that is the property which makes the number trustworthy.
  • Make it a gate with --fail-on major, so that the decision stays human while the basis for it stops being who argued hardest.

Note

The two versions are schema-1.1.0.ttl and schema-2.0.0.ttl beside this lesson. Open either in the editor; to run the diff, python -m ontology_suite version-diff schema-1.1.0.ttl schema-2.0.0.ttl --exclude-imports. Old first, then new.

Measured

Parses31 triples; 16 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
version-diff (Ontology Quality Suite)Suggested version bump: MAJOR
  • Removed classes [MAJOR]:
  • - https://example.org/bookshop-trail/schema#PublishedWork
  • Removed subclass edges [MAJOR]:
  • - https://example.org/bookshop-trail/schema#PublishedWork no longer rdfs:subClassOf https://example.org/bookshop-trail/schema#Work
  • Narrowed/changed property domains [MAJOR]:
  • - https://example.org/bookshop-trail/schema#isbn: {https://example.org/bookshop-trail/schema#PublishedWork} -> {https://example.org/bookshop-trail/schema#Edition}
  • Added classes [minor]:
  • - https://example.org/bookshop-trail/schema#Edition
  • Added properties [minor]:
  • - https://example.org/bookshop-trail/schema#website
  • Added subclass edges [minor]:
  • - https://example.org/bookshop-trail/schema#Edition rdfs:subClassOf https://example.org/bookshop-trail/schema#Work

Sources

O72

Publishing the IRIs, and keeping them alive

The vocabulary is published where its IRIs resolve, with a persistent redirector in front, documentation generated from the file, a channel for requests, and notice of changes; and where the Trail cannot do that, it says so.

Corner: symbol · Runs in the browser

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:  <http://purl.org/dc/terms/> .
@prefix vann: <http://purl.org/vocab/vann/> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .
skos:changeNote     a owl:AnnotationProperty .
skos:historyNote    a owl:AnnotationProperty .
dct:title           a owl:AnnotationProperty .
dct:description     a owl:AnnotationProperty .
dct:creator         a owl:AnnotationProperty .
dct:contributor     a owl:AnnotationProperty .
dct:created         a owl:AnnotationProperty .
dct:modified        a owl:AnnotationProperty .
dct:license         a owl:AnnotationProperty .
dct:isReplacedBy    a owl:AnnotationProperty .
vann:preferredNamespacePrefix  a owl:AnnotationProperty .
vann:preferredNamespaceUri     a owl:AnnotationProperty .
owl:deprecated      a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                              owl:Ontology ;
    owl:versionIRI                 <https://example.org/bookshop-trail/schema/2.0.0> ;
    owl:versionInfo                "2.0.0" ;
    owl:priorVersion               <https://example.org/bookshop-trail/schema/1.1.0> ;
    dct:title                      "The Bookshop Trail schema"@en ;
    skos:prefLabel                 "The Bookshop Trail schema"@en ;
    dct:description                "A vocabulary for a fictional network of independent bookshops at real British locations: shops, places, works, people, events, trail segments, stock and sources. OWL 2 DL."@en ;
    dct:creator                    <https://github.com/pwin> ;
    dct:created                    "2025-09-09"^^xsd:date ;
    dct:modified                   "2026-09-21"^^xsd:date ;
    dct:license                    <https://opensource.org/license/mit> ;
    vann:preferredNamespacePrefix  "bs" ;
    vann:preferredNamespaceUri     "https://example.org/bookshop-trail/schema#" ;
    rdfs:seeAlso                   <https://www.semantechs.co.uk/Ontology_Course/> ;
    skos:editorialNote             "Responsible for this vocabulary and for answering questions about it: the Trail committee, via the repository's issue tracker."@en .

<https://example.org/bookshop-trail/schema>
    skos:editorialNote  "Not published at a resolving IRI: example.org is reserved for documentation, and this vocabulary is a teaching one. Documentation is generated by scripts/build_docs.py and the Quality Suite's docgen; requests and questions go to the repository's issue tracker; changes are announced in the release notes and the course document."@en .

Why

An IRI that resolves is the last leg of o04's promise, that you can fetch it and get the definition back. Cool URIs sets out how, with a hash namespace served as one document or a slash namespace served a resource at a time, and content negotiation returning Turtle to a program and HTML to a person, while the Best Practice Recipes note gives the server configuration for either case. A persistent redirector, w3id.org in gist's case, sits in front of the lot so that the hosting can move and the IRIs need not.

Documentation is generated rather than written, so that it cannot drift: the Quality Suite's docgen produces a reference page from the file, with its classes and properties and diagrams, and the SemOps Manual's chapter 13 makes that a build step. Then come the OBO Foundry's social principles, a channel for community participation in FP-020, responsiveness to requests in FP-016, and notice of major changes to those who depend on the ontology in FP-013; none of them is technical, and each of them is a reason an ontology is still being used in five years' time.

The Trail says which of these it does. Its namespace is example.org, which is reserved for documentation, so nothing resolves and nothing ever will, the point of the course being the thinking rather than the hosting; its documentation is generated, its channel is the repository's issue tracker, and its notice of change is the commit log together with this course document. All of which is stated on the header, which is the SemOps ledger again: say what you do not do, in the place where somebody will look for it.

Diagram

   a program asks for bs:Bookshop     Accept: text/turtle        ->  the schema document, Turtle
   a person asks for bs:Bookshop      Accept: text/html          ->  the generated documentation, at #Bookshop
   the host moves                     w3id.org redirects         ->  the IRIs do not change

   docgen  ->  reference page from the file, every build       (SemOps 13.2)
   FP-020  a channel        FP-016  responsiveness        FP-013  notice of major changes

   the Trail: example.org, so nothing resolves; the header says so

What to take away

  • Resolving IRIs close the loop from symbol to definition, and a redirector in front is what keeps them resolving when the host changes.
  • Generate the documentation from the file, because written documentation drifts and generated documentation cannot.
  • The last three principles are social ones, a channel and responsiveness and notice of change, and they decide whether the ontology is still alive in five years.

Measured

Parses32 triples; 16 annotation properties
Profile (ROBOT)OWL 2 DL: in profile

Sources

Module 12

Putting it together

The whole Trail vocabulary, rebuilt and compared with the shipped one decision by decision; the genre scheme with its integrity checked; the competency questions the SPARQL course answers and the constraints the SHACL course enforces, taken together as the test suite this ontology exists to pass; and then a challenge to finish with.

Sources
O73

The whole vocabulary, decision by decision

The shipped vocabulary is read as the sum of the course's decisions, each traced to the lesson that made it, and put through every instrument the course has: profile, reasoner, metrics, checks, audits.

Corner: all three · Data: 01-vocabulary.ttl · Runs in the browser; the profile verdicts and HermiT are ROBOT; the findings are the Quality Suite; the metrics and audits are the course's scripts

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Decisions to take at the next release: OntoClean annotations on every class (o13); SKOS annotations in place of rdfs:label and rdfs:comment (o23); gist:isCategorizedBy for the event kinds (o60)."@en .

Why

data/01-vocabulary.ttl is the Bookshop Trail's schema as the SPARQL and SHACL courses use it, and every choice in it now has a lesson behind it. Its classes are the backbone of o12 with one role hanging off them; its properties are named as o18 said, with the four departures recorded; its comments carry definitions in the form o21 asked for with usage notes beside them in the same property, which o23 called a compromise. bs:within is transitive and not functional, from o48, and bs:founded is not functional, from o29; the domain of bs:locatedIn is a union, from o38, and the year properties have no range at all, from o50; the genres are a concept scheme, from o24 and o36, and the borrowed terms are all declared, from o26 and o42; and bs:disputes is an annotation property because it points at an RDF 1.2 triple term, which is where OWL 2 stops.

The measured line puts the whole thing through every instrument at once: OWL 2 DL and in none of EL, QL or RL, since EL and QL refuse a functional object property and RL the union domain of bs:locatedIn; consistent, with nothing unsatisfiable; seventeen classes of its own and twenty-five counting the borrowed ones; every local name conforming; no circular definition; the OQuaRE scores from module 10; and the Quality Suite's findings scoped to bs:, which come down to the SKOS policy question and the domains left open on purpose.

What it does not yet have are the two things the course added and the shipped file has not adopted, which are OntoClean annotations on the classes and SKOS annotations in place of rdfs:label and rdfs:comment. Both are one pass of work, and both are recorded here as decisions still to take, which is the right state for a vocabulary to be in between releases.

Diagram

   in the shipped file                          decided in
   ten top-level kinds, one role                o12, o08
   Author under Person, never the reverse       o08, o09
   Work = work + edition, noted                 o10, o30
   author, locatedIn, publishedBy, genre        o18 (departures recorded)
   rdfs:label + rdfs:comment                    o23 (a compromise; SKOS annotations to adopt)
   bs:within transitive, not functional         o48
   bs:founded not functional                    o29
   locatedIn domain: a union                    o38, o31
   gYear properties: no range                   o50
   genres: skos:Concept, not classes            o24, o36
   borrowed terms declared                      o26, o42
   bs:disputes an annotation property           o42

   measured: DL; not EL/QL/RL; consistent; 17 own classes; names clean; definitions clean

What to take away

  • A vocabulary is the sum of its decisions, so trace each of them to its reason and the file becomes readable as an argument.
  • Put it through every instrument at once before a release, which is what the measured line shows you.
  • The decisions not yet taken are recorded too, and that is the state a vocabulary should be in between releases.

Measured

Parses6 triples; 3 annotation properties (with the data: 513 triples)
Naming auditevery local name conforms
Definition auditno circular definition
Profile (ROBOT)OWL 2 DL: in profile
OWL 2 EL: not in profile Axiom type not allowed in profile [FunctionalObjectProperty(<https://example.org/bookshop-trail/schema#atShop>) in OntologyID(OntologyIRI(<h
OWL 2 QL: not in profile Axiom type not allowed in profile [FunctionalObjectProperty(<https://example.org/bookshop-trail/schema#atShop>) in OntologyID(OntologyIRI(<h
OWL 2 RL: not in profile Use of non-superclass expression in position that requires a superclass expression: ObjectUnionOf(<https://example.org/bookshop-trail/schema
HermiTconsistent; 1 inferred
  • bs:author owl:inverseOf bs:wrote
Ontology Quality Suite
  • QUA-009 ×83: Term https://example.org/bookshop-trail/schema#Author has no skos:prefLabel.
  • QUA-010 ×83: Term https://example.org/bookshop-trail/schema#Author has no skos:definition.
  • STR-003 ×1: Property https://example.org/bookshop-trail/schema#statedOn declares neither rdfs:domain/range nor gist-style domainIncludes/rangeIncludes annotations
  • STR-004 ×11: Class https://example.org/bookshop-trail/schema#Bookshop has no formal definition (no owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:un
OQuaRE (scripts/metrics.py)17 classes, 25 object properties, 40 datatype properties, 11 roots, 14 leaves
MetricValueScore 1 to 5
LCOMOnto1.4295
WMCOnto3.2945
DITOnto25
NACOnto0.4295
NOCOnto15
CBOOnto15
RFCOnto6.1673
NOMOnto1.8245
RROnto74%4
AROnto182%5
INROnto147%5
CROnto0%1
ANOnto200%5
TMOnto15
Query: The vocabulary's own account of its properties without a range6 rows
  • bs:born "Year of birth.No range is declared, and that is deliberate: the shipped 1.1 data types this as xsd:gYear, which is not in the OWL 2 datatype map, so declaring the range would put the specification outside OWL 2 DL. Declaring xsd:integer instead would be worse -- it would be false of the data. See data/bookshop-trail-owl-dl.ttl."
  • bs:died "Year of death. Absent for the living.No range is declared, and that is deliberate: the shipped 1.1 data types this as xsd:gYear, which is not in the OWL 2 datatype map, so declaring the range would put the specification outside OWL 2 DL. Declaring xsd:integer instead would be worse -- it would be false of the data. See data/bookshop-trail-owl-dl.ttl."
  • bs:eventDate "The day it happened.No range is declared, and that is deliberate: the shipped 1.1 data types this as xsd:date, which is not in the OWL 2 datatype map, so declaring the range would put the specification outside OWL 2 DL. Declaring xsd:integer instead would be worse -- it would be false of the data. See data/bookshop-trail-owl-dl.ttl."
  • bs:founded "The year the shop or publisher opened. NOT functional: the RDF 1.2 layer records rival claims about four of them, and a functional declaration would make those a contradiction rather than a disagreement.No range is declared, and that is deliberate: the shipped 1.1 data types this as xsd:gYear, which is not in the OWL 2 datatype map, so declaring the range would put the specification outside OWL 2 DL. Declaring xsd:integer instead would be worse -- it would be false of the data. See data/bookshop-trail-owl-dl.ttl."
  • bs:publicationYear "Year of first publication of this edition.No range is declared, and that is deliberate: the shipped 1.1 data types this as xsd:gYear, which is not in the OWL 2 datatype map, so declaring the range would put the specification outside OWL 2 DL. Declaring xsd:integer instead would be worse -- it would be false of the data. See data/bookshop-trail-owl-dl.ttl."
  • bs:statedOn "The date a claim was made. Subject is a reifier.No range is declared, and that is deliberate: the shipped 1.1 data types this as xsd:date, which is not in the OWL 2 datatype map, so declaring the range would put the specification outside OWL 2 DL. Declaring xsd:integer instead would be worse -- it would be false of the data. See data/bookshop-trail-owl-dl.ttl."

Afterwards, in the SPARQL panel

The vocabulary's own account of its properties without a range (on the data)
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?property ?comment WHERE {
  ?property a owl:DatatypeProperty ; rdfs:comment ?comment .
  FILTER NOT EXISTS { ?property rdfs:range ?r }
  FILTER(STRSTARTS(STR(?property), "https://example.org/bookshop-trail/schema#"))
} ORDER BY ?property

Sources

O74

The genre scheme, complete

The shipped genre scheme, thirty-one concepts two to four levels deep, passes the SKOS integrity shapes of o39 and the cycle and depth queries, and is kept a concept scheme rather than promoted.

Corner: all three · Data: 02-genres.ttl · Runs in the browser

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

skos:ConceptScheme  a owl:Class .
skos:hasTopConcept  a owl:ObjectProperty .

bt:genre-scheme
    a                   skos:ConceptScheme ;
    skos:prefLabel      "Bookshop Trail genre scheme"@en ;
    skos:hasTopConcept  bt:genre-literature ;
    skos:editorialNote  "Kept as a concept scheme: no competency question needs an axiom about the members of a genre. Promote (o40) only if one does."@en .

Why

data/02-genres.ttl is the shelf plan: one concept scheme with one top concept and thirty-one concepts, preferred labels in English with Welsh and Gaelic wherever those names exist, alternative labels for the American spellings and the trade names, scope notes throughout, and a tree that runs two levels deep under Poetry and four under Science Fiction, because that is how the shops shelve them. o36 said why the depth is uneven and why that is right, o24 said why the genres are particulars, and o39 gave the integrity conditions.

The measured line runs the o39 shapes over the whole scheme, checking one preferred label per language, every concept in the scheme, related disjoint from broader, and no concept its own ancestor, and it conforms. The queries count the concepts, find the deepest of them, and confirm that skos:broader+ from the top concept reaches all thirty of the rest; that last one is the test that matters for question 4 in the brief, which the SPARQL course's q33 answers with skos:broader+ and which a fixed chain of two hops gets wrong, as q28 shows on the place hierarchy.

The scheme stays a scheme. No question in the brief needs an axiom about the members of a genre, so o40's promotion is not taken, and the shelf plan can be rearranged by the shops without a reasoner retyping a single work.

Diagram

   bt:genre-scheme  a skos:ConceptScheme ; skos:hasTopConcept bt:genre-literature
     Literature
       Fiction: Crime (Cosy, Tartan Noir, Police Procedural), Speculative (SF: Hard, Space Opera, Climate; Fantasy: Folk, Portal), ...
       Non-fiction: Nature Writing, History (Maritime, Industrial), Travel, Biography, ...
       Poetry

   31 concepts, depth 2 to 4, one preferred label per language, alt labels, scope notes
   integrity shapes: conforms.  broader+ from the top: reaches every concept.

What to take away

  • A concept scheme is finished when its integrity conditions pass and its questions are answered by paths, neither of which needs a reasoner.
  • Uneven depth is a fact about the domain, and the queries that walk it must not assume otherwise.
  • Not promoting is itself a decision, so record it, and revisit it on the day an axiom about members is needed.

Note

Open in the editor opens this lesson with the shapes; to validate the whole scheme, load data/02-genres.ttl in the first tab, keep the shapes tab, and press Validate. The AFTERWARDS queries run on the scheme.

Measured

Parses9 triples; 1 class; 1 object property; 3 annotation properties (with the data: 221 triples)
SHACL (5 shapes compiled)conforms · 0 violations, 0 warnings, 0 info
Query: How many concepts, and how many the top concept reaches1 row
  • "31" "30"
Query: The deepest concepts5 rows
  • bt:genre-hard-sf "4"
  • bt:genre-space-opera "4"
  • bt:genre-climate-fiction "4"
  • bt:genre-folk-fantasy "4"
  • bt:genre-portal-fantasy "4"
Query: Concepts that are their own ancestor (none)0 rows

Afterwards, in the SPARQL panel

How many concepts, and how many the top concept reaches (on the data)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT (COUNT(DISTINCT ?c) AS ?concepts) (COUNT(DISTINCT ?reached) AS ?reachedFromTop) WHERE {
  ?c a skos:Concept .
  OPTIONAL { ?reached skos:broader+ bt:genre-literature . FILTER(?reached = ?c) }
}
The deepest concepts (on the data)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?leaf (COUNT(?up) AS ?depth) WHERE { ?leaf a skos:Concept ; skos:broader+ ?up }
GROUP BY ?leaf ORDER BY DESC(?depth) LIMIT 5
Concepts that are their own ancestor (none) (on the data)
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?c WHERE { ?c skos:broader+ ?c }

Sources

O75

The questions answered, and the rules kept elsewhere

The brief's ten questions are run against the whole dataset as the acceptance test of the vocabulary, and the constraints the ontology declined to express are found in the SHACL course, where they belong.

Corner: all three · Data: bookshop-trail-1.1.ttl · Runs in the browser

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema"@en ;
    skos:editorialNote  "Acceptance test: the brief's ten questions as queries, run on bookshop-trail-1.1.ttl. The SPARQL course holds the full suite; the SHACL course holds the rules."@en .

Why

An ontology exists to answer questions, as o07 said, and the SPARQL course is the Trail's questions written down, 145 of them, run on three engines and compared value by value. This lesson runs the brief's ten as the acceptance test, against the shipped vocabulary and data, in the editor's SPARQL panel or in the checker, and each of them is a row count on the measured line: four towns with no bookshop, four shops in Wales, the shops you can reach on foot from The Inkwell, the imprints that belong in the end to the Lodestone Group, the works with no ISBN, and the two founding claims for Ex Libris. Where the SPARQL course has the question already, its number is given.

The other half of the package is the SHACL course, which holds the rules this course refused to put into the ontology. Exactly one town per shop is s04; one founding year is s04 as well, as a finding rather than a contradiction, which is o29's argument; a valid ISBN check digit is s39; a segment's ends being two different shops is s36; and nothing being within itself is s30. Each of those is a rule about the dataset, with a severity somebody chose, and each is exactly the kind of thing gist's tenth rule sends off to SHACL.

Three courses, then, over one dataset and one vocabulary: the ontology says what the terms mean, the queries ask, and the shapes check. A question that cannot be asked, or a rule that cannot be checked, is a gap in the vocabulary, and this is how you would find it.

Diagram

   question (the brief)                                 answered by          SPARQL course   rule kept in SHACL course
   1  shops in Wales / an area                          bs:within+           q28             --
   2  towns with no bookshop                            NOT EXISTS           q16             s21 (as a warning)
   3  walkable from The Inkwell, and how far            (connectsTo|^)+      q31, q32        s36
   4  crime fiction shops, including its kinds          skos:broader*        q33, q28        --
   5  events in York, and the cost                      heldAt/locatedIn     q20s            --
   6  works with no ISBN                                NOT EXISTS           q15             s39 (check digit)
   7  influenced at any remove by Rhona Blackwood       influencedBy+        q34             s30 (no cycles)
   8  imprints ultimately of the Lodestone Group        imprintOf+           q44             --
   9  when Ex Libris opened, and who says so            RDF 1.2 layer        q61 to q63      s53 to s55
   10 who stocks a book, cheapest                       StockRecord          q21             s47

What to take away

  • The competency questions are the acceptance test, so run them over the whole dataset before every release.
  • The rules the ontology refused are not lost but sitting in the SHACL course as shapes, each with a severity somebody chose.
  • Three courses make one package, meaning and questions and checks, and a gap in any one of them shows up in another.

Measured

Parses6 triples; 3 annotation properties (with the data: 4830 triples)
Query: Q3: shops reachable on foot from The Inkwell1 row
  • "30"
Query: Q4: shops specialising in crime fiction, including its kinds4 rows
  • bt:shop-broken-spine "Tartan Noir"
  • bt:shop-clock-tower "Cosy Crime"
  • bt:shop-dog-eared "Cosy Crime"
  • bt:shop-inkwell "Crime Fiction"
Query: Q6: works with no ISBN1 row
  • "11"
Query: Q8: imprints that belong, ultimately, to the Lodestone Group5 rows
  • bt:pub-bell-rock
  • bt:pub-cairn
  • bt:pub-drovers-road
  • bt:pub-northwind
  • bt:pub-saltmarsh
Query: Q10: who stocks The Book Town, cheapest first3 rows
  • bt:shop-inkwell "9.99"
  • bt:shop-dog-eared "9.99"
  • bt:shop-clock-tower "9.99"

Afterwards, in the SPARQL panel

Q3: shops reachable on foot from The Inkwell (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT (COUNT(DISTINCT ?shop) AS ?reachable) WHERE { bt:shop-inkwell (bs:connectsTo|^bs:connectsTo)+ ?shop . FILTER(?shop != bt:shop-inkwell) }
Q4: shops specialising in crime fiction, including its kinds (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX bt: <https://example.org/bookshop-trail/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?shop ?genre WHERE { ?shop bs:specialises ?g . ?g skos:broader* bt:genre-crime-fiction ; skos:prefLabel ?genre . FILTER(LANG(?genre) = "en") } ORDER BY ?shop
Q6: works with no ISBN (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
SELECT (COUNT(?w) AS ?withoutIsbn) WHERE { ?w a bs:Work . FILTER NOT EXISTS { ?w bs:isbn ?i } }
Q8: imprints that belong, ultimately, to the Lodestone Group (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?imprint WHERE { ?imprint bs:imprintOf+ bt:pub-lodestone-group } ORDER BY ?imprint
Q10: who stocks The Book Town, cheapest first (on the data)
PREFIX bs: <https://example.org/bookshop-trail/schema#>
PREFIX bt: <https://example.org/bookshop-trail/>
SELECT ?shop ?price WHERE { ?rec bs:ofWork bt:book-the-book-town ; bs:atShop ?shop ; bs:shelfPrice ?price } ORDER BY ?price

Sources

O76

Challenge: the day a second edition appears

Split bs:Work into Work and Edition so that the compromise of o10 is retired, every assertion made under the old meaning stays true, the competency questions still answer, and the reasoner agrees; an answer is beside the lesson, to be read after yours.

Corner: all three · Runs in the browser

@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

skos:prefLabel      a owl:AnnotationProperty .
skos:definition     a owl:AnnotationProperty .
skos:editorialNote  a owl:AnnotationProperty .

<https://example.org/bookshop-trail/schema>
    a                   owl:Ontology ;
    skos:prefLabel      "The Bookshop Trail schema, 3.0.0 (the challenge)"@en ;
    skos:editorialNote  "Your answer goes here. answer.ttl beside this lesson is one answer, measured."@en .

Why

Here is the brief that arrives after the site has launched: Bell Rock has issued a second edition of The Book Town, with a new ISBN and a different page count, and the committee wants both of them stocked and told apart. Under the shipped vocabulary a work has one ISBN, functionally, so there is no way to say it; o10 recorded the compromise and o27 said what the fix must respect, which is that the meaning of bs:Work must not move underneath the data already written.

The task, then, is this. Introduce bs:Edition with an identity criterion of its own, the ISBN, declared as a key. Decide which of bs:Work's properties belong to the work, which are author, title, genre and original language, and which belong to the edition, which are publisher, ISBN, pages, price and publication year, and move only the second group, with a change note on each. Decide how existing bs:Work assertions carrying an ISBN are to be migrated, where a first edition minted per work is one answer. Keep Work and Edition disjoint, their criteria of identity being different. Decide the version number by the diff, as o71 does. And then check the result: OWL 2 DL, consistent with the data, the stock records now pointing at editions with question 10 in the brief still answering, and a query for the editions of The Book Town returning two.

answer.ttl beside this lesson is one way of doing it, measured as DL and consistent, with the migration of one work shown. It is not the only way, since a version that keeps bs:Work for editions and mints a new bs:AbstractWork above it is perfectly defensible, and the argument about which of them is better is the argument this whole course has been about.

Diagram

   before                                   after
   bs:Work  isbn, pages, publishedBy,       bs:Work      author, title, genre, originalLanguage
            author, title, genre            bs:Edition   editionOf (functional), isbn (key), pages, publishedBy, rrp, publicationYear
                                            Work and Edition disjoint: different criteria of identity

   acceptance
   - OWL 2 DL; consistent with the data
   - every 2.x assertion still true of something (the work, or its first edition)
   - question 10 still answers; 'editions of The Book Town' returns two
   - version-diff says MAJOR, and the change notes say why

What to take away

  • The day the identity criterion changes is the day a class splits, and the split is a new term rather than a new meaning for the old one.
  • Sort the properties by which referent they belong to, since the design work is that sorting and the axioms follow from it.
  • Acceptance is measured, by profile and reasoner and competency questions and diff, so read the answer only once yours has passed them.

Note

The checker measures answer.ttl as well: python scripts/check_lessons.py o76 --show prints what HermiT and the profile checker made of it. To measure your own, put it in place of answer.ttl and run the same command.

Measured

Parses6 triples; 3 annotation properties
Profile (ROBOT)OWL 2 DL: in profile
answer.ttlOWL 2 DL; consistent

Sources

Reference

The three courses

One dataset, one vocabulary, one editor, three courses. This one says what the terms mean; the SPARQL course asks the questions; the SHACL course checks the data. Lesson ids link across: o52 reaches the same 186 pairs as the SPARQL course's module 18 and the SHACL course's s51.

This courseThe SPARQL courseThe SHACL course
o29 bs:founded left non-functionalq61 to q63, the disputed datess04 exactly one founding year, as a shape
o36 the uneven genre treeq33 every book of fiction however narrow the genre; q28 the same lesson on the place hierarchys20 the same chain in a shape, on places
o38 domain as an inference ruleq143 the inference nobody wanteds59 the inference that hides an error
o43 disjointness, as a query in the browsermodule 13, debuggings25 sh:not
o47 cardinality merges before it complainsq03 which town is each shop ins04 the shape that was meant
o48 bs:within transitivemodule 18, the 186 pairs from a reasoners50, s51 the same by a rule
o49 the ISBN as a keyq15 works with no ISBNs39 the check digit
o52 three routes to one inferencemodule 00 §3, the reasoner and the paths51 to a fixpoint
o67 style shapes on the vocabulary—s16 sh:uniqueLang, s15 sh:pattern
o75 the ten questions as the acceptance testthe whole courses64 the whole trail in one shapes graph

The SemOps Manual is the fourth text this course leans on: chapter 2 on definitions as territory, 8 on the inner loop, 9 on the gate, 12 on evidence-based versions.

Reference

Every term, and where it is shown

Generated from the lesson models themselves, so it cannot disagree with them. The same index is FEATURES.md in the repository.

RDF and RDF Schema

SKOS

TermDefined inLessons
skos:ConceptSKOS Reference 3 The skos:Concept Classo05 o24 o26 o35 o36 o39 o40 o42 o59 o74
skos:ConceptSchemeSKOS Reference 4 Concept Schemeso36 o39 o74
skos:inSchemeSKOS Reference 4 Concept Schemeso36 o39 o74
skos:hasTopConceptSKOS Reference 4 Concept Schemeso36 o74
skos:prefLabelSKOS Reference 5 Lexical Labelso15 o16 o17 o18 o19 o20 o21 o22 o23 o24 o25 o26 o27 o28 o29 o30 o31 o32 o33 o34 o35 o36 o37 o38 o39 o40 o41 o42 o43 o44 o45 o46 o47 o48 o49 o50 o51 o52 o53 o54 o55 o56 o57 o58 o59 o60 o61 o62 o63 o64 o65 o66 o67 o68 o69 o70 o71 o72 o73 o74 o75 o76
skos:altLabelSKOS Reference 5 Lexical Labelso15 o16 o17 o18 o19 o20 o21 o22 o23 o24 o25 o26 o27 o35 o36 o37 o38 o39 o40 o41
skos:hiddenLabelSKOS Reference 5 Lexical Labelso21 o22 o23 o24 o25 o26 o27 o39
skos:notationSKOS Reference 5 Lexical Labelso04 o15 o16 o17 o18 o19 o20 o21 o22 o23 o24 o25 o26 o27
skos:definitionSKOS Reference 7 Documentation Propertieso15 o16 o17 o18 o19 o20 o21 o22 o23 o24 o25 o26 o27 o28 o29 o30 o31 o32 o33 o34 o35 o36 o37 o38 o39 o40 o41 o42 o43 o44 o45 o46 o47 o48 o49 o50 o51 o52 o53 o54 o55 o56 o57 o58 o59 o60 o61 o62 o63 o64 o65 o66 o67 o68 o69 o70 o71 o72 o73 o74 o75 o76
skos:scopeNoteSKOS Reference 7 Documentation Propertieso07 o21 o22 o23 o24 o25 o26 o27 o28 o29 o30 o31 o32 o33 o34 o35 o36 o37 o38 o39 o40 o41
skos:exampleSKOS Reference 7 Documentation Propertieso21 o22 o23 o24 o25 o26 o27
skos:editorialNoteSKOS Reference 7 Documentation Propertieso03 o08 o09 o10 o11 o12 o13 o14 o15 o16 o17 o18 o19 o20 o21 o22 o23 o24 o25 o26 o27 o28 o29 o30 o31 o32 o33 o34 o35 o36 o37 o38 o39 o40 o41 o42 o43 o44 o45 o46 o47 o48 o49 o50 o51 o52 o53 o54 o55 o56 o57 o58 o59 o60 o61 o62 o63 o64 o65 o66 o67 o68 o69 o70 o71 o72 o73 o74 o75 o76
skos:historyNoteSKOS Reference 7 Documentation Propertieso21 o22 o23 o24 o25 o26 o27 o69 o70 o71 o72
skos:changeNoteSKOS Reference 7 Documentation Propertieso21 o22 o23 o24 o25 o26 o27 o69 o70 o71 o72 o76
skos:broaderSKOS Reference 8 Semantic Relationso24 o35 o36 o39 o40 o74
skos:narrowerSKOS Reference 8 Semantic Relationso39 o74
skos:relatedSKOS Reference 8 Semantic Relationso39 o74
skos:CollectionSKOS Reference 9 Concept Collectionso39
skos:memberSKOS Reference 9 Concept Collectionso39
skos:closeMatchSKOS Reference 10 Mapping Propertieso39

OWL 2: classes

TermDefined inLessons
owl:ClassOWL 2 Primer 4.1 Classes, Properties and Individualso08 o09 o10 o11 o12 o13 o14 o15 o16 o17 o19 o20 o21 o22 o23 o24 o25 o26 o27 o28 o29 o30 o31 o32 o33 o34 o35 o36 o37 o38 o39 o40 o42 o43 o44 o45 o46 o47 o48 o49 o50 o51 o52 o53 o54 o55 o56 o58 o59 o60 o61 o62 o67 o69 o70 o71 o74 o76
owl:ThingOWL 2 Primer 4.1 Classes, Properties and Individualso35
owl:NothingOWL 2 Primer 4.1 Classes, Properties and Individualso54
owl:equivalentClassOWL 2 Primer 5.1 Complex Classeso46 o55 o56 o70
owl:disjointWithOWL 2 Primer 4.3 Class Disjointnesso34 o35 o43 o53 o54
owl:AllDisjointClassesOWL 2 Primer 4.3 Class Disjointnesso10 o31 o43 o67 o76
owl:membersOWL 2 Primer 4.3 Class Disjointnesso10 o31 o43 o53 o76
owl:intersectionOfOWL 2 Primer 5.1 Complex Classeso46 o55 o56
owl:unionOfOWL 2 Primer 5.1 Complex Classeso31 o61
owl:complementOfOWL 2 Primer 5.1 Complex Classeso46
owl:oneOfOWL 2 Primer 5.3 Enumeration of Individualso46
owl:RestrictionOWL 2 Primer 5.2 Property Restrictionso44 o45 o46 o47 o54 o55 o56 o67 o76
owl:onPropertyOWL 2 Primer 5.2 Property Restrictionso44 o45 o46 o47 o54 o55 o56 o76
owl:someValuesFromOWL 2 Primer 5.2 Property Restrictions: existential quantificationo44 o46 o54 o55 o56 o76
owl:allValuesFromOWL 2 Primer 5.2 Property Restrictions: universal quantificationo45 o54
owl:hasValueOWL 2 Primer 5.2 Property Restrictionso55
owl:cardinalityOWL 2 Primer 5.2.3 Property Cardinality Restrictionso47
owl:minCardinalityOWL 2 Primer 5.2.3 Property Cardinality Restrictionso47
owl:maxCardinalityOWL 2 Primer 5.2.3 Property Cardinality Restrictionso47
owl:qualifiedCardinalityOWL 2 Primer 5.2.3 Property Cardinality Restrictionso47
owl:onClassOWL 2 Primer 5.2.3 Property Cardinality Restrictionso47

OWL 2: properties

OWL 2: individuals, datatypes, the ontology

Metadata: Dublin Core and vann

OntoClean marks (the course's own annotations)

gUFO types

gist classes and patterns

BFO classes (through gistBFO)

SHACL, in the companion shapes

Reference

The sources

Everything this course teaches is defined or argued somewhere, and each lesson's header names the two places that matter most for it. python scripts/check_links.py fetches every document and confirms each anchor still lands.

Meaning, and the identifiers that carry it

Systematics and definitions

  • Evaluating ontological decisions with OntoClean. Guarino and Welty's short account: rigidity, identity, unity, dependence, and the subsumptions they forbid.
  • An overview of OntoClean. The longer treatment, in the Handbook on Ontologies. The backbone taxonomy is defined here.
  • gUFO. The Unified Foundational Ontology as a small OWL vocabulary: Kind, SubKind, Role, Phase, Category. Module 02 types the Trail's classes with it, as gistUFO types gist's.
  • ANSI/NISO Z39.19. Monolingual controlled vocabularies: the rules for preferred terms, synonyms, scope notes and hierarchy that SKOS was built to carry.
  • The OBO Foundry principles. Sixteen principles, each on a page: open, textual definitions, stability of meaning, versioning, a named person responsible.

Style guides and manuals

  • The gist Style Guide. Local names, labels, the annotation set and its cardinalities, no inverses, and eleven rules for restrictions. The most concrete style guide in print.
  • gist. Semantic Arts' minimalist upper ontology, CC-BY-4.0. Module 09 stands the Trail on it.
  • gistBFO. Forty subclass assertions from gist into BFO, each with a note saying why. Read as worked examples of alignment.
  • gistUFO. gist's classes typed as gUFO kinds, roles and phases. OntoClean applied to a real ontology.
  • Is an upper ontology useful?. Winstanley, ISKO UK 2023. A starter palette, a common vocabulary, a tool for thought, a hanger for data, and the Goldilocks level of abstraction. Module 09 opens with it.
  • The SemOps Manual. Running a semantic practice the way software is run. Chapters 2, 8, 9 and 12 are cited here: definitions are territory, the inner loop, the gate, and evidence-based versioning.
  • OQuaRE. The quality model module 10 measures with: fourteen metrics, each scored one to five against published thresholds.
  • The Ontology Quality Suite. The checks module 10 runs, by identifier. pip install ontology-quality-suite.

The languages

  • RDF 1.2 Schema. rdfs:Class, rdfs:subClassOf, rdfs:domain, rdfs:range, rdfs:label and rdfs:comment. The first rung of the ladder.
  • SKOS Reference. Concept schemes, labels, broader and narrower, notes, collections, mapping, and the integrity conditions.
  • SKOS Primer. The readable one, including the section on combining SKOS with OWL.
  • OWL 2 Primer. Start here for OWL. Every construct module 07 uses is introduced with an example.
  • OWL 2 Structural Specification and Functional-Style Syntax. The normative one: the datatype map, the global restrictions, and what OWL 2 DL requires of declarations.
  • OWL 2 Profiles. EL, QL and RL: what each gives up and what it gets in return.
  • SHACL. For the constraints an ontology should not try to express. The SHACL course covers it in full.