Writing shapes

Experimental: this can change at any time

Shapeshift is experimental. The language, the shifty shape commands, the shifty.experimental.shapeshift Python module, and the shifty-experimental Rust crate can change in any release, without a deprecation period. Pin the Shifty version if you depend on them.

In this tutorial you write shapes for the VAV boxes in a building, validate a small building against them, and fix the one failure. You need the shifty command (see Install Shifty).

Write the shapes

Save this as vav.shm:

-- Shapes for the VAV boxes of a building.
base <urn:example/tutorial/>
prefix brick: <https://brickschema.org/schema/Brick#>

doc "A VAV box: fed by one AHU, with an airflow sensor and a damper."
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
  damper: brick:hasPart at least 1 matches damper
}

doc "A damper that can be commanded to a position."
shape damper {
  is a brick:Damper
  brick:hasPoint at least 1 is a brick:Damper_Position_Command
}

Line by line:

  • base sets the IRI the shapes are named under. vav becomes <urn:example/tutorial/vav>.

  • prefix works as in Turtle. -- starts a comment.

  • doc "..." documents the next shape. Unlike a comment, it is kept: it is exported as rdfs:comment and shown in reports.

  • shape vav for every brick:VAV defines a shape and says which nodes it checks. damper has no for, so it checks nothing by itself. vav uses it.

  • Each line in braces is one requirement, and all must hold. brick:isFedBy exactly 1, each a brick:AHU means one brick:isFedBy value, and it is a brick:AHU.

  • at least 1 is a brick:Air_Flow_Sensor counts only the points that are air-flow sensors. The box can have other points too.

  • a tests a class and matches tests a shape, so at least 1 matches damper counts parts that match damper.

  • fed:, flow:, and damper: name their lines. Names are optional. Reports and the exported SHACL use them.

  • is a brick:Damper is a requirement on the node itself.

Check them

shifty shape check builds the shapes without any data, so you catch mistakes before validating anything:

ok: vav.shm builds

Errors point at their line. typo.shm is vav.shm with exactly misspelled:

error: unexpected `exactl` in a field line
  --> typo.shm:7:22
  7 |   fed: brick:isFedBy exactl 1, each a brick:AHU
    |                      ^
  hint: for values that are `exactl`, write `each matches exactl`; to follow another property, write a path like `ex:p/exactl`
error: 1 error in typo.shm

Validate a building

This building has two VAV boxes. The damper of vav-2 has no position command:

@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix ex: <urn:example/building/> .

ex:ahu-1 a brick:AHU .

ex:vav-1 a brick:VAV ;
    brick:isFedBy ex:ahu-1 ;
    brick:hasPoint ex:vav-1-flow ;
    brick:hasPart ex:vav-1-damper .
ex:vav-1-flow a brick:Air_Flow_Sensor .
ex:vav-1-damper a brick:Damper ;
    brick:hasPoint ex:vav-1-damper-cmd .
ex:vav-1-damper-cmd a brick:Damper_Position_Command .

# vav-2's damper has no position command.
ex:vav-2 a brick:VAV ;
    brick:isFedBy ex:ahu-1 ;
    brick:hasPoint ex:vav-2-flow ;
    brick:hasPart ex:vav-2-damper .
ex:vav-2-flow a brick:Air_Flow_Sensor .
ex:vav-2-damper a brick:Damper .
<urn:example/building/vav-2> fails vav  (vav.shm:6)
  "A VAV box: fed by one AHU, with an airflow sensor and a damper."
  damper: brick:hasPart at least 1 matches damper  (vav.shm:9)
    at least 1 value(s) matching `instance of brick:Damper and ∃[1..] brick:hasPoint . instance of brick:Damper_Position_Command` required along brick:hasPart, found 0

does not conform: 1 node failed

The report names the node, the shape, and the line that failed, and points into vav.shm. It quotes the shape’s doc, and says what the line needed and what it found. vav-1 conforms, so it isn’t listed.

The command exits with status 1, so a CI job fails on it.

To fix the building, give the damper its command:

ex:vav-2-damper a brick:Damper ;
    brick:hasPoint ex:vav-2-damper-cmd .
ex:vav-2-damper-cmd a brick:Damper_Position_Command .

Export SHACL

Other SHACL engines don’t read .shm. shifty shape export writes the shapes as SHACL Turtle:

@prefix : <urn:example/tutorial/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix shapeshift: <https://shifty.gtf.fyi/ns/shapeshift#> .

:
    a owl:Ontology .

:vav
    a sh:NodeShape ;
    sh:property :vav.fed, :vav.flow, :vav.damper ;
    rdfs:comment "A VAV box: fed by one AHU, with an airflow sensor and a damper." ;
    sh:targetClass brick:VAV .

:vav.fed
    a sh:PropertyShape ;
    sh:path brick:isFedBy ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class brick:AHU .

:vav.flow
    a sh:PropertyShape ;
    sh:path brick:hasPoint ;
    sh:qualifiedValueShape [
        sh:class brick:Air_Flow_Sensor
    ] ;
    sh:qualifiedMinCount 1 .

:vav.damper
    a sh:PropertyShape ;
    sh:path brick:hasPart ;
    sh:qualifiedValueShape :damper ;
    sh:qualifiedMinCount 1 .

:damper
    a sh:NodeShape ;
    sh:property [
        sh:path brick:hasPoint ;
        sh:qualifiedValueShape [
            sh:class brick:Damper_Position_Command
        ] ;
        sh:qualifiedMinCount 1
    ] ;
    sh:class brick:Damper ;
    rdfs:comment "A damper that can be commanded to a position." .

Named lines become named property shapes (:vav.fed), so other shapes and reports can refer to them. each a brick:AHU became sh:class, and at least 1 is ... became a qualified value shape.

Format

shifty shape fmt prints a file in the standard layout, keeping comments and docs.

  • fmt -w rewrites the file.

  • fmt --check exits with status 1 if the file isn’t formatted. Use it in CI.

The same shapes in Python

When shapes come from data, it is easier to generate them. shifty.experimental.shapeshift builds the same module from Python:

from shifty.experimental import shapeshift as ss
from shifty.experimental.shapeshift import Shape

BRICK = ss.Namespace("brick", "https://brickschema.org/schema/Brick#")

m = ss.Module(
    "vav",
    base="urn:example/tutorial/",
    comment="Shapes for the VAV boxes of a building.",
)

m.vav = Shape(
    fed=BRICK.isFedBy.exactly(1).each(BRICK.AHU),
    flow=BRICK.hasPoint.at_least(1, BRICK.Air_Flow_Sensor),
    damper=BRICK.hasPart.at_least(1, m.damper),
    for_every=BRICK.VAV,
    doc="A VAV box: fed by one AHU, with an airflow sensor and a damper.",
)

m.damper = Shape(
    BRICK.Damper,
    BRICK.hasPoint.at_least(1, BRICK.Damper_Position_Command),
    doc="A damper that can be commanded to a position.",
)

print(m, end="")
-- Shapes for the VAV boxes of a building.
base <urn:example/tutorial/>
prefix brick: <https://brickschema.org/schema/Brick#>

doc "A VAV box: fed by one AHU, with an airflow sensor and a damper."
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
  damper: brick:hasPart at least 1 matches damper
}

doc "A damper that can be commanded to a position."
shape damper {
  is a brick:Damper
  brick:hasPoint at least 1 is a brick:Damper_Position_Command
}

The output matches vav.shm line for line.

Next steps