Step 3: SPARQL Query & qualify Function¶
Learn SPARQL basics and implement building qualification.
1. Understanding SPARQL for Brick¶
SPARQL queries RDF data (Brick models use RDF).
Basic Pattern:
Example - Find all temperature sensors:
Key Patterns:
- ?variable - Variable (like wildcards)
- rdf:type - "is a type of"
- rdfs:subClassOf* - Includes subclasses
- brick:hasPart - System/Loop has point (used by Hot_Water_Loop, Hot_Water_System)
- brick:hasPoint - Equipment has point (used by Boiler, Pump, Weather_Station)
- FILTER() - Filter results
Learn More: - Brick Docs: https://docs.brickschema.org/ - SPARQL Tutorial: https://www.w3.org/TR/sparql11-query/ - Brick Studio: https://brickstudio.io/
2. Visualize System Architecture¶
Before writing SPARQL, understand your target system architecture.
Reference Diagrams: See docs/Figures/Development_Guide/ for common patterns:
Pattern 1: Boiler System¶

- Equipment: Boiler, Hot_Water_Loop
- Sensors: Leaving/Entering Temperature Sensors
- Relationships: Loop
hasPartsensors
Pattern 2: District System¶

- Equipment: Heat_Exchanger, Hot_Water_Loop
- Sensors: Supply/Return Temperature Sensors
- Relationships: Loop
hasPartsensors
How to Use Diagrams:
1. Identify equipment (rectangles) → Use in ?equipment rdf:type brick:Hot_Water_Loop
2. Identify sensors (circles) → Use in ?sensor rdf:type brick:Temperature_Sensor
3. Trace relationships (arrows) → Use in ?equipment brick:hasPart ?sensor
Understanding Point-Equipment Relationships¶
Before writing SPARQL queries, you need to know which points are connected to which equipment and the relationship type used. This information comes from sensor_to_brick_mapping.yaml.
Important: Relationship Types
- Equipment (Boiler, Pump, Weather_Station) → Use brick:hasPoint to connect sensors
- Systems/Loops (Hot_Water_System, Primary_Loop, Secondary_Loop) → Use brick:hasPart to connect sensors
Complete Point-to-Equipment Mapping:
| Equipment/System | Relationship | Point Name | Point Type | Description |
|---|---|---|---|---|
| secondary_loop | hasPart |
sup |
Leaving_Hot_Water_Temperature_Sensor | Supply water temp entering building |
hasPart |
ret |
Entering_Hot_Water_Temperature_Sensor | Return water temp leaving building | |
hasPart |
flow |
Flow_Sensor | Flow rate entering building | |
hasPart |
dp |
Differential_Pressure_Sensor | End-of-line differential pressure | |
hasPart |
hw |
Thermal_Power_Sensor | Heating power supplied to building | |
hasPart |
gas_u |
Natural_Gas_Flow_Sensor | Gas consumption at utility meter | |
hasPart |
sup_stpt |
Hot_Water_Temperature_Setpoint | Supply temp setpoint | |
hasPart |
dp_stpt |
Differential_Pressure_Setpoint | Pressure setpoint | |
| primary_loop | hasPart |
supp |
Leaving_Hot_Water_Temperature_Sensor | Common supply temp (primary circuit) |
hasPart |
retp |
Entering_Hot_Water_Temperature_Sensor | Common return temp (primary circuit) | |
hasPart |
flowp |
Flow_Sensor | Hot water flow rate (primary circuit) | |
hasPart |
gas |
Natural_Gas_Flow_Sensor | Gas consumption at boiler plant | |
| boiler | hasPoint |
sup1-4 |
Leaving_Hot_Water_Temperature_Sensor | Boiler 1-4 outlet water temp |
hasPoint |
ret1-4 |
Entering_Hot_Water_Temperature_Sensor | Boiler 1-4 inlet water temp | |
hasPoint |
fire1-4 |
Firing_Rate_Sensor | Boiler 1-4 firing rate | |
| pump | hasPoint |
pmp1_pwr |
Power_Sensor | Pump 1 power consumption |
hasPoint |
pmp2_pwr |
Power_Sensor | Pump 2 power consumption | |
hasPoint |
pmp1_spd |
Speed_Command | Pump 1 speed command | |
hasPoint |
pmp2_spd |
Speed_Command | Pump 2 speed command | |
hasPoint |
pmp_spd |
Speed_Command | Main pump speed command | |
hasPoint |
pmp1_vfd |
VFD_Enable_Command | Pump 1 VFD enable | |
hasPoint |
pmp2_vfd |
VFD_Enable_Command | Pump 2 VFD enable | |
| weather_station | hasPoint |
t_out |
Outside_Air_Temperature_Sensor | Outdoor drybulb temperature |
| hot_water_system | hasPart |
enab |
Enable_Command | System enable/disable command |
hasPart |
oper |
Enable_Status | System operation status |
Key Summary by Type:
- Loops (use hasPart):
- Secondary Loop: 8 points
- Primary Loop: 4 points
- Hot Water System: 2 points
- Equipment (use hasPoint):
- Boiler: 12 points (4 boilers)
- Pump: 7 points
- Weather Station: 1 point
Example 1: For secondary loop (uses hasPart), SPARQL query:
SELECT ?loop ?supply ?return WHERE {
# Find secondary hot water loop (Loop uses hasPart)
?loop rdf:type brick:Hot_Water_Loop .
FILTER(CONTAINS(LCASE(STR(?loop)), "secondary"))
# Loops use brick:hasPart for sensors
?loop brick:hasPart ?supply .
?supply rdf:type brick:Leaving_Hot_Water_Temperature_Sensor .
?loop brick:hasPart ?return .
?return rdf:type brick:Entering_Hot_Water_Temperature_Sensor .
}
Example 2: For boiler sensors (uses hasPoint), SPARQL query:
SELECT ?boiler ?supply ?return WHERE {
# Find boiler equipment (Equipment uses hasPoint)
?boiler rdf:type brick:Boiler .
# Equipment use brick:hasPoint for sensors
?boiler brick:hasPoint ?supply .
?supply rdf:type brick:Leaving_Hot_Water_Temperature_Sensor .
?boiler brick:hasPoint ?return .
?return rdf:type brick:Entering_Hot_Water_Temperature_Sensor .
}
3. Write SPARQL Query¶
Add this function to find required sensors:
def find_required_sensors(graph):
"""
Find supply and return temperature sensors on hot water loop
Returns:
Tuple of (loop, supply_sensor, return_sensor) or None
"""
from hhw_brick.utils import query_sensors
query = """
SELECT ?loop ?supply ?return WHERE {
# Find hot water loop (could be secondary_loop or primary_loop)
?loop rdf:type/rdfs:subClassOf* brick:Hot_Water_Loop .
# Find supply sensor (part of loop)
# In our system: 'sup' on secondary_loop, 'supp' on primary_loop
?loop brick:hasPart ?supply .
?supply rdf:type/rdfs:subClassOf* brick:Leaving_Hot_Water_Temperature_Sensor .
# Find return sensor (part of loop)
# In our system: 'ret' on secondary_loop, 'retp' on primary_loop
?loop brick:hasPart ?return .
?return rdf:type/rdfs:subClassOf* brick:Entering_Hot_Water_Temperature_Sensor .
}
"""
results = query_sensors(graph, [], custom_query=query)
return results[0] if results else None
How it works:
1. Find any Hot_Water_Loop (secondary or primary)
2. Find supply sensor (Leaving temp) that's part of the loop
- brick:hasPart establishes the point-equipment relationship
3. Find return sensor (Entering temp) that's part of the loop
4. Return first match or None
Equipment-Point Relationships:
- Based on sensor_to_brick_mapping.yaml, points are assigned to equipment:
- Secondary loop: sup, ret, flow, dp, hw, gas_u, sup_stpt, dp_stpt
- Primary loop: supp, retp, flowp, gas
- Boiler: sup1-4, ret1-4, fire1-4
- Pump: pmp1_pwr, pmp2_pwr, pmp1_spd, pmp2_spd, pmp1_vfd, pmp2_vfd
- Weather station: t_out
4. Implement qualify()¶
Check if building has required sensors:
def qualify(brick_model_path):
"""
Check if building has required sensors
Args:
brick_model_path: Path to Brick model (.ttl file)
Returns:
Tuple of (qualified: bool, details: dict)
"""
print(f"\n{'='*60}")
print(f"QUALIFY: Checking required sensors")
print(f"{'='*60}\n")
from rdflib import Graph
# Load Brick model
g = Graph()
g.parse(brick_model_path, format="turtle")
# Find sensors
result = find_required_sensors(g)
if result:
loop, supply, return_sensor = result
print(f"[OK] Building qualified")
print(f" Loop: {loop}")
print(f" Supply: {supply}")
print(f" Return: {return_sensor}\n")
return True, {
"loop": str(loop),
"supply": str(supply),
"return": str(return_sensor)
}
else:
print(f"[FAIL] Building NOT qualified")
print(f" Missing: Supply and return sensors on hot water loop\n")
return False, {}
Returns:
- qualified=True + sensor details if building has sensors
- qualified=False + empty dict if missing sensors
5. Test qualify()¶
Create test_qualify.py:
"""Test qualification"""
from pathlib import Path
import sys
app_dir = Path(__file__).parent
sys.path.insert(0, str(app_dir.parent.parent.parent))
from hhw_brick.applications.my_first_app.app import qualify
# Test with a Brick model
fixtures = Path(__file__).parent.parent.parent.parent / "tests" / "fixtures"
model_file = fixtures / "Brick_Model_File" / "building_29.ttl"
if model_file.exists():
qualified, details = qualify(str(model_file))
if qualified:
print("✅ Test passed - building qualified")
print(f" Found sensors: {list(details.keys())}")
else:
print("⚠️ Building not qualified")
else:
print("⚠️ Test file not found")
Run:
6. Complete app.py So Far¶
Your app.py should now have:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""My First Application"""
import sys
from pathlib import Path
import yaml
app_dir = Path(__file__).parent
package_dir = app_dir.parent.parent.parent
sys.path.insert(0, str(package_dir))
__all__ = ["qualify", "analyze", "load_config"]
def load_config(config_file=None):
"""Load configuration from YAML file"""
# ...implementation from Step 2...
def find_required_sensors(graph):
"""Find required sensors using SPARQL"""
# ...implementation from this step...
def qualify(brick_model_path):
"""Check if building has required sensors"""
# ...implementation from this step...
# analyze() will be added in Step 4
Checkpoint¶
- Understand basic SPARQL patterns
-
find_required_sensors()implemented -
qualify()function implemented - Test finds qualified buildings
- Returns correct format: (bool, dict)
SPARQL Tips¶
Debug queries: 1. Start simple - find just the loop 2. Add one sensor at a time 3. Test in Brick Studio
Common patterns:
# Find by type
?equipment rdf:type brick:Hot_Water_Loop .
# Find with subclasses
?sensor rdf:type/rdfs:subClassOf* brick:Temperature_Sensor .
# Filter by name
FILTER(CONTAINS(LCASE(STR(?equipment)), "primary"))
# Relationships (use hasPart for Systems/Loops, hasPoint for Equipment)
?loop brick:hasPart ?point . # For Hot_Water_Loop, Hot_Water_System
?equipment brick:hasPoint ?point . # For Boiler, Pump, Weather_Station
Next Step¶
👉 Step 4: analyze Function - Part 1
In this step, you'll learn to write SPARQL queries to find sensors in Brick models and implement the qualify() function.
Goal of This Step¶
- Learn how to write SPARQL queries for Brick Schema
- Implement sensor discovery logic
- Create the
qualify()function to check if buildings have required sensors
Step 3.1: Understanding Brick Schema¶
Before writing SPARQL, understand how Brick models buildings:
Brick uses RDF triples (Subject - Predicate - Object):
Example:
building:loop1 rdf:type brick:Hot_Water_Loop .
building:loop1 brick:hasPart building:temp_sensor1 .
building:temp_sensor1 rdf:type brick:Temperature_Sensor .
Common Relationships:
- brick:hasPart - System/Loop has a point as part (used for Hot_Water_System, Hot_Water_Loop)
- brick:hasPoint - Equipment has a point (used for Boiler, Pump, Weather_Station)
- brick:isPointOf - Point belongs to equipment (inverse of hasPoint)
- rdf:type - Entity is of a certain type
- rdfs:subClassOf* - Includes subclasses (e.g., all types of temperature sensors)
Step 3.2: Learning SPARQL Basics¶
SPARQL is a query language for RDF data.
Basic SPARQL Pattern:
SELECT ?variable1 ?variable2 WHERE {
?variable1 predicate object .
?variable2 predicate ?variable1 .
}
Example - Find all temperature sensors:
Point-to-Equipment Relationships:
In our system, points are connected to equipment/systems via two different relationships:
- Systems/Loops use brick:hasPart (e.g., secondary_loop, primary_loop, hot_water_system)
- Equipment use brick:hasPoint (e.g., boiler, pump, weather_station)
This mapping is defined in sensor_to_brick_mapping.yaml. For example:
- secondary_loop hasPart: sup, ret, flow, dp, hw (Loops use hasPart)
- primary_loop hasPart: supp, retp, flowp, gas (Loops use hasPart)
- boiler hasPoint: sup1-4, ret1-4, fire1-4 (Equipment use hasPoint)
- pump hasPoint: pmp1_pwr, pmp2_pwr, pmp1_spd, pmp2_spd (Equipment use hasPoint)
SPARQL Resources: - Brick Schema Docs: https://docs.brickschema.org/ - SPARQL Tutorial: https://www.w3.org/TR/sparql11-query/ - Brick Studio (Visual Tool): https://brickstudio.io/
Step 3.3: Write a Simple SPARQL Query¶
Let's create a query to find temperature sensors on a hot water loop.
Add this function to app.py:
def find_required_sensors(graph):
"""
Find required sensors using SPARQL query
Args:
graph: RDF graph loaded from Brick model
Returns:
Tuple of (equipment, sensor1, sensor2) or None if not found
Example:
This query finds:
- A Hot_Water_Loop (secondary_loop or primary_loop)
- A Leaving_Hot_Water_Temperature_Sensor (supply) - part of that loop
- An Entering_Hot_Water_Temperature_Sensor (return) - part of that loop
Note:
Based on sensor_to_brick_mapping.yaml:
- secondary_loop has: sup (leaving), ret (entering)
- primary_loop has: supp (leaving), retp (entering)
"""
from hhw_brick.utils import query_sensors
# SPARQL query to find sensors
query = """
SELECT ?equipment ?supply_sensor ?return_sensor WHERE {
# Find a Hot Water Loop (secondary_loop or primary_loop)
?equipment rdf:type/rdfs:subClassOf* brick:Hot_Water_Loop .
# Find supply temperature sensor (part of the loop)
# This could be 'sup' on secondary_loop or 'supp' on primary_loop
?equipment brick:hasPart ?supply_sensor .
?supply_sensor rdf:type/rdfs:subClassOf* brick:Leaving_Hot_Water_Temperature_Sensor .
# Find return temperature sensor (part of the loop)
# This could be 'ret' on secondary_loop or 'retp' on primary_loop
?equipment brick:hasPart ?return_sensor .
?return_sensor rdf:type/rdfs:subClassOf* brick:Entering_Hot_Water_Temperature_Sensor .
}
"""
# Execute query using HHW Brick utility
results = query_sensors(graph, [], custom_query=query)
# Return first result or None
return results[0] if results else None
Understanding the Query:
- Find equipment:
?equipmentis a variable (like a wildcard)rdf:type/rdfs:subClassOf*means "is a type of or subtype of"-
Finds any Hot_Water_Loop
-
Find supply sensor:
- First line: loop has this sensor as a part
-
Second line: sensor is a Leaving (supply) temperature sensor
-
Find return sensor:
- Similar pattern for return temperature sensor
Step 3.4: Add Filters (Optional)¶
You can add filters to make queries more specific.
Example - Filter by name:
SELECT ?equipment ?supply_sensor ?return_sensor WHERE {
?equipment rdf:type/rdfs:subClassOf* brick:Hot_Water_Loop .
# Filter to only primary loops (name contains "primary")
FILTER(CONTAINS(LCASE(STR(?equipment)), "primary"))
?equipment brick:hasPart ?supply_sensor .
?supply_sensor rdf:type/rdfs:subClassOf* brick:Leaving_Hot_Water_Temperature_Sensor .
?equipment brick:hasPart ?return_sensor .
?return_sensor rdf:type/rdfs:subClassOf* brick:Entering_Hot_Water_Temperature_Sensor .
}
Filter Functions:
- CONTAINS(string, substring) - Check if string contains substring
- LCASE(string) - Convert to lowercase
- STR(uri) - Convert URI to string
Step 3.5: Implement qualify() Function¶
Now create the qualify() function that uses the SPARQL query.
Add this to app.py:
def qualify(brick_model_path):
"""
Check if building has required sensors
Args:
brick_model_path (str|Path): Path to Brick model file (.ttl)
Returns:
Tuple of (qualified: bool, details: dict)
- qualified: True if building has all required sensors
- details: Dictionary with sensor URIs if qualified, empty dict otherwise
Example:
>>> qualified, details = qualify("building_model.ttl")
>>> if qualified:
... print(f"Loop: {details['equipment']}")
... print(f"Supply sensor: {details['supply']}")
... print(f"Return sensor: {details['return']}")
"""
print(f"\n{'='*60}")
print(f"QUALIFY: Checking required sensors")
print(f"{'='*60}\n")
# Load Brick model
from rdflib import Graph
g = Graph()
g.parse(brick_model_path, format="turtle")
# Find sensors using SPARQL
result = find_required_sensors(g)
if result:
equipment, supply_sensor, return_sensor = result
# Building is qualified
print(f"[OK] Building qualified")
print(f" Equipment: {equipment}")
print(f" Supply Sensor: {supply_sensor}")
print(f" Return Sensor: {return_sensor}\n")
return True, {
"equipment": str(equipment),
"supply": str(supply_sensor),
"return": str(return_sensor)
}
else:
# Building not qualified
print(f"[FAIL] Building NOT qualified")
print(f" Missing: Required sensors (supply and return temperature on hot water loop)\n")
return False, {}
Understanding the Function:
- Print header: Inform user what's happening
- Load model: Parse Brick .ttl file into RDF graph
- Query sensors: Use SPARQL to find required sensors
- Check result:
- If found: return
Truewith sensor details - If not found: return
Falsewith empty dict
Step 3.6: Test qualify() Function¶
Create a test script to verify qualify() works.
Create test_qualify.py:
"""
Test script for qualify function
"""
from pathlib import Path
import sys
# Add parent directory to path
app_dir = Path(__file__).parent
sys.path.insert(0, str(app_dir.parent.parent.parent))
from hhw_brick.applications.my_first_app.app import qualify
def test_qualify():
"""Test qualification function"""
print("Testing qualify() function...\n")
# Path to test Brick models
fixtures = Path(__file__).parent.parent.parent.parent / "tests" / "fixtures" / "Brick_Model_File"
if not fixtures.exists():
print("Warning: Test fixtures not found. Skipping test.")
return
# Test on multiple buildings
for model_file in fixtures.glob("*.ttl"):
print(f"\n{'='*60}")
print(f"Testing: {model_file.name}")
print(f"{'='*60}")
try:
qualified, details = qualify(str(model_file))
if qualified:
print(f"✓ Building qualifies!")
print(f" Found sensors:")
for key, value in details.items():
print(f" {key}: {value.split('#')[-1]}")
else:
print(f"✗ Building does not qualify")
except Exception as e:
print(f"❌ Error: {e}")
print(f"\n{'='*60}")
print("Test complete!")
print(f"{'='*60}\n")
if __name__ == "__main__":
test_qualify()
Run the test:
Step 3.7: Your app.py Structure So Far¶
Your app.py should now have:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""My First Application"""
import sys
from pathlib import Path
import yaml
# Setup paths
app_dir = Path(__file__).parent
package_dir = app_dir.parent.parent.parent
sys.path.insert(0, str(package_dir))
__all__ = ["qualify", "analyze", "load_config"]
def load_config(config_file=None):
"""Load configuration from YAML file"""
# ... implementation from Step 2 ...
def find_required_sensors(graph):
"""Find required sensors using SPARQL query"""
# ... implementation from this step ...
def qualify(brick_model_path):
"""Check if building has required sensors"""
# ... implementation from this step ...
# analyze() will be added in next step
Checkpoint¶
Before proceeding, verify:
-
find_required_sensors()function is implemented - SPARQL query is correct and finds sensors
-
qualify()function is implemented - Function returns (bool, dict) tuple
- Test script runs and finds qualified buildings
Next Steps¶
✅ Qualification logic complete!
👉 Continue to Step 4: Write analyze Function - Part 1
SPARQL Tips¶
Debugging SPARQL Queries:
-
Start simple: First find just the equipment
-
Add one constraint at a time: Then add sensors
-
Add type filters: Finally filter sensor types
Common SPARQL Patterns:
# Find all points of a system/loop (hasPart)
?system brick:hasPart ?point .
# Find all points of equipment (hasPoint)
?equipment brick:hasPoint ?point .
# Find equipment a point belongs to (inverse)
?point brick:isPointOf ?equipment .
# Find by exact type
?entity rdf:type brick:SomeClass .
# Find by type including subclasses
?entity rdf:type/rdfs:subClassOf* brick:SomeClass .
# Optional relationships (won't fail if missing)
OPTIONAL { ?equipment brick:hasPart ?optionalPoint . }
# Filter by property value
?point brick:hasUnit "degreesCelsius" .
Common Issues¶
Issue: Query returns no results
Solution:
- Check sensor type names (case-sensitive!)
- Try without rdfs:subClassOf* first
- Use Brick Studio to explore the model
Issue: ImportError: cannot import name 'query_sensors'
Solution: Make sure you added sys.path.insert(0, ...) at top of file
Issue: Multiple results returned
Solution: Add filters or take first result: results[0]
Resources¶
Learn More About SPARQL for Brick: - Brick Schema Documentation: https://docs.brickschema.org/ - Brick Query Examples: https://docs.brickschema.org/query/index.html - SPARQL 1.1 Specification: https://www.w3.org/TR/sparql11-query/ - Brick Studio (Interactive): https://brickstudio.io/
Brick Sensor Types:
- Temperature: Leaving_Hot_Water_Temperature_Sensor, Entering_Hot_Water_Temperature_Sensor
- Flow: Water_Flow_Sensor, Hot_Water_Flow_Sensor
- Power: Thermal_Power_Sensor, Electric_Power_Sensor
- Full list: https://brickschema.org/ontology/