Skip to content

Commit

Permalink
Merge pull request #329 from wouterpeere/issue272-optimise-load-profi…
Browse files Browse the repository at this point in the history
…le-with-dhw

Issue272 optimise load profile with dhw
  • Loading branch information
wouterpeere authored Feb 14, 2025
2 parents 5ab78ee + 8a5ea0e commit 201d1bb
Show file tree
Hide file tree
Showing 14 changed files with 170 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Added

- Added support for DHW profiles in optimisation (issue #272).
- Added Prandtl number to FluidData class (issue #326).

## [2.3.1] - 2025-01-23
Expand Down
21 changes: 11 additions & 10 deletions GHEtool/Examples/optimise_load_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,35 @@ def optimise():
borefield.create_rectangular_borefield(10, 10, 6, 6, 150, 1, 0.075)

# load the hourly profile
load = HourlyBuildingLoad(efficiency_heating=10 ** 6, efficiency_cooling=10 ** 6)
load.load_hourly_profile("hourly_profile.csv", header=True, separator=";")
load = HourlyBuildingLoad(efficiency_heating=5, efficiency_cooling=25)
load.load_hourly_profile(FOLDER.joinpath("Examples/hourly_profile.csv"), header=True, separator=";")
load.dhw = 100000 # add domestic hot water

# optimise the load for a 10x10 field (see data above) and a fixed depth of 150m.
# first for an optimisation based on the power
borefield.optimise_load_profile_power(building_load=load, depth=150)
borefield.optimise_load_profile_power(building_load=load)

print(f'Max heating power (primary): {borefield.load.max_peak_extraction:,.0f}kW')
print(f'Max cooling power (primary): {borefield.load.max_peak_injection:,.0f}kW')
print(f'Max extraction power (primary): {borefield.load.max_peak_extraction:,.0f}kW')
print(f'Max injection power (primary): {borefield.load.max_peak_injection:,.0f}kW')

print(
f'Total energy extracted from the borefield over simulation period: {np.sum(borefield.load.monthly_baseload_extraction_simulation_period):,.0f}MWh')
print(
f'Total energy injected in the borefield over simulation period): {np.sum(borefield.load.monthly_baseload_injection_simulation_period):,.0f}MWh')
f'Total energy injected in the borefield over simulation period: {np.sum(borefield.load.monthly_baseload_injection_simulation_period):,.0f}MWh')
print('------------------------------------------------------------------------')
borefield.calculate_temperatures(hourly=True)
borefield.print_temperature_profile(plot_hourly=True)

# first for an optimisation based on the energy
borefield.optimise_load_profile_energy(building_load=load, depth=150)
borefield.optimise_load_profile_energy(building_load=load)

print(f'Max heating power (primary): {borefield.load.max_peak_extraction:,.0f}kW')
print(f'Max cooling power (primary): {borefield.load.max_peak_injection:,.0f}kW')
print(f'Max extraction power (primary): {borefield.load.max_peak_extraction:,.0f}kW')
print(f'Max injection power (primary): {borefield.load.max_peak_injection:,.0f}kW')

print(
f'Total energy extracted from the borefield over simulation period: {np.sum(borefield.load.monthly_baseload_extraction_simulation_period):,.0f}MWh')
print(
f'Total energy injected in the borefield over simulation period): {np.sum(borefield.load.monthly_baseload_injection_simulation_period):,.0f}MWh')
f'Total energy injected in the borefield over simulation period: {np.sum(borefield.load.monthly_baseload_injection_simulation_period):,.0f}MWh')

borefield.calculate_temperatures(hourly=True)
borefield.print_temperature_profile(plot_hourly=True)
Expand Down
11 changes: 2 additions & 9 deletions GHEtool/Examples/optimise_load_profile_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,10 @@ def optimise():
load = HourlyBuildingLoad(efficiency_heating=4.5, efficiency_cooling=20)
load.load_hourly_profile(FOLDER.joinpath("test\methods\hourly_data\\auditorium.csv"), header=True, separator=";",
col_cooling=0, col_heating=1)
import matplotlib.pyplot as plt

# plt.figure()
# plt.plot(load.hourly_cooling_load_simulation_period)
# plt.plot(load.hourly_heating_load_simulation_period)
# plt.show()
# borefield.calculate_temperatures()
# borefield.print_temperature_profile(plot_hourly=False)

# optimise the load for a 10x10 field (see data above) and a fixed length of 150m.
# first for an optimisation based on the power
borefield.optimise_load_profile_energy(building_load=load, length=100)
borefield.optimise_load_profile_energy(building_load=load)

print(f'Max heating power (primary): {borefield.load.max_peak_extraction:,.0f}kW')
print(f'Max cooling power (primary): {borefield.load.max_peak_injection:,.0f}kW')
Expand Down
35 changes: 29 additions & 6 deletions GHEtool/Methods/optimise_load_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def optimise_load_profile_power(
temperature_threshold: float = 0.05,
use_hourly_resolution: bool = True,
max_peak_heating: float = None,
max_peak_cooling: float = None
max_peak_cooling: float = None,
dhw_preferential: bool = None
) -> tuple[HourlyBuildingLoad, HourlyBuildingLoad]:
"""
This function optimises the load for maximum power in extraction and injection based on the given borefield and
Expand All @@ -35,6 +36,9 @@ def optimise_load_profile_power(
The maximum peak power for the heating (building side) [kW]
max_peak_cooling : float
The maximum peak power for the cooling (building side) [kW]
dhw_preferential : bool
True if heating should first be reduced only after which the dhw share is reduced.
If it is None, then the dhw profile is not optimised and kept constant.
Returns
-------
Expand Down Expand Up @@ -66,6 +70,7 @@ def optimise_load_profile_power(

# set initial peak loads
init_peak_heating: float = borefield.load.max_peak_heating
init_peak_dhw: float = borefield.load.max_peak_dhw
init_peak_cooling: float = borefield.load.max_peak_cooling

# correct for max peak powers
Expand All @@ -76,6 +81,7 @@ def optimise_load_profile_power(

# peak loads for iteration
peak_heat_load: float = init_peak_heating
peak_dhw_load: float = init_peak_dhw
peak_cool_load: float = init_peak_cooling

# set iteration criteria
Expand All @@ -88,6 +94,9 @@ def optimise_load_profile_power(
borefield.load.set_hourly_heating_load(
np.minimum(peak_heat_load, building_load.hourly_heating_load
if isinstance(borefield.load, HourlyBuildingLoad) else building_load.hourly_heating_load_simulation_period))
borefield.load.set_hourly_dhw_load(
np.minimum(peak_dhw_load, building_load.hourly_dhw_load
if isinstance(borefield.load, HourlyBuildingLoad) else building_load.hourly_dhw_load_simulation_period))

# calculate temperature profile, just for the results
borefield.calculate_temperatures(length=borefield.H, hourly=use_hourly_resolution)
Expand All @@ -96,11 +105,23 @@ def optimise_load_profile_power(
if abs(min(borefield.results.peak_extraction) - borefield.Tf_min) > temperature_threshold:
# check if it goes below the threshold
if min(borefield.results.peak_extraction) < borefield.Tf_min:
peak_heat_load = max(0.1, peak_heat_load - 1 * max(1, 10 * (
borefield.Tf_min - min(borefield.results.peak_extraction))))
if (dhw_preferential and peak_heat_load > 0.1) \
or (not dhw_preferential and peak_dhw_load <= 0.1) \
or dhw_preferential is None:
# first reduce the peak load in heating before touching the dhw load
# if dhw_preferential is None, it is not optimised and kept constant
peak_heat_load = max(0.1, peak_heat_load - 1 * max(1, 10 * (
borefield.Tf_min - min(borefield.results.peak_extraction))))
else:
peak_dhw_load = max(0.1, peak_dhw_load - 1 * max(1, 10 * (
borefield.Tf_min - min(borefield.results.peak_extraction))))
else:
peak_heat_load = min(init_peak_heating, peak_heat_load * 1.01)
if peak_heat_load == init_peak_heating:
if (dhw_preferential and peak_heat_load != init_peak_heating) or (
not dhw_preferential and 0.1 >= peak_dhw_load) or dhw_preferential is None:
peak_heat_load = min(init_peak_heating, peak_heat_load * 1.01)
else:
peak_dhw_load = min(init_peak_dhw, peak_dhw_load * 1.01)
if peak_heat_load == init_peak_heating and peak_dhw_load == init_peak_dhw:
heat_ok = True
else:
heat_ok = True
Expand All @@ -124,6 +145,8 @@ def optimise_load_profile_power(
np.maximum(0, building_load.hourly_heating_load - borefield.load.hourly_heating_load))
external_load.set_hourly_cooling_load(
np.maximum(0, building_load.hourly_cooling_load - borefield.load.hourly_cooling_load))
external_load.set_hourly_dhw_load(
np.maximum(0, building_load.hourly_dhw_load - borefield.load.hourly_dhw_load))

return borefield.load, external_load

Expand All @@ -133,7 +156,7 @@ def optimise_load_profile_energy(
building_load: Union[HourlyBuildingLoad, HourlyBuildingLoadMultiYear],
temperature_threshold: float = 0.05,
max_peak_heating: float = None,
max_peak_cooling: float = None
max_peak_cooling: float = None,
) -> tuple[HourlyBuildingLoadMultiYear, HourlyBuildingLoadMultiYear]:
"""
This function optimises the load for maximum energy extraction and injection based on the given borefield and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ def max_peak_dhw(self) -> float:

def load_hourly_profile(
self, file_path: str, header: bool = True, separator: str = ";", decimal_seperator: str = ".",
col_heating: int = 0, col_cooling: int = 1) -> None:
col_heating: int = 0, col_cooling: int = 1, col_dhw: int = None) -> None:
"""
This function loads in an hourly load profile [kW].
Expand All @@ -511,6 +511,8 @@ def load_hourly_profile(
Column index for heating data
col_cooling : int
Column index for cooling data
col_dhw : int
Column index for dhw data. None if not applicable
Returns
-------
Expand All @@ -529,6 +531,8 @@ def load_hourly_profile(
# set data
self.hourly_heating_load = np.array(df.iloc[:, col_heating])
self.hourly_cooling_load = np.array(df.iloc[:, col_cooling])
if col_dhw is not None:
self.dhw = np.array(df.iloc[:, col_dhw])

@property
def max_peak_injection(self) -> float:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ def _monthly_peak_extraction_heating_simulation_period(self) -> np.ndarray:
def _monthly_peak_extraction_dhw_simulation_period(self) -> np.ndarray:
"""
This function returns the monthly extraction peak of the DHW production in kW/month for the whole simulation period.
Returns
-------
peak extraction : np.ndarray
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,27 @@ def set_hourly_heating_load(self, load: ArrayLike) -> None:
"""
self.hourly_heating_load = load

def set_hourly_dhw_load(self, load: ArrayLike) -> None:
"""
This function sets the hourly domestic hot water load [kWh/h] after it has been checked.
Parameters
----------
load : np.ndarray, list or tuple
Hourly dhw load [kWh/h]
Returns
-------
None
Raises
------
ValueError
When either the length is not 8760, the input is not of the correct type, or it contains negative
values
"""
self.dhw = load

@property
def hourly_cooling_load(self) -> np.ndarray:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,27 @@ def set_hourly_heating_load(self, load: ArrayLike) -> None:
"""
self.hourly_heating_load = load

def set_hourly_dhw_load(self, load: ArrayLike) -> None:
"""
This function sets the hourly domestic hot water load [kWh/h] after it has been checked.
Parameters
----------
load : np.ndarray, list or tuple
Hourly dhw load [kWh/h]
Returns
-------
None
Raises
------
ValueError
When either the length is not 8760, the input is not of the correct type, or it contains negative
values
"""
self.dhw = load

@property
def hourly_dhw_load_simulation_period(self) -> np.ndarray:
"""
Expand Down
6 changes: 4 additions & 2 deletions GHEtool/test/methods/TestMethodClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def __init__(self, borefield: Borefield, load, depth: float, percentage_heating:
peak_heating_ext: float,
peak_cooling_ext: float, name: str = "", power: bool = True, hourly: bool = True,
max_peak_heating: float = None,
max_peak_cooling: float = None):
max_peak_cooling: float = None,
dhw_preferential: bool = True):
self.borefield = copy.deepcopy(borefield)
self.load = copy.deepcopy(load)
self.depth = depth
Expand All @@ -45,6 +46,7 @@ def __init__(self, borefield: Borefield, load, depth: float, percentage_heating:
self.hourly = hourly
self.max_peak_heating = max_peak_heating
self.max_peak_cooling = max_peak_cooling
self.dhw_preferential = dhw_preferential

def test(self): # pragma: no cover
self.borefield.optimise_load_profile(self.load, self.depth, self.SCOP, self.SEER)
Expand Down Expand Up @@ -185,7 +187,7 @@ def optimise_load_profile_input(self) -> list:
if isinstance(i, SizingObject):
continue
temp.append((copy.deepcopy(i.borefield), copy.deepcopy(i.load), i.depth, i.power, i.hourly,
i.max_peak_heating, i.max_peak_cooling))
i.max_peak_heating, i.max_peak_cooling, i.dhw_preferential))
return temp

@property
Expand Down
44 changes: 43 additions & 1 deletion GHEtool/test/methods/method_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,6 @@
639.283, 195.053, 37.132, 340.983,
name='Optimise load profile 1, reversed (power, hourly)', power=True,
hourly=True))

list_of_test_objects.add(OptimiseLoadProfileObject(borefield, hourly_load, 150, 99.999, 72.205,
676.415, 345.9849, 22.46646, 342.1411,
name='Optimise load profile 1, reversed (energy)', power=False))
Expand Down Expand Up @@ -600,3 +599,46 @@
25.315, 42.2817092190839, 0.0, 64.4014083207306,
name='Optimise load profile (auditorium) (energy)', power=False,
hourly=False))
borefield = Borefield()
data = GroundConstantTemperature(3, 10)
borefield.set_ground_parameters(data)
borefield.set_Rb(0.2)
borefield.set_borefield(borefield_gt)
borefield.set_max_avg_fluid_temperature(16)
borefield.set_min_avg_fluid_temperature(0)
hourly_load.load_hourly_profile(FOLDER.joinpath("test\methods\hourly_data\hourly_profile.csv"), col_heating=1,
col_cooling=0)
list_of_test_objects.add(OptimiseLoadProfileObject(borefield, hourly_load, 150, 99.976, 66.492,
643.137, 195.331, 33.278, 340.705,
name='Optimise load profile 1, reversed (power, dhw not preferential)',
power=True,
hourly=False, dhw_preferential=False))
list_of_test_objects.add(OptimiseLoadProfileObject(borefield, hourly_load, 150, 99.971, 66.424,
639.283, 195.053, 37.132, 340.983,
name='Optimise load profile 1, reversed (power, hourly, dhw not preferential)',
power=True,
hourly=True, dhw_preferential=False))
hourly_load.load_hourly_profile(FOLDER.joinpath("test\methods\hourly_data\hourly_profile.csv"), col_heating=1,
col_cooling=0, col_dhw=1)
hourly_load.set_hourly_heating_load(np.zeros(8760))
hourly_load.cop_dhw = 10 ** 6
list_of_test_objects.add(OptimiseLoadProfileObject(borefield, hourly_load, 150, 99.976, 66.492,
643.137, 195.331, 0, 340.705,
name='Optimise load profile 1, reversed (power, dhw load)',
power=True,
hourly=False, dhw_preferential=False))
list_of_test_objects.add(OptimiseLoadProfileObject(borefield, hourly_load, 150, 99.971, 66.424,
639.283, 195.053, 0, 340.983,
name='Optimise load profile 1, reversed (power, hourly, dhw load)',
power=True,
hourly=True, dhw_preferential=False))
list_of_test_objects.add(OptimiseLoadProfileObject(borefield, hourly_load, 150, 99.976, 66.492,
643.137, 195.331, 0, 340.705,
name='Optimise load profile 1, reversed (power, dhw load, preferential)',
power=True,
hourly=False, dhw_preferential=True))
list_of_test_objects.add(OptimiseLoadProfileObject(borefield, hourly_load, 150, 99.971, 66.424,
639.283, 195.053, 0, 340.983,
name='Optimise load profile 1, reversed (power, hourly, dhw load, preferential)',
power=True,
hourly=True, dhw_preferential=True))
12 changes: 7 additions & 5 deletions GHEtool/test/methods/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,24 @@ def test_L4(model: Borefield, result):
ids=list_of_test_objects.names_optimise_load_profile)
def test_optimise(input, result):
model: Borefield = input[0]
load, depth, power, hourly, max_peak_extraction, max_peak_injection = input[1:]
load, depth, power, hourly, max_peak_extraction, max_peak_injection, dhw_preferential = input[1:]
model.H = depth
if power:
borefield_load, external_load = optimise_load_profile_power(model, load,
use_hourly_resolution=hourly,
max_peak_heating=max_peak_extraction,
max_peak_cooling=max_peak_injection)
max_peak_cooling=max_peak_injection,
dhw_preferential=dhw_preferential)
else:
borefield_load, external_load = optimise_load_profile_energy(model, load,
max_peak_heating=max_peak_extraction,
max_peak_cooling=max_peak_injection)
percentage_extraction, percentage_injection, peak_extraction_geo, peak_injection_geo, peak_extraction_ext, peak_injection_ext = \
result

_percentage_extraction = np.sum(borefield_load.hourly_heating_load_simulation_period) / \
np.sum(load.hourly_heating_load_simulation_period) * 100
_percentage_extraction = (np.sum(borefield_load.hourly_heating_load_simulation_period) + np.sum(
borefield_load.hourly_dhw_load_simulation_period)) / \
(np.sum(load.hourly_heating_load_simulation_period) + np.sum(
load.hourly_dhw_load_simulation_period)) * 100
_percentage_injection = np.sum(borefield_load.hourly_cooling_load_simulation_period) / \
np.sum(load.hourly_cooling_load_simulation_period) * 100
# print(_percentage_extraction)
Expand Down
Loading

0 comments on commit 201d1bb

Please sign in to comment.