Model Creation#

The purpose of this tutorial is to learn about a few of the basic features of BuildingMOTIF by creating a Brick model similar to the Small Office Commercial Prototype Building model[1]. It assumes that the reader has some familiarity with the Turtle syntax[2] for Resource Description Framework (RDF) graphs[3].

Note

This tutorial has the following learning objectives:

  1. creating a BuildingMOTIF model instance and Model

  2. loading Libraries into a BuildingMOTIF instance

  3. adding to a model by evaluating Templates

Creating a Model#

BuildingMOTIF needs a database to store models, libraries, templates, ontologies, and other data. This database can be in-memory (which will be deleted when the process ends) or persistent. For simplicity, we will start with an in-memory database where bm represents a BuildingMOTIF instance.

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

Now that we have a BuildingMOTIF instance, we can create a Model. Creating a model requires importing the Model class, creating an RDF namespace to hold all of the entities in the model, and telling BuildingMOTIF to create a new model with that namespace. The namespace is a URL used to uniquely identify the model.

from rdflib import Namespace, RDF
from buildingmotif.dataclasses import Model

# create the namespace
BLDG = Namespace('urn:bldg/')

# Create the model! This will raise an exception if the namespace is not syntactically valid.
model = Model.create(BLDG, description="This is a test model for a simple building") 

We can print out the contents of the model by accessing the .graph property of the model object. Printing this out reveals that BuildingMOTIF has added a couple annotations to the model for us, but there is otherwise no metadata about the building itself:

print(model.graph.serialize())
@prefix owl: <http://www.w3.org/2002/07/owl#> .

<urn:bldg/> a owl:Ontology .

The model.graph object is just the RDFLib Graph[4] that stores the model. You can interact with the graph by adding triples with model.graph.add((subject, predicate, object)) but as we will soon see, BuildingMOTIF can automate some of that!

Loading Libraries#

Before we can add semantic metadata to the model, we need to import some Libraries. We import libraries by calling Library.load in BuildingMOTIF. Libraries can be loaded from directories containing .yml and .ttl files (for Templates and Shapes, respectively), or from ontology files directly. The code below contains an example of importing the brick library, which is simply the Brick ontology. This allows BuildingMOTIF to take advantage of the classes and relationships defined by Brick when validating the model. Loading in these definitions also allows other libraries to refer to Brick definitions. You can also ask a library for the names of the templates it defines, which we’ll limit to the first ten below.

# load a library
from buildingmotif.dataclasses import Library
brick = Library.load(ontology_graph="../../libraries/brick/Brick-full.ttl")

# print the first 10 templates
print("The Brick library contains the following templates:")
for template in brick.get_templates()[:10]:
    print(f"  - {template.name}")
/opt/hostedtoolcache/Python/3.11.10/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."))
The Brick library contains the following templates:
  - https://brickschema.org/schema/Brick#Frequency_Setpoint
  - https://brickschema.org/schema/Brick#Enable_Command
  - https://brickschema.org/schema/Brick#Low_Discharge_Air_Flow_Alarm
  - https://brickschema.org/schema/Brick#Chilled_Water_Meter
  - https://brickschema.org/schema/Brick#Cooling_Temperature_Setpoint
  - https://brickschema.org/schema/BrickShape#PhasesShape
  - https://brickschema.org/schema/Brick#Laminar_Flow_Air_Diffuser
  - https://brickschema.org/schema/Brick#On_Off_Command
  - https://brickschema.org/schema/Brick#Generation_Sensor
  - https://brickschema.org/schema/Brick#Water_Loop

Adding to a Model Manually#

Because Models are wrappers around RDF graphs, it is possible to manipulate those RDF graphs directly. BuildingMOTIF supports adding individual triples to models as well as adding whole graphs to models.

The RDF graph underlying a Model is accessible via the .graph attribute. This is an instance of RDFlib.Graph.

Adding Individual RDF Triples to a Model#

To add an RDF triple to a model, use the Model.graph.add() method:

# import this to make writing URIs easier
from buildingmotif.namespaces import BRICK, RDF

model.graph.add((BLDG["zone-temp"], RDF.type, BRICK.Zone_Air_Temperature_Sensor))
<Graph identifier=75f3c75d-773b-43df-8f0e-063205976bf0 (<class 'rdflib.graph.Graph'>)>

Importing RDF Graphs Into a Model#

It is also possible to import RDF graphs into a Model.

If you already have an in-memory RDFlib.Graph object available (e.g. through an Graph Ingress or loading in a Turtle file), just use Model.add_graph:

import rdflib
my_graph = rdflib.Graph()
# parse files:
# my_graph.parse("my_external_file.ttl")
# or parse raw strings:
my_graph.parse(data='''
@prefix bldg: <urn:ex/> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
bldg:Core_ZN-PSC_AC a brick:Air_Handler_Unit .
''')
model.add_graph(my_graph)

You can also use the Model.graph.parse method directly:

model.graph.parse(data='''
@prefix bldg: <urn:ex/> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
bldg:Core_ZN-PSC_AC a brick:Air_Handler_Unit .
''')
<Graph identifier=75f3c75d-773b-43df-8f0e-063205976bf0 (<class 'rdflib.graph.Graph'>)>

Adding to a Model with Templates#

Exploring a Template#

Templates make it easy to add metadata to a model without having to touch any RDF at all by generating parts of an RDF graph. This graph may represent a simple device like a fan, a complex entity like a chilled water system, or other parts of a building. The body of a template contains the basic structure of the graph representing that entity. Typically, a template defines several parameters that represent user-provided input necessary to create that entity in the model.

Let’s start with the template for an air handling unit (AHU) from the brick library, which we can fetch out of the library by referring to it by name. Then, let’s ask the template for the parameters it defines.

# import this to make writing URIs easier
from buildingmotif.namespaces import BRICK

# get template
ahu_template = brick.get_template_by_name(BRICK.AHU)

# print template parameters
print("The template has the following parameters:")
for param in ahu_template.parameters:
    print(f"  {param}")
The template has the following parameters:
  name

All templates have a mandatory name parameter. We can also print out the body of the template to see how those parameters will be used. Any URI that starts with a p: is a parameter.

print(ahu_template.body.serialize())
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:___param___#name> a brick:AHU .

Evaluating a Template#

Now that we know what the template looks like and what parameters it needs, we can evaluate the Template. Evaluation is the process of turning a template into a graph that can be added to a model. We accomplish this with the evaluate function, which takes a dictionary of bindings as an argument. Bindings relate a parameter in the template to a value (typically a URI in your building’s namespace). Put another way, the keys of the dictionary are the names of the parameters and the values of the dictionary are what will replace that parameter in the template’s body.

Let’s create an AHU named Core_ZN-PSZ_AC, which represents the core zone’s packaged single zone air conditioner in the Small Office model, and take a look at the result of evaluating the template.

ahu_name = "Core_ZN-PSC_AC"
ahu_binding_dict = {"name": BLDG[ahu_name]}
ahu_graph = ahu_template.evaluate(ahu_binding_dict)

# ahu_graph is just an instance of rdflib.Graph
print(ahu_graph.serialize())
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> a brick:AHU .

Adding Evaluated Templates to the Model#

Now that we have an RDF graph representing an AHU, let’s add it to the model using the add_graph function.

model.add_graph(ahu_graph)
print(model.graph.serialize())
@prefix bldg: <urn:ex/> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .

<urn:bldg/> a owl:Ontology .

<urn:bldg/Core_ZN-PSC_AC> a brick:AHU .

<urn:bldg/zone-temp> a brick:Zone_Air_Temperature_Sensor .

bldg:Core_ZN-PSC_AC a brick:Air_Handler_Unit .

Next, we’ll add some of the AHU’s components (a fan, a damper, and a cooling coil) to the model using Templates. Then we’ll connect the components to the AHU using RDFLib’s graph.add((subject, predicate, object)) method.

# templates
oa_ra_damper_template = brick.get_template_by_name(BRICK.Outside_Damper)
damper_template = brick.get_template_by_name(BRICK.Damper)
fan_template = brick.get_template_by_name(BRICK.Supply_Fan)
clg_coil_template = brick.get_template_by_name(BRICK.Cooling_Coil)

# add fan
fan_name = f"{ahu_name}-Fan"
fan_binding_dict = {"name": BLDG[fan_name]}
fan_graph = fan_template.evaluate(fan_binding_dict)
model.add_graph(fan_graph)

# add outdoor air/return air damper
oa_ra_damper_name = f"{ahu_name}-OutsideDamper"
oa_ra_damper_binding_dict = {"name": BLDG[oa_ra_damper_name]}
oa_ra_damper_graph = oa_ra_damper_template.evaluate(oa_ra_damper_binding_dict)
model.add_graph(oa_ra_damper_graph)

# add other damper
damper_name = f"{ahu_name}-Damper"
damper_binding_dict = {"name": BLDG[damper_name]}
damper_graph = damper_template.evaluate(damper_binding_dict)
model.add_graph(damper_graph)

# add clg coil
clg_coil_name = f"{ahu_name}-Clg_Coil"
clg_coil_binding_dict = {"name": BLDG[clg_coil_name]}
clg_coil_graph = clg_coil_template.evaluate(clg_coil_binding_dict)
model.add_graph(clg_coil_graph)

# connect zone-temp, fan, dampers, and clg coil to AHU
model.graph.add((BLDG[ahu_name], BRICK.hasPoint, BLDG["zone-temp"]))
model.graph.add((BLDG[ahu_name], BRICK.hasPart, BLDG[oa_ra_damper_name]))
model.graph.add((BLDG[ahu_name], BRICK.hasPart, BLDG[damper_name]))
model.graph.add((BLDG[ahu_name], BRICK.hasPart, BLDG[fan_name]))
model.graph.add((BLDG[ahu_name], BRICK.hasPart, BLDG[clg_coil_name]))

# you can add triples directly too
model.graph.add((BLDG[oa_ra_damper_name], BRICK.hasPoint, BLDG[oa_ra_damper_name + "Position"]))
model.graph.add((BLDG[oa_ra_damper_name + "Position"], RDF.type, BRICK.Damper_Position_Command))

# print model to confirm components were added and connected
print(model.graph.serialize())
@prefix bldg: <urn:ex/> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .

<urn:bldg/> a owl:Ontology .

<urn:bldg/Core_ZN-PSC_AC> a brick:AHU ;
    brick:hasPart <urn:bldg/Core_ZN-PSC_AC-Clg_Coil>,
        <urn:bldg/Core_ZN-PSC_AC-Damper>,
        <urn:bldg/Core_ZN-PSC_AC-Fan>,
        <urn:bldg/Core_ZN-PSC_AC-OutsideDamper> ;
    brick:hasPoint <urn:bldg/zone-temp> .

bldg:Core_ZN-PSC_AC a brick:Air_Handler_Unit .

<urn:bldg/Core_ZN-PSC_AC-Clg_Coil> a brick:Cooling_Coil .

<urn:bldg/Core_ZN-PSC_AC-Damper> a brick:Damper .

<urn:bldg/Core_ZN-PSC_AC-Fan> a brick:Supply_Fan .

<urn:bldg/Core_ZN-PSC_AC-OutsideDamper> a brick:Outside_Damper ;
    brick:hasPoint <urn:bldg/Core_ZN-PSC_AC-OutsideDamperPosition> .

<urn:bldg/Core_ZN-PSC_AC-OutsideDamperPosition> a brick:Damper_Position_Command .

<urn:bldg/zone-temp> a brick:Zone_Air_Temperature_Sensor .

Attention

If you want some additional practice, try writing some Python code that adds a Brick Heating_Coil to the model and connects it to the AHU.

Finally, let’s save the model to use in the next tutorial.

#save model
model.graph.serialize(destination="tutorial1_model.ttl")
<Graph identifier=75f3c75d-773b-43df-8f0e-063205976bf0 (<class 'rdflib.graph.Graph'>)>