The Shapeshift language¶
This page covers the whole language, from a file down to a single test. For the SHACL each construct exports to, see Shapeshift reference.
Files¶
A file starts with a header, then declares shapes and paths in any order:
-- Shapes for VAV boxes.
base <urn:example/vav/>
prefix brick: <https://brickschema.org/schema/Brick#>
doc "A VAV box."
shape vav for every brick:VAV {
brick:isFedBy exactly 1
}
base <iri>names the file’s shapes:vavis<urn:example/vav/vav>. Without a base, the build warns and uses a placeholderurn:shapeshift:IRI made from the file name.prefix name: <iri>works as in Turtle.rdf,rdfs,xsd,owl, andshare already declared.importbrings in another file or an RDF graph. See Imports.--starts a comment, which runs to the end of the line.doc "..."documents the next shape or line. Unlike a comment, a doc is exported and quoted in reports. Triple quotes ("""...""") allow several lines.
Inside braces, a line ends at a newline or a ;. Names use letters,
digits, _, and -: zone-temperature, SAT_AVG.
Shapes and targets¶
shape name { lines } defines a shape. A node conforms when every line
holds. for clauses say which nodes the shape checks:
shape vav
for every brick:VAV -- instances of a class
for every node with brick:hasDamper -- subjects of a property
for every value of brick:feeds -- objects of a property
for node ex:vav-7 -- one node
{
brick:hasPoint required
}
Two more targets:
for every node matching schecks the nodes that conform to shapes.for sparql "SELECT ?this WHERE { ... }"checks the nodes a query returns.
A shape with no for checks nothing by itself. It is a definition for
other shapes to use. In SHACL it is easy to target nodes by accident. In
Shapeshift, what a shape checks is always on its first line.
for every C and the test a C include instances of subclasses of
C, as in SHACL, as long as the rdfs:subClassOf triples are in the
validated graph.
Fields¶
Most lines are fields. A field gives a path, how many values it has, and tests on the values.
brick:isFedBy exactly 1, each a brick:AHU
brick:hasPoint at least 1 is a brick:Air_Flow_Sensor
rdfs:label required, each literal, each language "en"
A count limits the number of values:
Count |
Values |
|---|---|
|
at least one |
|
exactly |
|
at most one |
|
at least, or at most, |
|
from |
A field with no count can have any number of values.
Each count has one spelling. Write required, not at least 1, and
optional, not at most 1. The build points out the other spellings.
After the count come qualifiers, separated by commas:
each T: every value passes the testT. A node with no values passes too, so “one or more, all points” isrequired, each a brick:Point.at least n is T(andat most,exactly): count only the values that passT. Other values are allowed. For a shape, writematches:at least 1 matches damper.
The two are easy to mix up, so they use different words:
-- every point is a brick:Point
brick:hasPoint each a brick:Point
-- one point, at least, is an airflow sensor; the others can be anything
brick:hasPoint at least 1 is a brick:Air_Flow_Sensor
A value test always follows each or a qualified count. ex:p string
is an error; write ex:p each string.
Fields can also compare two properties, or check languages:
ex:start below ex:end -- each value of one below each of the other
ex:low up to ex:high -- ... or equal to it
rdfs:label same as skos:prefLabel -- the same set of values
ex:parent different from ex:child -- no value in common
rdfs:label unique language -- at most one label per language
Tests¶
A test is what each and at least n is check on each value. Every
test starts with a word that says what kind of test it is:
Test |
The value |
|---|---|
|
is an instance of the class, or of a subclass |
|
is an instance of one of the classes |
|
conforms to the shape |
|
conforms to these lines: a shape written in place |
|
is one of these nodes or literals |
|
is a literal of the datatype |
|
is that kind of node |
|
has a string of this length |
|
is in the range, bounds included |
|
is in the range, bounds excluded |
|
matches the regular expression |
|
is a string in one of these languages |
a and matches keep classes and shapes apart. each brick:AHU is
an error that suggests each a brick:AHU. each damper suggests
each matches damper.
Built-in datatypes are written as words: string, not
datatype xsd:string.
or after a lists classes. A value passes when it is an instance of
any of them. For alternatives that aren’t all classes, use any of (see
Frames).
brick:isFedBy exactly 1, each a brick:AHU or brick:RTU
brick:hasPoint at least 1 is a brick:Air_Flow_Sensor or brick:Air_Flow_Setpoint
Ranges work on numbers, dates, and times. Dates and times are written bare,
in XSD form, with an optional timezone (Z, +02:00):
2024-01-31is anxsd:date.2024-01-31T08:00:00Zis anxsd:dateTime.08:00:00is anxsd:time.
one of takes them too:
ex:commissioned optional, each date, each from 2000-01-01
ex:reading each above 2024-01-01T00:00:00Z below 2025-01-01T00:00:00Z
ex:opens each from 06:00:00 to 10:00:00
ex:holiday each one of 2024-12-25 2024-12-26
Both ends of a range must be the same kind, and only values of that kind
fall in it. No number is in a date range, and no xsd:dateTime either.
A date that doesn’t exist, like 2024-02-30, is an error.
Paths¶
A path is a property, or properties combined:
Path |
Follows |
|---|---|
|
one property, then the other |
|
either property |
|
the property backwards |
|
one or more steps, zero or more, zero or one |
|
parentheses group |
|
no step: the node itself |
Long paths repeat, so path names one for reuse. A path with parameters
is a template:
prefix brick: <https://brickschema.org/schema/Brick#>
path points = brick:hasPoint
path downstream(p = brick:hasPart?) = brick:feeds+/p
shape box {
points required
downstream(brick:hasPoint) at least 1 is a brick:Zone_Air_Temperature_Sensor
}
Names and composition¶
name: before a line names it. The name doesn’t change what the line
requires. It gives the line an identity:
It exports as a named property shape (
<urn:example/vav/vav.fed>).Reports and shape maps refer to it by name.
Other shapes can reuse it.
A field fits on one line. Its doc, message, and severity go on
the lines before it:
shape vav for every brick:VAV {
flow: brick:hasPoint at least 1 is a brick:Air_Flow_Sensor
doc "Dual-duct boxes are modelled as two VAVs."
message "A VAV must be fed by exactly one AHU."
fed: brick:isFedBy exactly 1, each a brick:AHU
unit: brick:hasUnit optional, each iri
}
To reuse a shape, write its name alone on a line. Its lines then hold here
too, but its targets don’t come along. shape.name reuses one named
line:
shape vav for every brick:VAV {
fed: brick:isFedBy exactly 1, each a brick:AHU
flow: brick:hasPoint at least 1 is a brick:Air_Flow_Sensor
}
shape reheat-vav for every brick:RVAV {
vav.fed -- only the AHU rule
reheat: brick:hasPoint at least 1 is a brick:Reheat_Valve_Command
}
is on a line tests the node itself. is a brick:Damper says the node
is a damper, and is iri says it is an IRI. A shape doesn’t need is:
alone on a line, it already applies to the node.
Groups¶
Specifications list equipment as points: a supply-air sensor, a return-air sensor, and so on. A group writes such a list along one path. Each row names a value and says what it is:
shape ahu {
is a brick:AHU
brick:hasPoint {
sat: brick:Supply_Air_Temperature_Sensor
rat: brick:Return_Air_Temperature_Sensor optional
doc "outdoor air"
oat: brick:Outside_Air_Temperature_Sensor
}
brick:hasPoint required { brick:Damper_Position_Command; brick:Frequency_Command }
brick:hasPart {
fan: brick:Supply_Fan { brick:hasPoint required { brick:Fan_Status } }
}
}
A row counts the values of its class, or that match its shape. Rows are
always a class or a shape, so they skip a and matches.
A row requires exactly one value by default.
A count after the path sets the default for every row. A count on a row sets its own.
C { lines }counts values that areCand satisfy the lines.A row’s name is optional, and its
docgoes on the line before it.
For two values of one class, write one row with exactly 2. Repeating
the row requires only one value, and the build warns about it.
With path points = brick:hasPoint, a group reads points { ... }.
Frames¶
Some rules are choices (“a damper command or a damper part”) or bans. Frames write these as blocks after a phrase that says how they combine:
any of { brick:hasPoint at least 1 is a brick:Damper_Position_Command }
{ brick:hasPart at least 1 is a brick:Damper }
exactly one of { brick:hasTag required } { rdfs:label required }
none of { brick:hasLocation required }
at least 2 of { brick:hasPart required } { brick:isPartOf required } { brick:feeds required }
at most 1 of { brick:hasTag required } { brick:hasLocation required } { brick:feeds required }
when { brick:hasPoint at least 1 is a brick:Economizer_Status }
then { brick:hasPoint at least 1 is a brick:Outside_Air_Damper }
Every line of a block must hold, so there is no
all of.at least 1 ofis writtenany of, andat most 0 ofisnone of.when C then Bmeans “ifCholds,Bmust”: “if it has X, it must have Y”.
There is no or between lines and no not. The first word of a frame
tells you what it means.
To choose among values instead of lines, use a test:
each one of ex:on ex:off.
Templates¶
Many rules differ only in a class or a number. A shape with parameters is a template for them. A parameter can have a default, which can use an earlier parameter:
prefix brick: <https://brickschema.org/schema/Brick#>
doc "At least `n` points of class `cls`."
shape has-point(cls, n = 1) { brick:hasPoint at least n is a cls }
doc "Between `min` and `max` points of class `cls`: exactly `min` by default."
shape has-points(cls, min = 1, max = min) {
brick:hasPoint at least min is a cls
brick:hasPoint at most max is a cls
}
shape vav for every brick:VAV {
has-point(brick:Air_Flow_Sensor)
temp: has-point(brick:Zone_Air_Temperature_Sensor, n = 2)
has-points(brick:Damper_Position_Command)
has-points(brick:Sensor, 2, max = 6)
}
Arguments are positional, then named. Each call is checked against the template. A missing argument, or a name where a number belongs, is an error that shows the template’s parameters.
On export, calls with the same arguments share one shape, which records the
call it came from. A labelled call (temp: has-point(...)) is exported
under its label instead: vav.temp.
Annotations¶
doc, message, and severity each go on the line before what they
annotate:
shape zone for every brick:HVAC_Zone {
severity warning
message "A zone should report its temperature."
brick:hasPoint at least 1 is a brick:Zone_Air_Temperature_Sensor
brick:hasPart required
}
severity (info, warning, or violation) and message
apply to the next line only. Here the temperature line is a warning, and a
missing part is still a violation. Before a shape, they apply to the
whole shape.
message speaks to whoever wrote the data. doc explains the line to
whoever reads the shapes next.
only these properties closes a shape. A node can then have no
properties the shape doesn’t name, other than rdf:type:
shape space for every brick:Space {
brick:hasPart each matches space
only these properties
}
Imports¶
import reads another Shapeshift file, or an RDF graph:
import "./sensors.shm" as sensors
import <https://brickschema.org/schema/1.4/Brick>
A .shm path is relative to the importing file.
With
as sensors, its names are writtensensors.temp-sensor.Without an alias, they are written bare.
Shapes keep the IRIs of the file that defines them.
An RDF import can be an ontology, a SHACL shapes graph, or both. Importing
an ontology catches typos: shifty shape check reports a class the
ontology doesn’t define, and suggests the nearest one that it does.
Imported shapes can be used with matches.
Imports are never fetched unless you allow it. --map IRI=PATH reads an
IRI from a local file, and --fetch downloads the rest.
Recursion¶
Buildings nest: a space has parts that are spaces. A shape can refer to
itself, directly or through other shapes. each matches is always
safe:
shape space for every brick:Space {
brick:hasPart each matches space
}
A loop must not pass an odd number of times through none of,
at most n of, an at most n is or at most n matches qualifier, or
a when condition. Such a loop has no consistent meaning, and the build
names it:
-- A recursive shape Shifty refuses, for the language guide.
base <urn:example/odd/>
prefix ex: <http://example.org/>
shape odd {
none of { ex:next required, each matches odd }
}
error: shape `odd` has no consistent meaning
--> odd.shm:5:1
5 | shape odd {
| ^
odd --> none of (not) (upper bound) (not) --> odd
this loop passes through 3 negations (an odd number); a node could be neither valid nor invalid
hint: recursion through `none of`, `at most ... is matches`, or a `when` condition must be balanced; `each matches` is always safe
error: 1 error in odd.shm
Recursive shapes export as legal SHACL, but the SHACL specification
doesn’t say what they mean. Shifty uses greatest-fixpoint semantics, and
export warns that other engines may differ.