Model Correction#

The purpose of this tutorial is to learn how to fix a model that fails validation using BuildingMOTIF Templates to automate the correction process.

Setup#

Like the previous tutorial, we’ll create an in-memory BuildingMOTIF instance, load the model, and load some libraries. We’ll also load the manifest from the previous tutorial.

from rdflib import Namespace, URIRef
from buildingmotif import BuildingMOTIF
from buildingmotif.dataclasses import Model, Library, Template
from buildingmotif.namespaces import BRICK, RDF # import this to make writing URIs easier

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

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

# create the building model
model = Model.create(BLDG, description="This is a test model for a simple building")

# load libraries included with the python package
constraints = Library.load(ontology_graph="constraints/constraints.ttl")

# load libraries excluded from the python package (available from the repository)
brick = Library.load(ontology_graph="../../libraries/brick/Brick-subset.ttl")
g36 = Library.load(directory="../../libraries/ashrae/guideline36")

# load tutorial 2 model and manifest
model.graph.parse("tutorial2_model.ttl", format="ttl")
manifest = Library.load(ontology_graph="tutorial2_manifest.ttl")

# assign the manifest to our model
model.update_manifest(manifest.get_shape_collection())
/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-09-20 03:13:14,893 | root |  WARNING: An ontology could not resolve a dependency on urn:ashrae/g36 (Name: urn:ashrae/g36). Check this is loaded into BuildingMOTIF

Model Validation#

Let’s validate the model again to see what’s causing the failure.

validation_result = model.validate()
print(f"Model is valid? {validation_result.valid}")

# print reasons
for uri, diffset in validation_result.diffset.items():
    for diff in diffset:
        print(f" - {diff.reason()}")
2025-09-20 03:13:15,316 | buildingmotif.dataclasses.shape_collection |  WARNING: Could not resolve import of urn:ashrae/g36 from Libraries (Name: urn:ashrae/g36). Trying shape collections
2025-09-20 03:13:15,323 | buildingmotif.dataclasses.shape_collection |  WARNING: Could not resolve import of urn:ashrae/g36 from Libraries. Trying shape collections
Model is valid? False
 - urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Return_Air_Temperature_Sensor on path brick:hasPoint
 - urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of ns1:sa-fan on path https://brickschema.org/schema/Brick#hasPart
 - urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Heating_Command on path brick:hasPoint
 - urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Supply_Air_Temperature_Sensor on path brick:hasPoint
 - urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Cooling_Command on path brick:hasPoint
 - urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Outside_Air_Temperature_Sensor on path brick:hasPoint
 - urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Mixed_Air_Temperature_Sensor on path brick:hasPoint
 - urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Filter_Differential_Pressure_Sensor on path brick:hasPoint

We can get this information in a few different ways, too. For example, asking for all the entities which have failed validation:

for e in validation_result.get_broken_entities():
    print(e)
urn:bldg/Core_ZN-PSC_AC

We can also get all reasons a particular entity has failed validation:

for diff in validation_result.get_diffs_for_entity(BLDG["Core_ZN-PSC_AC"]):
    print(diff.reason())
urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Return_Air_Temperature_Sensor on path brick:hasPoint
urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of ns1:sa-fan on path https://brickschema.org/schema/Brick#hasPart
urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Heating_Command on path brick:hasPoint
urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Supply_Air_Temperature_Sensor on path brick:hasPoint
urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Cooling_Command on path brick:hasPoint
urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Outside_Air_Temperature_Sensor on path brick:hasPoint
urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Mixed_Air_Temperature_Sensor on path brick:hasPoint
urn:bldg/Core_ZN-PSC_AC expected 1 instance(s) of brick:Filter_Differential_Pressure_Sensor on path brick:hasPoint

Model Correction with Templates#

The model is failing because the AHU doesn’t have the minimum number of supply fans associated with it. We could add the fan explicitly by adding those triples to the model like we’ve done previously, but we can also ask BuildingMOTIF to generate new templates that explicitly prompt us for the missing information. We can also take a closer look at the first autogenerated template

# create a new library to hold these generated templates
generated_templates = Library.create("my-autogenerated-templates")

# loop through all results for the AHU
for diff in validation_result.get_diffs_for_entity(BLDG["Core_ZN-PSC_AC"]):
    diff.resolve(generated_templates)

# print some of the autogenerated template
for templ in generated_templates.get_templates():
    templ = templ.inline_dependencies()
    print(f"Name (autogenerated): {templ.name}")
    print(f"Parameters (autogenerated): {templ.parameters}")
    print("Template body (autogenerated):")
    print(templ.body.serialize())
    print('-' * 79)
Name (autogenerated): resolveCore_ZN-PSC_ACReturn_Air_Temperature_Sensor
Parameters (autogenerated): {'p141'}
Template body (autogenerated):
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> brick:hasPoint <urn:___param___#p141> .

<urn:___param___#p141> a brick:Return_Air_Temperature_Sensor .


-------------------------------------------------------------------------------
Name (autogenerated): resolveCore_ZN-PSC_ACsa-fan
Parameters (autogenerated): {'name', 'p139', 'p140', 'p138'}
Template body (autogenerated):
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> brick:hasPart <urn:___param___#name> .

<urn:___param___#name> a brick:Supply_Fan,
        <urn:ashrae/g36/4.8/sz-vav-ahu/sa-fan> ;
    brick:hasPoint <urn:___param___#p138>,
        <urn:___param___#p139>,
        <urn:___param___#p140> .

<urn:___param___#p138> a brick:Start_Stop_Command .

<urn:___param___#p139> a brick:Frequency_Command .

<urn:___param___#p140> a brick:Fan_Status .


-------------------------------------------------------------------------------
Name (autogenerated): resolveCore_ZN-PSC_ACHeating_Command
Parameters (autogenerated): {'p142'}
Template body (autogenerated):
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> brick:hasPoint <urn:___param___#p142> .

<urn:___param___#p142> a brick:Heating_Command .


-------------------------------------------------------------------------------
Name (autogenerated): resolveCore_ZN-PSC_ACSupply_Air_Temperature_Sensor
Parameters (autogenerated): {'p143'}
Template body (autogenerated):
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> brick:hasPoint <urn:___param___#p143> .

<urn:___param___#p143> a brick:Supply_Air_Temperature_Sensor .


-------------------------------------------------------------------------------
Name (autogenerated): resolveCore_ZN-PSC_ACCooling_Command
Parameters (autogenerated): {'p144'}
Template body (autogenerated):
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> brick:hasPoint <urn:___param___#p144> .

<urn:___param___#p144> a brick:Cooling_Command .


-------------------------------------------------------------------------------
Name (autogenerated): resolveCore_ZN-PSC_ACOutside_Air_Temperature_Sensor
Parameters (autogenerated): {'p145'}
Template body (autogenerated):
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> brick:hasPoint <urn:___param___#p145> .

<urn:___param___#p145> a brick:Outside_Air_Temperature_Sensor .


-------------------------------------------------------------------------------
Name (autogenerated): resolveCore_ZN-PSC_ACMixed_Air_Temperature_Sensor
Parameters (autogenerated): {'p146'}
Template body (autogenerated):
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> brick:hasPoint <urn:___param___#p146> .

<urn:___param___#p146> a brick:Mixed_Air_Temperature_Sensor .


-------------------------------------------------------------------------------
Name (autogenerated): resolveCore_ZN-PSC_ACFilter_Differential_Pressure_Sensor
Parameters (autogenerated): {'p147'}
Template body (autogenerated):
@prefix brick: <https://brickschema.org/schema/Brick#> .

<urn:bldg/Core_ZN-PSC_AC> brick:hasPoint <urn:___param___#p147> .

<urn:___param___#p147> a brick:Filter_Differential_Pressure_Sensor .


-------------------------------------------------------------------------------

In this case, the generated templates are fairly simple. They require an input for the name of the supply fan and the names of several missing points. We can loop through each of these generated templates and create the names. Here, we are creating arbitrary names for the points but in a real setting you would likely pull the equipment or point names from an external source like a Building Information Model or BACnet network[1] (see future tutorials for how to do this!) Another challenge is the fact that we already have a supply fan in the model. Here, we can take advantage of the fact that the name of the fan in the existing model are just the name of the AHU wtih the -Fan postfix. The name of the AHU is in the generated templates (see above) so we can just pull out the name of the AHU, add the postfix, and use that as the value for the name parameter.

If we just add the generated templates to the building model, we will probably pass validation but the entity names will have no significance to the building. It is highly recommended to use the template evaluation features (demonstrated below) to fill in the parameters with the “real” names of the entities as they appear in the building and/or building management system.

# use the name of the AHU from above as the base of our template names
ahu_name = "Core_ZN-PSC_AC"

# lookup for the name of the template to the name of the point or part
points_and_parts = {
    "resolveCore_ZN-PSC_ACMixed_Air_Temperature_Sensor": "-MAT",
    "resolveCore_ZN-PSC_ACFilter_Differential_Pressure_Sensor": "-FilterDPS",
    "resolveCore_ZN-PSC_ACCooling_Command": "-CCmd",
    "resolveCore_ZN-PSC_ACHeating_Command": "-HCmd",
    "resolveCore_ZN-PSC_ACOutside_Air_Temperature_Sensor": "-OAT",
    "resolveCore_ZN-PSC_ACSupply_Air_Temperature_Sensor": "-SAT",
    "resolveCore_ZN-PSC_ACReturn_Air_Temperature_Sensor": "-RAT",
    "resolveCore_ZN-PSC_ACsa-fan": "-Fan", # this is an existing fan in the model!
}

for templ in generated_templates.get_templates():
    templ = templ.inline_dependencies()

    suffix = points_and_parts[templ.name]

    # we know from the exploration above that each template has
    # 1 parameter which is the name of the missing item
    param = list(templ.parameters)[0]
    bindings = {
        param: BLDG[ahu_name + suffix],
    }
    thing = templ.evaluate(bindings)
    if isinstance(thing, Template):
        # there might be other parameters on a template. Invent names for them
        _, thing = thing.fill(BLDG)
    model.add_graph(thing)
/home/runner/work/BuildingMOTIF/BuildingMOTIF/buildingmotif/dataclasses/template.py:483: UserWarning: Parameters "p139, p140, p138" were not provided during evaluation
  warnings.warn(

We use the same code as before to ask BuildingMOTIF if the model is now valid:

validation_result = model.validate()
print(f"Model is valid? {validation_result.valid}")
# print reasons
for uri, diffset in validation_result.diffset.items():
    for diff in diffset:
        print(f" - {diff.reason()}")
2025-09-20 03:13:16,947 | buildingmotif.dataclasses.shape_collection |  WARNING: Could not resolve import of urn:ashrae/g36 from Libraries (Name: urn:ashrae/g36). Trying shape collections
2025-09-20 03:13:16,953 | buildingmotif.dataclasses.shape_collection |  WARNING: Could not resolve import of urn:ashrae/g36 from Libraries. Trying shape collections
Model is valid? True

We are still not finished. The sa-fan shape has its own requirements for necessary points. Let’s use the same process above to get templates we can fill out to repair the model

generated_templates_sf = Library.create("my-autogenerated-templates-sf")
for diff in validation_result.get_diffs_for_entity(BLDG["Core_ZN-PSC_AC-Fan"]):
    diff.resolve(generated_templates_sf)

# print some of the autogenerated template
for templ in generated_templates_sf.get_templates():
    templ = templ.inline_dependencies()
    print(f"Name (autogenerated): {templ.name}")
    print(f"Parameters (autogenerated): {templ.parameters}")
    print("Template body (autogenerated):")
    print(templ.body.serialize())
    print('-' * 79)

Use the names of these templates to build a lookup table for the point and part names.

sf_name = "Core_ZN-PSC_AC-Fan"

# lookup for the name of the template to the name of the point or part
points_and_parts = {
    "resolve_Core_ZN-PSC_AC-FanFrequency_Command": "-Freq",
    "resolve_Core_ZN-PSC_AC-FanStart_Stop_Command": "-StartStop",
    "resolve_Core_ZN-PSC_AC-FanFan_Status": "-Sts",
}
for templ in generated_templates_sf.get_templates():
    templ = templ.inline_dependencies()

    suffix = points_and_parts[templ.name]

    param = list(templ.parameters)[0]
    bindings = {
        param: BLDG[sf_name + suffix],
    }
    thing = templ.evaluate(bindings)
    model.add_graph(thing)

We can re-check the validation of the model now:

validation_result = model.validate()
print(f"Model is valid? {validation_result.valid}")
print(validation_result.report.serialize())

# print reasons
for uri, diffset in validation_result.diffset.items():
    for diff in diffset:
        print(f" - {diff.reason()}")
2025-09-20 03:13:18,416 | buildingmotif.dataclasses.shape_collection |  WARNING: Could not resolve import of urn:ashrae/g36 from Libraries (Name: urn:ashrae/g36). Trying shape collections
2025-09-20 03:13:18,422 | buildingmotif.dataclasses.shape_collection |  WARNING: Could not resolve import of urn:ashrae/g36 from Libraries. Trying shape collections
Model is valid? True
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

[] a sh:ValidationReport ;
    sh:conforms true .

Success! The model is valid with respect to the targeted use case, i.e. the model can support the high-performance sequences of operation for single zone VAV AHUs from ASHRAE Guideline 36. Let’s take a look at the validated model and save it for use in future tutorials.

# print model
print(model.graph.serialize())

#save model
model.graph.serialize(destination="tutorial3_model.ttl")
@prefix bldg: <urn:ex/> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix vav48: <urn:ashrae/g36/4.8/sz-vav-ahu/> .

<urn:bldg/> a owl:Ontology ;
    rdfs:comment "This is a test model for a simple building" .

<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-Htg_Coil>,
        <urn:bldg/Core_ZN-PSC_AC-OutsideDamper> ;
    brick:hasPoint <urn:bldg/Core_ZN-PSC_AC-CCmd>,
        <urn:bldg/Core_ZN-PSC_AC-FilterDPS>,
        <urn:bldg/Core_ZN-PSC_AC-HCmd>,
        <urn:bldg/Core_ZN-PSC_AC-MAT>,
        <urn:bldg/Core_ZN-PSC_AC-OAT>,
        <urn:bldg/Core_ZN-PSC_AC-RAT>,
        <urn:bldg/Core_ZN-PSC_AC-SAT>,
        <urn:bldg/zone-temp> .

bldg:Core_ZN-PSC_AC a brick:Air_Handler_Unit .

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

<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,
        vav48:sa-fan ;
    brick:hasPoint <urn:bldg/p138_195df506>,
        <urn:bldg/p139_e2d270e1>,
        <urn:bldg/p140_78db791a> .

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

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

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

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

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

<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/Core_ZN-PSC_AC-RAT> a brick:Return_Air_Temperature_Sensor .

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

<urn:bldg/p138_195df506> a brick:Start_Stop_Command .

<urn:bldg/p139_e2d270e1> a brick:Frequency_Command .

<urn:bldg/p140_78db791a> a brick:Fan_Status .

<urn:bldg/zone-temp> a brick:Zone_Air_Temperature_Sensor .
<Graph identifier=eeb89e28-3528-4e2d-9ca0-2c9433d3d2f1 (<class 'rdflib.graph.Graph'>)>