Heat Pump: Ruhnau

This notebook demonstrates how to use the Ruhnau method to generate heat pump COP time series for different heat pump types, heating systems, and domestic hot water (DHW).

The Ruhnau method is based on the paper by Ruhnau et al. (2019) “Time series of heat demand and heat pump efficiency for energy system modeling”.

Imports

Import required libraries and set visualization defaults.

import json
import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from entise.constants import SEP, Types
from entise.core.generator import Generator as TSGen

%matplotlib inline

Load Data

We load the heat pump 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 = {}
data_folder = "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)
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)
    elif file.endswith(".json"):
        name = file.split(".")[0]
        with open(os.path.join(os.path.join(cwd, data_folder, file)), "r") as f:
            data[name] = json.load(f)

print("Loaded data keys:", list(data.keys()))
print(objects)
Loaded data keys: ['weather', 'system']

Display Objects

Let’s take a look at the heat pump objects we’ve loaded.

# Display the objects
objects
id hp weather hp_source hp_sink sink_temperature[C] gradient_sink water_temperature[C] correction_factor hp_system
0 1 ruhnau weather air radiator 50.0 -1.0 50.0 NaN NaN
1 2 ruhnau weather soil floor 30.0 -0.5 55.0 NaN NaN
2 3 ruhnau weather water radiator 35.0 -1.0 60.0 NaN NaN
3 4 ruhnau weather NaN floor 25.0 -0.5 NaN NaN NaN
4 5 ruhnau weather air floor 35.0 NaN NaN NaN NaN
5 6 ruhnau weather soil radiator NaN NaN NaN NaN NaN
6 7 ruhnau weather water floor NaN NaN NaN NaN NaN
7 8 ruhnau weather NaN NaN NaN NaN NaN 0.95 system

Instantiate and Configure Model

Initialize the time series generator and add the objects.

gen = TSGen()
gen.add_objects(objects)

Run the Simulation

Generate heat pump COP time series for each object.

summary, df = gen.generate(data, workers=1)
100%|██████████| 8/8 [00:01<00:00,  4.46it/s]

Results Summary

Below is a summary of the COP values for each heat pump system.

print("Summary:")
summary
Summary:
hp:heating_avg[1] hp:heating_min[1] hp:heating_max[1] hp:dhw_avg[1] hp:dhw_min[1] hp:dhw_max[1]
1 3.53 1.97 7.40 2.88 2.16 4.30
2 7.31 5.22 10.27 3.30 2.72 3.98
3 7.16 4.50 11.81 2.89 2.89 2.89
4 4.66 2.91 8.01 2.88 2.16 4.30
5 4.01 2.51 7.02 2.88 2.16 4.30
6 6.71 3.72 11.99 3.82 3.17 4.56
7 7.00 5.61 9.15 3.77 3.77 3.77
8 5.06 2.95 9.89 3.69 2.89 5.28

Preparation for Visualization

Before we create visualizations, we need to prepare the data.

# Convert index to datetime for all time series
for obj_id in df:
    if Types.HP in df[obj_id]:
        df[obj_id][Types.HP].index = pd.to_datetime(df[obj_id][Types.HP].index, utc=True)

# Get heat pump parameters from objects dataframe
system_configs = {}
for _, row in objects.iterrows():
    obj_id = row['id']
    if obj_id in df:
        hp_source = row['hp_source'] if not pd.isna(row.get('hp_source', pd.NA)) else "Default"
        hp_sink = row['hp_sink'] if not pd.isna(row.get('hp_sink', pd.NA)) else "Default"
        temp_sink = row['temp_sink'] if not pd.isna(row.get('temp_sink', pd.NA)) else "Default"
        temp_water = row['temp_water'] if not pd.isna(row.get('temp_water', pd.NA)) else "Default"
        system_configs[obj_id] = {
            'hp_source': hp_source,
            'hp_sink': hp_sink,
            'temp_sink': temp_sink,
            'temp_water': temp_water
        }

Visualization 1: COP Distribution by System

Let’s compare the distribution of COP values for different heat pump systems using boxplots.

# Collect the full distribution of COP values for each system
heating_cop_data = []
dhw_cop_data = []
system_ids = []

for obj_id in df:
    if Types.HP in df[obj_id]:
        # Extract heating COP
        heating_col = f"{Types.HP}{SEP}{Types.HEATING}[1]"
        if heating_col in df[obj_id][Types.HP].columns:
            heating_cop_data.append(df[obj_id][Types.HP][heating_col].values)

            # Only add system ID if we haven't already (to keep lists aligned)
            if len(heating_cop_data) > len(system_ids):
                system_ids.append(obj_id)

        # Extract DHW COP
        dhw_col = f"{Types.HP}{SEP}{Types.DHW}[1]"
        if dhw_col in df[obj_id][Types.HP].columns:
            dhw_cop_data.append(df[obj_id][Types.HP][dhw_col].values)

# Create a boxplot for heating COP distribution
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.boxplot(heating_cop_data, labels=[f"ID {id}" for id in system_ids])
plt.title("Heating COP Distribution by System")
plt.ylabel("COP")
plt.xticks(rotation=45)
plt.grid(axis="y")

# Create a boxplot for DHW COP distribution
plt.subplot(1, 2, 2)
plt.boxplot(dhw_cop_data, labels=[f"ID {id}" for id in system_ids])
plt.title("DHW COP Distribution by System")
plt.ylabel("COP")
plt.xticks(rotation=45)
plt.grid(axis="y")

plt.tight_layout()
plt.show()
../../_images/521678871d0ec3eaf9f6d95945c4064375f032775c6d6224484955a7ced8dfd8.png

Visualization 2: Time Series for All Systems

Let’s visualize the COP time series for all heat pump systems.

# Calculate the number of rows and columns for the subplots
n_systems = len(df)
n_cols = min(4, n_systems)
n_rows = (n_systems + n_cols - 1) // n_cols  # Ceiling division

fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 4 * n_rows))
# Always flatten the axes array to make it easier to index
if n_rows == 1 and n_cols == 1:
    axes = np.array([axes])  # Make axes iterable if there's only one subplot
else:
    axes = axes.flatten()  # Flatten the array of axes for easier indexing

# For each heat pump system, create a separate subplot
for i, obj_id in enumerate(df):
    if i >= len(axes):
        break  # Safety check
    
    if Types.HP not in df[obj_id]:
        continue
    
    # Get system parameters for the title
    config = system_configs.get(obj_id, {})
    hp_source = config.get('hp_source', 'Default')
    hp_sink = config.get('hp_sink', 'Default')
    temp_sink = config.get('temp_sink', 'Default')
    temp_water = config.get('temp_water', 'Default')
    
    # Plot the heating COP time series
    heating_col = f"{Types.HP}{SEP}{Types.HEATING}[1]"
    if heating_col in df[obj_id][Types.HP].columns:
        df[obj_id][Types.HP][heating_col].plot(ax=axes[i], color="#1f77b4", linewidth=1, label="Heating COP")

    # Plot the DHW COP time series
    dhw_col = f"{Types.HP}{SEP}{Types.DHW}[1]"
    if dhw_col in df[obj_id][Types.HP].columns:
        df[obj_id][Types.HP][dhw_col].plot(ax=axes[i], color='#ff7f0e', linewidth=1, label='DHW COP')
    
    axes[i].set_title(f'ID {obj_id}, Source: {hp_source}, Sink: {hp_sink}')
    axes[i].set_xlabel('Time')
    axes[i].set_ylabel('COP')
    axes[i].set_ylim(0, 12)
    axes[i].legend()
    axes[i].grid(True)

# Hide empty subplots
for i in range(len(df), len(axes)):
    if i < len(axes):
        axes[i].axis('off')

plt.tight_layout()
plt.show()
../../_images/11b9b8a2fd95dead20676eb194d383d5cde5220226ed6e7e26a3632c6f459eac.png

Visualization 3: COP Heatmap (Heating Only)

Let’s create a heatmap visualization to show how heating COP varies by hour of day and day of year.

# Create a figure with appropriate number of subfigures
fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 4 * n_rows))
# Always flatten the axes array to make it easier to index
if n_rows == 1 and n_cols == 1:
    axes = np.array([axes])  # Make axes iterable if there's only one subplot
else:
    axes = axes.flatten()  # Flatten the array of axes for easier indexing

# For each heat pump system, create a separate subplot
for i, obj_id in enumerate(df):
    if i >= len(axes):
        break  # Safety check
    
    if Types.HP not in df[obj_id]:
        continue
    
    # Get system parameters for the title
    config = system_configs.get(obj_id, {})
    hp_source = config.get('hp_source', 'Default')
    hp_sink = config.get('hp_sink', 'Default')
    
    # Process heating COP
    heating_col = f"{Types.HP}{SEP}{Types.HEATING}[1]"
    if heating_col in df[obj_id][Types.HP].columns:
        ts = df[obj_id][Types.HP][heating_col]
        
        # Create a pivot table with hours as columns and days as rows
        pivot_data = pd.DataFrame({
            'hour': ts.index.hour,
            'day_of_year': ts.index.dayofyear,
            'cop': ts.values
        })
        pivot_table = pivot_data.pivot_table(values='cop', index='day_of_year', columns='hour', aggfunc='mean')
        
        # Create heatmap
        im = axes[i].imshow(pivot_table, aspect='auto', cmap='viridis')
        axes[i].set_title(f'ID {obj_id}, Source: {hp_source}, Sink: {hp_sink}')
        axes[i].set_xlabel('Hour of Day')
        axes[i].set_ylabel('Day of Year')
        
        # Add colorbar
        fig.colorbar(im, ax=axes[i], label='Heating COP')

# Hide empty subplots
for i in range(len(df), len(axes)):
    if i < len(axes):
        axes[i].axis('off')

plt.tight_layout()
plt.show()
../../_images/d97d6d496c664385661d6753bc26972791f21b1b510aac6aadbd54a642d5814f.png

Visualization 4: Seasonal Daily Profile Analysis

Let’s analyze how COP values vary throughout the day for different seasons.

# Define seasons
seasons = {
    'Winter': [12, 1, 2],
    'Spring': [3, 4, 5],
    'Summer': [6, 7, 8],
    'Fall': [9, 10, 11]
}

# Create a figure with appropriate number of subfigures
fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 4 * n_rows))
# Always flatten the axes array to make it easier to index
if n_rows == 1 and n_cols == 1:
    axes = np.array([axes])  # Make axes iterable if there's only one subplot
else:
    axes = axes.flatten()  # Flatten the array of axes for easier indexing

# For each heat pump system, create a separate subplot
for i, obj_id in enumerate(df):
    if i >= len(axes):
        break  # Safety check
    
    if Types.HP not in df[obj_id]:
        continue
    
    # Get system parameters for the title
    config = system_configs.get(obj_id, {})
    hp_source = config.get('hp_source', 'Default')
    hp_sink = config.get('hp_sink', 'Default')
    
    # Process heating COP
    heating_col = f"{Types.HP}{SEP}{Types.HEATING}[1]"
    if heating_col in df[obj_id][Types.HP].columns:
        ts_heating = df[obj_id][Types.HP][heating_col]

        # Plot each season on the same subplot
        for season_name, months in seasons.items():
            # Filter data for the season
            season_data = ts_heating[ts_heating.index.month.isin(months)]
            if not season_data.empty:
                # Create average daily profile
                daily_profile = season_data.groupby(season_data.index.hour).mean()
                axes[i].plot(daily_profile.index, daily_profile.values, label=f"{season_name} (Heating)", linewidth=2)

    # Process DHW COP
    dhw_col = f"{Types.HP}{SEP}{Types.DHW}[1]"
    if dhw_col in df[obj_id][Types.HP].columns:
        ts_dhw = df[obj_id][Types.HP][dhw_col]

        # Plot each season on the same subplot (using dashed lines for DHW)
        for season_name, months in seasons.items():
            # Filter data for the season
            season_data = ts_dhw[ts_dhw.index.month.isin(months)]
            if not season_data.empty:
                # Create average daily profile
                daily_profile = season_data.groupby(season_data.index.hour).mean()
                axes[i].plot(
                    daily_profile.index, daily_profile.values, label=f"{season_name} (DHW)", linewidth=2, linestyle="--"
                )

    axes[i].set_title(f'ID {obj_id}, Source: {hp_source}, Sink: {hp_sink}')
    axes[i].set_xlabel('Hour of Day')
    axes[i].set_ylabel('Average COP')
    axes[i].set_ylim(0, 10)
    axes[i].legend()
    axes[i].grid(True)
    axes[i].set_xticks(range(0, 24, 4))  # Show fewer ticks for readability

# Hide empty subplots
for i in range(len(df), len(axes)):
    if i < len(axes):
        axes[i].axis('off')

plt.tight_layout()
plt.show()
../../_images/7e96e7b28df0c687eba8f36be403488801d883c3e893a1c9875962e7b5557f1d.png

Visualization 5: COP vs. Temperature Analysis

Let’s analyze the relationship between COP and outdoor temperature.

# Create a figure with appropriate number of subfigures
fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 4 * n_rows))
# Always flatten the axes array to make it easier to index
if n_rows == 1 and n_cols == 1:
    axes = np.array([axes])  # Make axes iterable if there's only one subplot
else:
    axes = axes.flatten()  # Flatten the array of axes for easier indexing

# For each heat pump system, create a separate subplot
for i, obj_id in enumerate(df):
    if i >= len(axes):
        break  # Safety check

    if Types.HP not in df[obj_id]:
        continue

    # Get system parameters for the title
    config = system_configs.get(obj_id, {})
    hp_source = config.get("hp_source", "Default")
    hp_sink = config.get("hp_sink", "Default")

    # Get weather data
    weather_data = data.get("weather")
    if weather_data is None:
        continue

    # Ensure weather data has the same index as the COP data
    # Check if the index is already a datetime index
    if not isinstance(weather_data.index, pd.DatetimeIndex):
        # Check if 'datetime' column exists
        if "datetime" in weather_data.columns:
            weather_data["datetime"] = pd.to_datetime(weather_data["datetime"], utc=True)
            weather_data.set_index("datetime", inplace=True)
        # If not, check if the index can be converted to datetime
        else:
            try:
                weather_data.index = pd.to_datetime(weather_data.index, utc=True)
            except:
                # If all else fails, try to find a column that looks like a datetime
                datetime_cols = [col for col in weather_data.columns if "time" in col.lower() or "date" in col.lower()]
                if datetime_cols:
                    weather_data[datetime_cols[0]] = pd.to_datetime(weather_data[datetime_cols[0]], utc=True)
                    weather_data.set_index(datetime_cols[0], inplace=True)
                else:
                    # If no datetime column is found, skip this iteration
                    continue

    # Get temperature column
    temp_col = "air_temperature[C]"

    # Process heating COP
    heating_col = f"{Types.HP}{SEP}{Types.HEATING}[1]"
    if heating_col in df[obj_id][Types.HP].columns:
        # Merge COP and temperature data
        merged_data = pd.merge(
            df[obj_id][Types.HP][heating_col],
            weather_data[temp_col],
            left_index=True,
            right_index=True,
            how="inner",
        )

        # Plot scatter plot
        axes[i].scatter(merged_data[temp_col], merged_data[heating_col], label="Heating COP", alpha=0.5, s=10)

    # Process DHW COP
    dhw_col = f"{Types.HP}{SEP}{Types.DHW}[1]"
    if dhw_col in df[obj_id][Types.HP].columns:
        # Merge COP and temperature data
        merged_data = pd.merge(
            df[obj_id][Types.HP][dhw_col],
            weather_data[temp_col],
            left_index=True,
            right_index=True,
            how="inner",
        )

        # Plot scatter plot
        axes[i].scatter(merged_data[temp_col], merged_data[dhw_col], label="DHW COP", alpha=0.5, s=10)

    axes[i].set_title(f"ID {obj_id}, Source: {hp_source}, Sink: {hp_sink}")
    axes[i].set_xlabel("Temperature (°C)")
    axes[i].set_ylabel("COP")
    axes[i].set_ylim(0, 12)
    axes[i].legend()
    axes[i].grid(True)

# Hide empty subplots
for i in range(len(df), len(axes)):
    if i < len(axes):
        axes[i].axis("off")

plt.tight_layout()
plt.show()
../../_images/d3a9353131da7140cc55ceb84bf9034b8ff5e7a3177aa8d4087d2963e13d3a13.png

Conclusion

In this notebook, we’ve demonstrated how to use the Ruhnau method to generate heat pump COP time series for different heat pump types, heating systems, and domestic hot water (DHW). We’ve also visualized the results in various ways to gain insights into the performance of different heat pump systems.

You can further explore:

  • Adjusting heat pump parameters in objects.csv

  • Testing different system configurations

  • Analyzing the impact of temperature on COP values

  • Comparing performance across different seasons