Building CFM Network
This example demonstrates the relationships, location, and changes related to airflow across different air systems within a building
Output
Show Code
import networkx as nx
from pyvis.network import Network
import pandas as pd
from bdx.core import BDX
from bdx.auth import UsernameAndPasswordAuthenticator
from bdx.types import TimeFrame, AggregationLevel
from bdx.components import ComponentFilter
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib.colors as mcolors
# BDX Credentials and Connection
BDX_URL = "http://yourURL.com" # Replace with your actual BDX URL
USERNAME = "YOUR_USERNAME"
PASSWORD = "YOUR_PASSWORD"
BUILDING_NAME = "YOUR_BUILDINGNAME"
# Connect to BDX
auth = UsernameAndPasswordAuthenticator(USERNAME, PASSWORD)
with BDX(BDX_URL, auth) as bdx:
buildings = bdx.buildings.list()
matching_buildings = [b for b in buildings if b.name.lower() == BUILDING_NAME.lower()]
if not matching_buildings:
print(f"No building found with the name: {BUILDING_NAME}")
exit()
BUILDING_ID = matching_buildings[0].componentInstanceId
BUILDING_NODE = f"Building: {BUILDING_NAME}"
print(f"\nSelected Building ID: {BUILDING_ID} for '{BUILDING_NAME}'")
# Retrieve all components
all_components = bdx.components.by_building(building_id=BUILDING_ID)
# Define AHUs of interest
AHU_NUMBERS = [1, 2, 3, 4, 6, 8]
ahu_names = {f"AHU_{num}": f"AHU {num}" for num in AHU_NUMBERS}
# Prepare dict to hold AHU -> list of VAV components
ahu_components = {ahu: [] for ahu in ahu_names}
# Manually filter for VAVs that belong to these AHUs
vav_components = [
comp for comp in all_components
if "VAV_" in comp.path.displayName
and any(comp.path.displayName.startswith(f"VAV_{ahu}_") for ahu in AHU_NUMBERS)
]
# Map each VAV displayName -> AHU_x
vav_to_ahu = {}
for vav in vav_components:
ahu_number = vav.path.displayName.split("_")[1] # Extract "1" from "VAV_1_xxx"
if f"AHU_{ahu_number}" in ahu_names:
vav_to_ahu[vav.path.displayName] = f"AHU_{ahu_number}"
ahu_components[f"AHU_{ahu_number}"].append(vav)
# Retrieve airFlow data for two timeframes
timeframe_current = TimeFrame.last_7_days()
timeframe_previous = TimeFrame.last_n_days(14) # last 14 days, but we'll only compare the first 7 to the last 7
properties = [{"componentPathId": vav.path.componentPathId, "propertyName": "airFlow"} for vav in vav_components]
# Get current week data (7 days)
trend_data_current = bdx.trending.retrieve_data(properties, timeframe_current, AggregationLevel.HOURLY)
df_current = trend_data_current.dataframe.fillna(0).set_index("time")
# Get previous week data (7 days before that)
trend_data_previous = bdx.trending.retrieve_data(properties, timeframe_previous, AggregationLevel.HOURLY)
df_previous = trend_data_previous.dataframe.fillna(0).set_index("time")
# Trim previous to same length as current (assumes same # of hours)
df_previous = df_previous.iloc[: len(df_current)]
# Aggregate total airflow for both timeframes
total_airflow_current = df_current.sum().to_dict() # e.g. {'compId_airFlow': totalCFM, ...}
total_airflow_previous = df_previous.sum().to_dict()
# -----------------------------------------------------------------------------
# 1) Compute all percent differences in one pass
# -----------------------------------------------------------------------------
all_percent_diffs = {}
all_current_airflows = {}
for vav_comp in vav_components:
comp_id = vav_comp.path.componentPathId
current_airflow = total_airflow_current.get(f"{comp_id}_airFlow", 0)
previous_airflow = total_airflow_previous.get(f"{comp_id}_airFlow", 0)
if previous_airflow != 0:
percent_diff = ((current_airflow - previous_airflow) / previous_airflow) * 100
else:
percent_diff = 0
# Store these results by VAV displayName
all_percent_diffs[vav_comp.path.displayName] = percent_diff
all_current_airflows[vav_comp.path.displayName] = current_airflow
# If everything is empty, avoid errors
if len(all_percent_diffs) == 0:
vmin, vmax = -1, 1
else:
vmin = min(all_percent_diffs.values())
vmax = max(all_percent_diffs.values())
# -----------------------------------------------------------------------------
# 2) Create the TwoSlopeNorm and colormap once
# -----------------------------------------------------------------------------
colormap = plt.get_cmap("RdBu_r") # Blue for negative, red for positive
norm = mcolors.TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
# For consistent node sizing, get the max current airflow across all VAVs
if len(all_current_airflows) == 0:
overall_max_airflow = 1
else:
overall_max_airflow = max(all_current_airflows.values())
# -----------------------------------------------------------------------------
# 3) Build the PyVis network
# -----------------------------------------------------------------------------
net = Network(height="1000px", width="100%", notebook=True, directed=False)
# Enable physics for dynamic spacing (avoids overlap)
net.barnes_hut(gravity=-7000, central_gravity=0.2, spring_length=50, spring_strength=0.03)
# Add building node
net.add_node(
BUILDING_NODE,
size=100,
color="#3e3e3e",
label=f"Building: Apex Pavilion",
physics=True,
font={"size": 50}
)
# Add AHU nodes & connect them to the building
for ahu, ahu_label in ahu_names.items():
net.add_node(
ahu,
size=50,
color="#f5d76e",
label=f"P1_{ahu_label}",
physics=True,
font={"size": 40}
)
net.add_edge(BUILDING_NODE, ahu)
# Get the VAV displayNames belonging to this AHU
ahu_vavs = [vav for vav, linked_ahu in vav_to_ahu.items() if linked_ahu == ahu]
# Sort them by absolute airflow change if you wish
# (We can derive airflow change from the stored percent or from the actual flows if needed.)
# For demonstration, let's just get the current minus previous from all_percent_diffs if needed.
# But you already had a dictionary "airflow_differences" if you want to re-use it.
# We'll do a quick inline approach:
def get_airflow_change(vav_disp_name):
# We can reconstruct from percent_diffs or better, do a direct sum again:
# But let's use the dictionary "airflow_differences" from your original code.
# If you still want that, we can compute it similarly:
comp = next((c for c in vav_components if c.path.displayName == vav_disp_name), None)
if comp is None:
return 0
comp_id = comp.path.componentPathId
cur_val = total_airflow_current.get(f"{comp_id}_airFlow", 0)
prev_val = total_airflow_previous.get(f"{comp_id}_airFlow", 0)
return (cur_val - prev_val)
sorted_vavs = sorted(
ahu_vavs,
key=lambda name: abs(get_airflow_change(name)),
reverse=True
)
# Loop through each VAV
for vav_disp_name in sorted_vavs:
# Grab the current airflow & percent diff we stored
current_airflow = all_current_airflows.get(vav_disp_name, 0)
percent_diff = all_percent_diffs.get(vav_disp_name, 0)
# Convert the percent_diff to a color
rgba_color = colormap(norm(percent_diff)) # e.g. negative => bluish, positive => reddish
hex_color = mcolors.to_hex(rgba_color)
# Scale VAV size by current airflow
node_size = 20 + (50 * (current_airflow / max(1, overall_max_airflow)))
# Add VAV node
net.add_node(
vav_disp_name,
size=node_size,
color=hex_color,
title=f"{vav_disp_name} - % Change: {percent_diff:.2f}%, Airflow: {current_airflow:.2f}",
physics=True,
font={"size": 30}
)
# Add edge from AHU -> VAV
net.add_edge(
ahu,
vav_disp_name,
width=1,
title=f"% Change: {percent_diff:.2f}%"
)
# Generate and save the network visualization
net.show("VAV_network.html")
print("\n✅ Network visualization saved as 'VAV_network.html'. Open it in a browser to view.")