Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue272 optimise load profile with dhw #329

Merged
merged 10 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@

# 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 @@

# 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 @@
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 @@
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 * (

Check warning on line 116 in GHEtool/Methods/optimise_load_profile.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/Methods/optimise_load_profile.py#L116

Added line #L116 was not covered by tests
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)

Check warning on line 123 in GHEtool/Methods/optimise_load_profile.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/Methods/optimise_load_profile.py#L123

Added line #L123 was not covered by tests
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 @@
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 @@
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 @@
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 @@
self.hourly = hourly
self.max_peak_heating = max_peak_heating
self.max_peak_cooling = max_peak_cooling
self.dhw_preferential = dhw_preferential

Check warning on line 49 in GHEtool/test/methods/TestMethodClass.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/TestMethodClass.py#L49

Added line #L49 was not covered by tests

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 @@
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,

Check warning on line 609 in GHEtool/test/methods/method_data.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/method_data.py#L602-L609

Added lines #L602 - L609 were not covered by tests
col_cooling=0)
list_of_test_objects.add(OptimiseLoadProfileObject(borefield, hourly_load, 150, 99.976, 66.492,

Check warning on line 611 in GHEtool/test/methods/method_data.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/method_data.py#L611

Added line #L611 was not covered by tests
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,

Check warning on line 616 in GHEtool/test/methods/method_data.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/method_data.py#L616

Added line #L616 was not covered by tests
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,

Check warning on line 621 in GHEtool/test/methods/method_data.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/method_data.py#L621

Added line #L621 was not covered by tests
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,

Check warning on line 625 in GHEtool/test/methods/method_data.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/method_data.py#L623-L625

Added lines #L623 - L625 were not covered by tests
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,

Check warning on line 630 in GHEtool/test/methods/method_data.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/method_data.py#L630

Added line #L630 was not covered by tests
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,

Check warning on line 635 in GHEtool/test/methods/method_data.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/method_data.py#L635

Added line #L635 was not covered by tests
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,

Check warning on line 640 in GHEtool/test/methods/method_data.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/method_data.py#L640

Added line #L640 was not covered by tests
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 @@
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:]

Check warning on line 50 in GHEtool/test/methods/test_methods.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/test_methods.py#L50

Added line #L50 was not covered by tests
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(

Check warning on line 64 in GHEtool/test/methods/test_methods.py

View check run for this annotation

Codecov / codecov/patch

GHEtool/test/methods/test_methods.py#L64

Added line #L64 was not covered by tests
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