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()
../../_images/607f3134e916e129c3d25b9f894582e2dca7c3132805b73de8e287f4c36087c3.png
# 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()
../../_images/101bc8cbadd570bff55b43e789af7b2ca76343bfa1d603866e1430d0e28bcf36.png
# 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()
../../_images/9d25f93b29e19dbc0f22f2002f575576d96907d39b97b2b0aed11ea2cc37da28.png

Next Steps

You can further explore:

  • Adjusting building parameters in objects.csv

  • Incorporating 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