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#

When Loading a Library#

BuildingMOTIF can automatically convert shapes to templates when loading a Library. 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.

This feature can be disabled by setting infer_templates=False when calling Library.load

From Shape Collections#

It is also possible to convert the shapes defined in a Shape Collection to templates. This is done by calling the infer_templates method on the Shape Collection. If infer_templates is True when calling Library.load, then BuildingMOTIF will automatically call infer_templates on the Shape Collection within that Library.

Being able to call infer_templates on a Shape Collection is useful when you have a graph of shapes that you programmatically created or loaded without packaging them in a Library.

from buildingmotif import BuildingMOTIF
from buildingmotif.dataclasses import Library, ShapeCollection

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

my_shapes_source = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix ex: <http://example.org/> .

ex:SimpleShape a sh:NodeShape, owl:Class ;
    sh:property [
        sh:path ex:hasPoint ;
        sh:qualifiedValueShape [ sh:class brick:Sensor ] ;
        sh:qualifiedMinCount 1 ;
    ] .
"""

# create a ShapeCollection to hold the shapes
my_shapes = ShapeCollection.create()
my_shapes.graph.parse(data=my_shapes_source, format="ttl")

# create a Library to hold the generated templates
lib = Library.create("my-library")
my_shapes.infer_templates(lib)

print(lib.get_templates())
[Template(_id=1, _name='http://example.org/SimpleShape', body=<Graph identifier=78ca2727-2685-4197-ad7b-5e53414d79ab (<class 'rdflib.graph.Graph'>)>, optional_args=[], _bm=<buildingmotif.building_motif.building_motif.BuildingMOTIF object at 0x7f6de4309c10>)]

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.13/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."))
2025-06-16 23:31:29,001 | root |  WARNING: An ontology could not resolve a dependency on https://w3id.org/rec/brickpatches (Name: https://w3id.org/rec/brickpatches). Check this is loaded into BuildingMOTIF
2025-06-16 23:31:29,002 | root |  WARNING: An ontology could not resolve a dependency on https://brickschema.org/schema/Brick/ref (Name: https://brickschema.org/schema/Brick/ref). Check this is loaded into BuildingMOTIF
2025-06-16 23:31:29,003 | root |  WARNING: An ontology could not resolve a dependency on https://brickschema.org/schema/1.3/Brick (Name: https://brickschema.org/schema/1.3/Brick). Check this is loaded into BuildingMOTIF
2025-06-16 23:31:29,004 | root |  WARNING: An ontology could not resolve a dependency on http://datashapes.org/dash (Name: http://datashapes.org/dash). Check this is loaded into BuildingMOTIF
2025-06-16 23:31:29,004 | root |  WARNING: An ontology could not resolve a dependency on http://qudt.org/3.1.0/vocab/quantitykind (Name: http://qudt.org/3.1.0/vocab/quantitykind). Check this is loaded into BuildingMOTIF
2025-06-16 23:31:29,005 | root |  WARNING: An ontology could not resolve a dependency on http://data.ashrae.org/bacnet/2020 (Name: http://data.ashrae.org/bacnet/2020). Check this is loaded into BuildingMOTIF
2025-06-16 23:31:29,006 | root |  WARNING: An ontology could not resolve a dependency on http://qudt.org/3.1.0/vocab/unit (Name: http://qudt.org/3.1.0/vocab/unit). Check this is loaded into BuildingMOTIF
2025-06-16 23:31:29,007 | root |  WARNING: An ontology could not resolve a dependency on https://w3id.org/rec/recimports (Name: https://w3id.org/rec/recimports). Check this is loaded into BuildingMOTIF

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___#p14>,
        <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___#p14>,
        <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.