Shapes and Templates#

Shapes and Templates interact in interesting ways in BuildingMOTIF. In this document, we explain the utility and function of these interactions.

Recall that a Shape (SHACL shape) is a set of conditions and constraints over RDF graphs, and a Template is a function that generates an RDF graph.

Converting Shapes to Templates#

BuildingMOTIF automatically converts shapes to templates. Evaluating the resulting template will generate a graph that validates against the shape.

When BuildingMOTIF loads a Library, it makes an attempt to find any shapes defined within it. The way this happens depends on how the library is loaded:

  • Loading library from directory or git repository: BuildingMOTIF searches for any RDF files in the directory (recursively) and loads them into a Shape Collection; loads any instances of sh:NodeShape in the union of these RDF files

  • Loading library from ontology file: loads all instances of sh:NodeShape in the provided graphc

Important

BuildingMOTIF only loads shapes which are instances of both sh:NodeShape and owl:Class. The assumption is that owl:Class-ified shapes could be “instantiated”.

Each shape is “decompiled” into components from which a Template can be constructed. The implementation of this decompilation is in the get_template_parts_from_shape method. BuildingMOTIF currently recognizes the following SHACL properties:

  • sh:property

  • sh:qualifiedValueShape

  • sh:node

  • sh:class

  • sh:targetClass

  • sh:datatype

  • sh:minCount / sh:qualifiedMinCount

  • sh:maxCount / sh:qualifiedMaxCount

BuildingMOTIF currently uses the name of the SHACL shape as the name of the generated Template. All other parameters (i.e., nodes corresponding to sh:property) are given invented names unless there is a sh:name attribute on the property shape.

Example#

Consider the following shape which has been loaded into BuildingMOTIF as part of a Library:

# myshapes.ttl
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix : <urn:example/> .

: a owl:Ontology .

:vav a sh:NodeShape, owl:Class ;
    sh:targetClass brick:Terminal_Unit ;
    sh:property [
        sh:path brick:hasPart ;
        sh:qualifiedValueShape [ sh:node :heating-coil ] ;
        sh:name "hc" ;
        sh:qualifiedMinCount 1 ;
    ] ;
    sh:property [
        sh:path brick:hasPoint ;
        sh:qualifiedValueShape [ sh:class brick:Supply_Air_Flow_Sensor ] ;
        sh:qualifiedMinCount 1 ;
    ] ;
    sh:property [
        sh:path brick:hasPoint ;
        sh:qualifiedValueShape [ sh:class brick:Supply_Air_Temperature_Sensor ] ;
        sh:name "sat" ;
        sh:qualifiedMinCount 1 ;
    ] ;
.

:heating-coil a sh:NodeShape, owl:Class ;
    sh:targetClass brick:Heating_Coil ;
    sh:property [
        sh:path brick:hasPoint ;
        sh:qualifiedValueShape [ sh:class brick:Position_Command ] ;
        sh:name "damper_pos" ; # will be used as the parameter name
        sh:qualifiedMinCount 1 ;
    ] ;
.

This code creates myshapes.ttl for you in the current directory.

with open("myshapes.ttl", "w") as f:
    f.write("""
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix : <urn:example/> .

: a owl:Ontology .

:vav a sh:NodeShape, owl:Class ;
    sh:targetClass brick:Terminal_Unit ;
    sh:property [
        sh:path brick:hasPart ;
        sh:qualifiedValueShape [ sh:node :heating-coil ] ;
        sh:name "hc" ;
        sh:qualifiedMinCount 1 ;
    ] ;
    sh:property [
        sh:path brick:hasPoint ;
        sh:qualifiedValueShape [ sh:class brick:Supply_Air_Flow_Sensor ] ;
        sh:qualifiedMinCount 1 ;
    ] ;
    sh:property [
        sh:path brick:hasPoint ;
        sh:qualifiedValueShape [ sh:class brick:Supply_Air_Temperature_Sensor ] ;
        sh:name "sat" ;
        sh:qualifiedMinCount 1 ;
    ] ;
.

:heating-coil a sh:NodeShape, owl:Class ;
    sh:targetClass brick:Heating_Coil ;
    sh:property [
        sh:path brick:hasPoint ;
        sh:qualifiedValueShape [ sh:class brick:Position_Command ] ;
        sh:name "damper_pos" ; # will be used as the parameter name
        sh:qualifiedMinCount 1 ;
    ] ;
.
""")

If this was in a file myshapes.ttl, we would load it into BuildingMOTIF as follows:

from buildingmotif import BuildingMOTIF
from buildingmotif.dataclasses import Library

# in-memory instance
bm = BuildingMOTIF("sqlite://")

# load library
brick = Library.load(ontology_graph="https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl")
lib = Library.load(ontology_graph="myshapes.ttl")
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pyshacl/extras/__init__.py:46: Warning: Extra "js" is not satisfied because requirement pyduktape2 is not installed.
  warn(Warning(f"Extra \"{extra_name}\" is not satisfied because requirement {req} is not installed."))
2024-09-13 21:58:12,123 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7627 in libraries []
2024-09-13 21:58:12,127 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7621 in libraries []
2024-09-13 21:58:12,132 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7625 in libraries []
2024-09-13 21:58:12,136 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7629 in libraries []
2024-09-13 21:58:12,140 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7623 in libraries []
2024-09-13 21:58:12,145 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7619 in libraries []
2024-09-13 21:58:12,150 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7699 in libraries []
2024-09-13 21:58:12,154 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7697 in libraries []
2024-09-13 21:58:12,158 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7701 in libraries []
2024-09-13 21:58:12,162 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7705 in libraries []
2024-09-13 21:58:12,166 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7703 in libraries []
2024-09-13 21:58:12,171 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7591 in libraries []
2024-09-13 21:58:12,175 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7589 in libraries []
2024-09-13 21:58:12,179 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7585 in libraries []
2024-09-13 21:58:12,183 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7583 in libraries []
2024-09-13 21:58:12,187 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7581 in libraries []
2024-09-13 21:58:12,192 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7587 in libraries []
2024-09-13 21:58:12,197 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7638 in libraries []
2024-09-13 21:58:12,200 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7636 in libraries []
2024-09-13 21:58:12,206 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7642 in libraries []
2024-09-13 21:58:12,210 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7644 in libraries []
2024-09-13 21:58:12,215 | root |  WARNING: Warning: could not find dependee n2d7afe5551134f8d91d627e13fdd833eb7640 in libraries []

Once the library has been loaded, all of the shapes have been turned into templates. We can load the template by name (using its full URI from the shape) as if it were defined explicitly:

# reading the template out by name
template = lib.get_template_by_name("urn:example/vav")

# dump the body of the template
print(template.body.serialize())
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:___param___#name> a brick:Terminal_Unit,
        <urn:example/vav> ;
    brick:hasPart <urn:___param___#hc0> ;
    brick:hasPoint <urn:___param___#p13>,
        <urn:___param___#sat0> .

<urn:___param___#hc0> a <urn:example/heating-coil> .

As with other templates, we often want to inline all dependencies to get a sense of what metadata will be added to the graph.

# reading the template out by name
template = lib.get_template_by_name("urn:example/vav").inline_dependencies()

# dump the body of the template
print(template.body.serialize())
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:___param___#name> a brick:Terminal_Unit,
        <urn:example/vav> ;
    brick:hasPart <urn:___param___#hc0> ;
    brick:hasPoint <urn:___param___#p13>,
        <urn:___param___#sat0> .

<urn:___param___#hc0> a brick:Heating_Coil,
        <urn:example/heating-coil> ;
    brick:hasPoint <urn:___param___#hc0-damper_pos0> .

Observe that the generated template uses the sh:name property of each property shape to inform the paramter name. If this is not provided (e.g. for the brick:Supply_Air_Flow_Sensor property shape), then a generated parameter will be used.