HVAC: 7R2C
This notebook demonstrates how to use a Resistance-Capacitance (RC) thermal model to simulate indoor temperatures and HVAC loads for multiple buildings. It is modeled after the VDI norm 6007.
Imports
Import required libraries and set visualization defaults.
import os
import matplotlib.pyplot as plt
import pandas as pd
from entise.constants import Columns as Cols
from entise.core.generator import Generator as TSGen
%matplotlib inline
Load Data
We load building parameters from objects.csv and simulation data from the data folder.
cwd = '.' # Current working directory: change if your kernel is not running in the same folder
objects = pd.read_csv(os.path.join(cwd, 'objects.csv'))
data = {}
common_data_folder = "../common_data"
for file in os.listdir(os.path.join(cwd, common_data_folder)):
if file.endswith(".csv"):
name = file.split(".")[0]
data[name] = pd.read_csv(os.path.join(os.path.join(cwd, common_data_folder, file)), parse_dates=True)
data_folder = 'data'
for file in os.listdir(os.path.join(cwd, data_folder)):
if file.endswith('.csv'):
name = file.split('.')[0]
data[name] = pd.read_csv(os.path.join(os.path.join(cwd, data_folder, file)), parse_dates=True)
print('Loaded data keys:', list(data.keys()))
print(objects)
Loaded data keys: ['validation_weather', 'weather', 'validation_windows', 'windows']
Instantiate and Configure Model
Initialize the time series generator and configure it with building objects.
gen = TSGen()
gen.add_objects(objects)
Run the Simulation
Generate sequential HVAC load and indoor temperature time series for each building.
summary, df = gen.generate(data, workers=1)
100%|██████████| 9/9 [00:02<00:00, 3.78it/s]
Results Summary
Below is a summary of the annual heating and cooling demands (in kWh/a) and peak loads (kW).
print("Summary:")
summary_kwh = (summary / 1000).round(0).astype(int)
summary_kwh.rename(columns=lambda x: x.replace("[W]", "[kW]").replace("[Wh]", "[kWh]"), inplace=True)
print(summary_kwh.to_string())
Summary:
heating:demand[kWh] heating:load_max[kW] cooling:demand[kWh] cooling:load_max[kW]
SFH_low 26147 14 6837 12
SFH_mid 11097 10 23273 16
SFH_high 7840 9 34429 17
DFH_low 46326 19 2684 12
DFH_mid 20316 14 16348 16
DFH_high 14717 12 27217 18
MFH_low 87407 34 2661 18
MFH_mid 43995 22 8925 17
MFH_high 34215 20 18272 20
Visualization of Results
Visualize indoor temperature, heating, and cooling loads for a selected building.
# Select building ID to visualize
building_id = summary.index[0] # Change index to visualize different buildings
building_data = df[building_id]['hvac']
# Figure 1: Indoor & Outdoor Temperature and Solar Radiation (GHI)
fig, ax1 = plt.subplots(figsize=(15, 6))
# Solar radiation plot (GHI) with separate y-axis
ax2 = ax1.twinx()
ax2.plot(
building_data.index, data["weather"][Cols.SOLAR_GHI],
label="Solar Radiation (GHI)", color="tab:orange", alpha=0.3
)
ax2.set_ylabel("Solar Radiation (W/m²)")
ax2.legend(loc="upper right")
ax2.set_ylim(-250, 1000)
# Temperature plot
ax1.plot(building_data.index, data["weather"][f"{Cols.TEMP_AIR}@2m"], label="Outdoor Temp", color="tab:cyan", alpha=0.7)
ax1.plot(building_data.index, building_data[Cols.TEMP_IN], label="Indoor Temp", color="tab:blue")
ax1.set_ylabel("Temperature (°C)")
ax1.set_title(f"Building ID: {building_id} - Temperatures and Solar Radiation")
ax1.legend(loc="upper left")
ax1.grid(True)
ax1.set_ylim(-10, 40)
ax1.set_zorder(ax2.get_zorder() + 1)
ax1.patch.set_visible(False) # required to see through ax1 to ax2
plt.tight_layout()
plt.show()
# Figure 2: Heating and Cooling Loads
fig, ax = plt.subplots(figsize=(14, 5))
heating_MWh = summary.loc[building_id, "heating:demand[Wh]"] / 1e6
cooling_MWh = summary.loc[building_id, "cooling:demand[Wh]"] / 1e6
(line1,) = ax.plot(
building_data.index,
building_data["heating:load[W]"],
label=f"Heating: {heating_MWh:.1f} MWh",
color="tab:red",
alpha=0.8,
)
(line2,) = ax.plot(
building_data.index,
building_data["cooling:load[W]"],
label=f"Cooling: {cooling_MWh:.1f} MWh",
color="tab:cyan",
alpha=0.8,
)
# Create the combined legend in the upper left corner
ax.set_ylabel("Load (W)")
ax.set_title(f"Building ID: {building_id} - HVAC Loads")
ax.legend()
ax.grid(True)
plt.tight_layout()
plt.show()
# Figure 3: Outdoor Temperature with Heating & Cooling Loads
fig, ax1 = plt.subplots(figsize=(15, 6))
# Plot outdoor temperature on left y-axis
air_temp = data["weather"][f"{Cols.TEMP_AIR}@2m"]
ax1.plot(building_data.index, air_temp
, label="Outdoor Temp", color="tab:cyan", alpha=0.7)
ax1.set_ylabel("Outdoor Temp (°C)")
ax1.set_ylim(air_temp.min().round() - 2, air_temp.max().round() + 2)
# Create second y-axis for loads
ax2 = ax1.twinx()
ax2.plot(building_data.index, building_data["heating:load[W]"], label="Heating Load", color="tab:red", alpha=0.8)
ax2.plot(building_data.index, building_data["cooling:load[W]"], label="Cooling Load", color="tab:blue", alpha=0.8)
ax2.set_ylabel("HVAC Load (W)")
ax2.set_ylim(
min(building_data["heating:load[W]"].min(), building_data["cooling:load[W]"].min()) * 1.1,
max(building_data["heating:load[W]"].max(), building_data["cooling:load[W]"].max()) * 1.1,
)
# Combine legends from both axes
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")
ax1.set_title(f"Building ID: {building_id} - Outdoor Temp & HVAC Loads")
ax1.grid(True)
fig.tight_layout()
plt.show()
Next Steps
You can further explore:
Adjusting building parameters in
objects.csvIncorporating or excluding additional data (e.g., internal gains, solar gains)
Investigate how different ventilation strategies impact a buildings energy demand (ventilation)
Automating analysis for larger building datasets