diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7f0857..941bceb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/GHEtool/Examples/optimise_load_profile.py b/GHEtool/Examples/optimise_load_profile.py index af29c434..1adffcae 100644 --- a/GHEtool/Examples/optimise_load_profile.py +++ b/GHEtool/Examples/optimise_load_profile.py @@ -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) diff --git a/GHEtool/Examples/optimise_load_profile_extra.py b/GHEtool/Examples/optimise_load_profile_extra.py index f14db8bd..0f0f2f9c 100644 --- a/GHEtool/Examples/optimise_load_profile_extra.py +++ b/GHEtool/Examples/optimise_load_profile_extra.py @@ -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') diff --git a/GHEtool/Methods/optimise_load_profile.py b/GHEtool/Methods/optimise_load_profile.py index 07421cbf..917274c0 100644 --- a/GHEtool/Methods/optimise_load_profile.py +++ b/GHEtool/Methods/optimise_load_profile.py @@ -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 @@ -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 ------- @@ -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 @@ -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 @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/GHEtool/VariableClasses/LoadData/Baseclasses/_HourlyDataBuilding.py b/GHEtool/VariableClasses/LoadData/Baseclasses/_HourlyDataBuilding.py index b96623d7..b7402e69 100644 --- a/GHEtool/VariableClasses/LoadData/Baseclasses/_HourlyDataBuilding.py +++ b/GHEtool/VariableClasses/LoadData/Baseclasses/_HourlyDataBuilding.py @@ -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]. @@ -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 ------- @@ -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: diff --git a/GHEtool/VariableClasses/LoadData/Baseclasses/_LoadDataBuilding.py b/GHEtool/VariableClasses/LoadData/Baseclasses/_LoadDataBuilding.py index 198a7207..f5ac9f8b 100644 --- a/GHEtool/VariableClasses/LoadData/Baseclasses/_LoadDataBuilding.py +++ b/GHEtool/VariableClasses/LoadData/Baseclasses/_LoadDataBuilding.py @@ -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 diff --git a/GHEtool/VariableClasses/LoadData/BuildingLoad/HourlyBuildingLoad.py b/GHEtool/VariableClasses/LoadData/BuildingLoad/HourlyBuildingLoad.py index b9f871df..3dbade01 100644 --- a/GHEtool/VariableClasses/LoadData/BuildingLoad/HourlyBuildingLoad.py +++ b/GHEtool/VariableClasses/LoadData/BuildingLoad/HourlyBuildingLoad.py @@ -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: """ diff --git a/GHEtool/VariableClasses/LoadData/BuildingLoad/HourlyBuildingLoadMultiYear.py b/GHEtool/VariableClasses/LoadData/BuildingLoad/HourlyBuildingLoadMultiYear.py index 69c2ebbb..ddb58658 100644 --- a/GHEtool/VariableClasses/LoadData/BuildingLoad/HourlyBuildingLoadMultiYear.py +++ b/GHEtool/VariableClasses/LoadData/BuildingLoad/HourlyBuildingLoadMultiYear.py @@ -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: """ diff --git a/GHEtool/test/methods/TestMethodClass.py b/GHEtool/test/methods/TestMethodClass.py index ffc2cb96..ae04d7c9 100644 --- a/GHEtool/test/methods/TestMethodClass.py +++ b/GHEtool/test/methods/TestMethodClass.py @@ -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 @@ -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) @@ -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 diff --git a/GHEtool/test/methods/method_data.py b/GHEtool/test/methods/method_data.py index 0a81fa67..de9a7d09 100644 --- a/GHEtool/test/methods/method_data.py +++ b/GHEtool/test/methods/method_data.py @@ -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)) @@ -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)) diff --git a/GHEtool/test/methods/test_methods.py b/GHEtool/test/methods/test_methods.py index 26939fe3..88cb722b 100644 --- a/GHEtool/test/methods/test_methods.py +++ b/GHEtool/test/methods/test_methods.py @@ -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) diff --git a/GHEtool/test/test_examples.py b/GHEtool/test/test_examples.py index e8c6bce6..e2b72e19 100644 --- a/GHEtool/test/test_examples.py +++ b/GHEtool/test/test_examples.py @@ -85,3 +85,9 @@ def test_tilted(monkeypatch): monkeypatch.setattr(plt, 'show', lambda: None) from GHEtool.Examples.tilted_borefield import tilted tilted() + + +def test_optimise(monkeypatch): + monkeypatch.setattr(plt, 'show', lambda: None) + from GHEtool.Examples.optimise_load_profile import optimise + optimise() diff --git a/GHEtool/test/unit-tests/test_loads/test_hourly_load_building.py b/GHEtool/test/unit-tests/test_loads/test_hourly_load_building.py index 20518391..b2138b9e 100644 --- a/GHEtool/test/unit-tests/test_loads/test_hourly_load_building.py +++ b/GHEtool/test/unit-tests/test_loads/test_hourly_load_building.py @@ -51,11 +51,16 @@ def test_load_hourly_data(): load1.load_hourly_profile(FOLDER.joinpath("Examples/hourly_profile.csv"), col_heating=1, col_cooling=0) assert np.array_equal(load.hourly_cooling_load, load1.hourly_heating_load) assert np.array_equal(load.hourly_heating_load, load1.hourly_cooling_load) + assert np.array_equal(load.hourly_dhw_load, load1.hourly_dhw_load) load2 = HourlyBuildingLoad() load2.load_hourly_profile(FOLDER.joinpath("test/methods/hourly_data/hourly_profile_without_header.csv"), header=False) assert np.array_equal(load.hourly_cooling_load, load2.hourly_cooling_load) assert np.array_equal(load.hourly_heating_load, load2.hourly_heating_load) + assert np.array_equal(load.hourly_dhw_load, load2.hourly_dhw_load) + load2.load_hourly_profile(FOLDER.joinpath("test/methods/hourly_data/hourly_profile_without_header.csv"), + header=False, col_heating=1, col_cooling=0, col_dhw=1) + assert np.allclose(load2.hourly_dhw_load, load2.hourly_heating_load) def test_checks(): @@ -161,6 +166,8 @@ def test_set_hourly_values(): load.set_hourly_heating_load(np.ones(10)) with pytest.raises(ValueError): load.set_hourly_cooling_load(np.ones(10)) + with pytest.raises(ValueError): + load.set_hourly_dhw_load(np.ones(10)) def test_start_month_general(): @@ -244,6 +251,12 @@ def test_dhw(): load.dhw = 'test' with pytest.raises(ValueError): load.dhw = np.full(120, 10) + with pytest.raises(ValueError): + load.set_hourly_dhw_load(-100) + with pytest.raises(ValueError): + load.set_hourly_dhw_load('test') + with pytest.raises(ValueError): + load.set_hourly_dhw_load(np.full(120, 10)) assert np.allclose(load.dhw, 0) assert np.allclose(load.hourly_dhw_load_simulation_period, np.zeros(87600)) diff --git a/GHEtool/test/unit-tests/test_loads/test_hourly_load_building_multi_year.py b/GHEtool/test/unit-tests/test_loads/test_hourly_load_building_multi_year.py index ab6c899c..3eb3d685 100644 --- a/GHEtool/test/unit-tests/test_loads/test_hourly_load_building_multi_year.py +++ b/GHEtool/test/unit-tests/test_loads/test_hourly_load_building_multi_year.py @@ -113,6 +113,12 @@ def test_dhw(): load.dhw = 'test' with pytest.raises(ValueError): load.dhw = np.full(120, 10) + with pytest.raises(ValueError): + load.set_hourly_dhw_load(-100) + with pytest.raises(ValueError): + load.set_hourly_dhw_load('test') + with pytest.raises(ValueError): + load.set_hourly_dhw_load(np.full(120, 10)) assert np.allclose(load.dhw, 0) assert np.allclose(load.hourly_dhw_load_simulation_period, np.zeros(87600))