v2.1.1

In GHEtool v2.1.1 there are two major code changes that reduce the computational time significantly. One has to do with the way the sizing methodology (L3/L4) is implemented and another with the new Gfunction class. Both improvements are explained below.

Improvement in sizing

Previously, in v2.1.0, for the sizing methodology L3 and L4 (i.e. monthly and hourly), the temperatures where calculated every time step. This however (especially for long simulation periods) requires a lot of time due to the convolution step, especially for an hourly sizing. In v2.1.1 this is changed so that only the first and last year are calculated, since only these years are relevant for the sizing 1. This means that however long the simulation period may be, only two years are calculated. In the table below, the time required for one sizing iteration (i.e. one g-value convolution) for an hourly sizing is shown in µs. The code to come up with these numbers is added below.

Speed improvement for g-value convolution in hourly sizing

Simulation period [years]

Required time old method [µs]

Required time new method [µs]

5 years

15625 µs

0 µs

15 years

15625 µs

0 µs

25 years

31250 µs

0 µs

35 years

46875 µs

0 µs

45 years

62500 µs

0 µs

55 years

78125 µs

0 µs

65 years

78125 µs

0 µs

75 years

78125 µs

0 µs

85 years

109375 µs

0 µs

95 years

125000 µs

0 µs

Gfunction class

Due to the implementation of the GFunction class in GHEtool, a substantial speed improvement is made w.r.t. GHEtool v2.1.0 for computationally expensive tasks. In the tables below, one can find this speed improvement for the different sizing methods and for several of the examples documents. The results can be recreated by running the code below. The computational times as shown in the table below, are an average of 5 runs.

Speed benchmark sizing methods

Sizing method

Time v2.1.0 [ms]

Time v2.1.1 [ms]

Improvement [%]

L2 (three pulse) sizing

1.84 ms

1.43 ms

28%

L3 (monthly) sizing

12.34 ms

5.8 ms

113%

L4 (hourly) sizing

4.21 ms

3.63 ms

16%

Speed benchmark examples

Example

Time v2.1.0 [ms]

Time v2.1.1 [ms]

Improvement [%]

Main functionalities

3.38 ms

2.57 ms

32%

Optimise load profile

15.07 ms

0.63 ms

2305%

Sizing with Rb calculation

9.97 ms

9.99 ms

0%

Effect borefield configuration

1.57 ms

1.5 ms

4%

  1import numpy as np
  2import pygfunction as gt
  3from GHEtool import GroundConstantTemperature, Borefield, HourlyGeothermalLoad, FOLDER
  4from scipy.signal import convolve
  5from math import pi
  6from time import process_time_ns
  7
  8
  9def test_new_calc_method(simulation_period: int):
 10    """
 11    Test the new calculation method which is just considering the first and last year.
 12
 13    Parameters
 14    ----------
 15    simulation_period : int
 16        simulation period [years]
 17
 18    Returns
 19    -------
 20        None
 21
 22    Raises
 23    -------
 24        AssertionError
 25    """
 26
 27    h = 110
 28
 29    # initiate ground data
 30    data = GroundConstantTemperature(3, 10)
 31
 32    # initiate pygfunction borefield model
 33    borefield_gt = gt.boreholes.rectangle_field(10, 10, 6, 6, 110, 1, 0.075)
 34
 35    # initiate borefield
 36    borefield = Borefield(100)
 37
 38    # set borehole thermal equivalent resistance
 39    borefield.Rb = 0.12
 40
 41    # set ground data in borefield
 42    borefield.set_ground_parameters(data)
 43
 44    # set pygfunction borefield model
 45    borefield.set_borefield(borefield_gt)
 46
 47
 48    # load the hourly profile
 49    load = HourlyGeothermalLoad(simulation_period=simulation_period)
 50    load.load_hourly_profile(f'hourly_profile.csv', header=True, separator=";")
 51
 52    borefield.load = load
 53
 54    # borefield.g-function is a function that uses the precalculated data to interpolate the correct values of the
 55    # g-function. This dataset is checked over and over again and is correct
 56    g_values = borefield.gfunction(borefield.time_L4, borefield.H)
 57
 58    # get process time at start of new method
 59    dt1 = process_time_ns()
 60    # determine load
 61    loads_short = borefield.hourly_cooling_load - borefield.hourly_heating_load
 62    # reverse the load
 63    loads_short_rev = loads_short[::-1]
 64    # init results vector
 65    results = np.zeros(loads_short.size * 2)
 66    # calculation of needed differences of the g-function values. These are the weight factors in the calculation
 67    # of Tb.
 68    g_value_differences = np.diff(g_values, prepend=0)
 69
 70    # convolution to get the results for the first year
 71    results[:8760] = convolve(loads_short * 1000, g_value_differences[:8760])[:8760]
 72    # sum up g_values until the pre last year
 73    g_sum_n1 = g_value_differences[:8760 * (borefield.simulation_period - 1)].reshape(borefield.simulation_period - 1, 8760).sum(axis=0)
 74    # add up last year
 75    g_sum = g_sum_n1 + g_value_differences[8760 * (borefield.simulation_period - 1):]
 76    # add zero at start and reverse the order
 77    g_sum_n2 = np.concatenate((np.array([0]), g_sum_n1[::-1]))[:-1]
 78    # determine results for the last year by the influence of the year (first term) and the previous years (last term)
 79    results[8760:] = convolve(loads_short * 1000, g_sum)[:8760] + convolve(loads_short_rev * 1000, g_sum_n2)[:8760][::-1]
 80    # calculation the borehole wall temperature for every month i
 81    t_b = results / (2 * pi * borefield.ground_data.k_s) / (borefield.H * borefield.number_of_boreholes) + borefield._Tg(borefield.H)
 82
 83    # get process time
 84    dt2 = process_time_ns()
 85    # determine hourly load
 86    hourly_load = np.tile(borefield.hourly_cooling_load - borefield.hourly_heating_load, borefield.simulation_period)
 87    # calculation of needed differences of the g-function values. These are the weight factors in the calculation
 88    # of Tb.
 89    g_value_differences = np.diff(g_values, prepend=0)
 90
 91    # convolution to get the monthly results
 92    results = convolve(hourly_load * 1000, g_value_differences)[:hourly_load.size]
 93
 94    # calculation the borehole wall temperature for every month i
 95    t_b_new = results / (2 * pi * borefield.ground_data.k_s) / (h * borefield.number_of_boreholes) + borefield._Tg(h)
 96
 97    # print time for the different methods
 98    print(f'simulation period: {simulation_period}; old: {(process_time_ns() - dt2)/1000:.0f} µs;'
 99          f'new: {(dt2 - dt1)/1000:.0f} µs')
100    # compare results to ensure they are the same
101    assert np.allclose(t_b[:8760], t_b_new[:8760])
102    assert np.allclose(t_b[8760:], t_b_new[8760*(borefield.simulation_period - 1):])
103
104
105if __name__ == "__main__":
106    for sim_year in np.arange(5, 101, 10):
107        test_new_calc_method(sim_year)
  1from GHEtool import *
  2import numpy as np
  3import pygfunction as gt
  4import time
  5import os, contextlib
  6
  7from GHEtool.Examples.main_functionalities import main_functionalities
  8from GHEtool.Examples.sizing_with_Rb_calculation import sizing_with_Rb
  9from GHEtool.Examples.effect_of_borehole_configuration import effect_borefield_configuration
 10from GHEtool import HourlyGeothermalLoad
 11
 12# disable the plot function by monkey patching over it
 13Borefield._plot_temperature_profile = lambda *args, **kwargs: None
 14
 15
 16# disable the print outputs
 17def supress_stdout(func):
 18    def wrapper(*a, **ka):
 19        with open(os.devnull, 'w') as devnull:
 20            with contextlib.redirect_stdout(devnull):
 21                return func(*a, **ka)
 22    return wrapper
 23
 24
 25@supress_stdout
 26def run_without_messages(callable) -> None:
 27    """
 28    This function runs the callable without messages.
 29
 30    Parameters
 31    ----------
 32    callable : Callable
 33        Function to be called
 34
 35    Returns
 36    -------
 37    None
 38    """
 39    callable()
 40
 41
 42def optimise_load_profile() -> None:
 43    """
 44    This is a benchmark for the optimise load profile method.
 45
 46    Returns
 47    -------
 48    None
 49    """
 50    # initiate ground data
 51    data = GroundData(3, 10, 0.2)
 52
 53    # initiate pygfunction borefield model
 54    borefield_gt = gt.boreholes.rectangle_field(10, 10, 6, 6, 110, 1, 0.075)
 55
 56    # initiate borefield
 57    borefield = Borefield()
 58
 59    # set ground data in borefield
 60    borefield.set_ground_parameters(data)
 61
 62    # set pygfunction borefield
 63    borefield.set_borefield(borefield_gt)
 64
 65    # load the hourly profile
 66    load = HourlyGeothermalLoad()
 67    load.load_hourly_profile("hourly_profile.csv", header=True, separator=";")
 68    borefield.load = load
 69
 70    # optimise the load for a 10x10 field (see data above) and a fixed depth of 150m.
 71    borefield.optimise_load_profile(depth=150, print_results=False)
 72
 73
 74def size_L2() -> None:
 75    """
 76    This is a benchmark for the L2 sizing method.
 77
 78    Returns
 79    -------
 80    None
 81    """
 82    number_of_iterations = 5
 83    max_value_cooling = 700
 84    max_value_heating = 800
 85
 86    monthly_load_cooling_array = np.empty((number_of_iterations, 12))
 87    monthly_load_heating_array = np.empty((number_of_iterations, 12))
 88    peak_load_cooling_array = np.empty((number_of_iterations, 12))
 89    peak_load_heating_array = np.empty((number_of_iterations, 12))
 90
 91    # populate arrays with random values
 92    for i in range(number_of_iterations):
 93        for j in range(12):
 94            monthly_load_cooling_array[i, j] = np.random.randint(0, max_value_cooling)
 95            monthly_load_heating_array[i, j] = np.random.randint(0, max_value_heating)
 96            peak_load_cooling_array[i, j] = np.random.randint(monthly_load_cooling_array[i, j], max_value_cooling)
 97            peak_load_heating_array[i, j] = np.random.randint(monthly_load_heating_array[i, j], max_value_heating)
 98
 99    # initiate borefield model
100    data = GroundData(3, 10, 0.2)
101    borefield_gt = gt.boreholes.rectangle_field(10, 12, 6, 6, 110, 1, 0.075)
102
103    # Monthly loading values
104    peak_cooling = np.array([0., 0, 34., 69., 133., 187., 213., 240., 160., 37., 0., 0.])  # Peak cooling in kW
105    peak_heating = np.array([160., 142, 102., 55., 0., 0., 0., 0., 40.4, 85., 119., 136.])  # Peak heating in kW
106
107    # annual heating and cooling load
108    annual_heating_load = 300 * 10 ** 3  # kWh
109    annual_cooling_load = 160 * 10 ** 3  # kWh
110
111    # percentage of annual load per month (15.5% for January ...)
112    monthly_load_heating_percentage = np.array(
113        [0.155, 0.148, 0.125, .099, .064, 0., 0., 0., 0.061, 0.087, 0.117, 0.144])
114    monthly_load_cooling_percentage = np.array([0.025, 0.05, 0.05, .05, .075, .1, .2, .2, .1, .075, .05, .025])
115
116    # resulting load per month
117    monthly_load_heating = annual_heating_load * monthly_load_heating_percentage  # kWh
118    monthly_load_cooling = annual_cooling_load * monthly_load_cooling_percentage  # kWh
119
120    # create the borefield object
121    borefield = Borefield(simulation_period=20,
122                          peak_heating=peak_heating,
123                          peak_cooling=peak_cooling,
124                          baseload_heating=monthly_load_heating,
125                          baseload_cooling=monthly_load_cooling)
126    borefield.set_ground_parameters(data)
127    borefield.set_borefield(borefield_gt)
128
129    # set temperature boundaries
130    borefield.set_max_avg_fluid_temperature(16)  # maximum temperature
131    borefield.set_min_avg_fluid_temperature(0)  # minimum temperature
132
133    # size according to L2 method
134    for i in range(number_of_iterations):
135        borefield.set_baseload_cooling(monthly_load_cooling_array[i])
136        borefield.set_baseload_heating(monthly_load_heating_array[i])
137        borefield.set_peak_cooling(peak_load_cooling_array[i])
138        borefield.set_peak_heating(peak_load_heating_array[i])
139        borefield.size(100, L2_sizing=True)
140
141
142def size_L3() -> None:
143    """
144    This is a benchmark for the L3 sizing method.
145
146    Returns
147    -------
148    None
149    """
150    number_of_iterations = 5
151    max_value_cooling = 700
152    max_value_heating = 800
153
154    monthly_load_cooling_array = np.empty((number_of_iterations, 12))
155    monthly_load_heating_array = np.empty((number_of_iterations, 12))
156    peak_load_cooling_array = np.empty((number_of_iterations, 12))
157    peak_load_heating_array = np.empty((number_of_iterations, 12))
158
159    # populate arrays with random values
160    for i in range(number_of_iterations):
161        for j in range(12):
162            monthly_load_cooling_array[i, j] = np.random.randint(0, max_value_cooling)
163            monthly_load_heating_array[i, j] = np.random.randint(0, max_value_heating)
164            peak_load_cooling_array[i, j] = np.random.randint(monthly_load_cooling_array[i, j], max_value_cooling)
165            peak_load_heating_array[i, j] = np.random.randint(monthly_load_heating_array[i, j], max_value_heating)
166
167    # initiate borefield model
168    data = GroundData(3, 10, 0.2)
169    borefield_gt = gt.boreholes.rectangle_field(10, 12, 6, 6, 110, 1, 0.075)
170
171    # Monthly loading values
172    peak_cooling = np.array([0., 0, 34., 69., 133., 187., 213., 240., 160., 37., 0., 0.])  # Peak cooling in kW
173    peak_heating = np.array([160., 142, 102., 55., 0., 0., 0., 0., 40.4, 85., 119., 136.])  # Peak heating in kW
174
175    # annual heating and cooling load
176    annual_heating_load = 300 * 10 ** 3  # kWh
177    annual_cooling_load = 160 * 10 ** 3  # kWh
178
179    # percentage of annual load per month (15.5% for January ...)
180    monthly_load_heating_percentage = np.array(
181        [0.155, 0.148, 0.125, .099, .064, 0., 0., 0., 0.061, 0.087, 0.117, 0.144])
182    monthly_load_cooling_percentage = np.array([0.025, 0.05, 0.05, .05, .075, .1, .2, .2, .1, .075, .05, .025])
183
184    # resulting load per month
185    monthly_load_heating = annual_heating_load * monthly_load_heating_percentage  # kWh
186    monthly_load_cooling = annual_cooling_load * monthly_load_cooling_percentage  # kWh
187
188    # create the borefield object
189    borefield = Borefield(simulation_period=20,
190                          peak_heating=peak_heating,
191                          peak_cooling=peak_cooling,
192                          baseload_heating=monthly_load_heating,
193                          baseload_cooling=monthly_load_cooling)
194    borefield.set_ground_parameters(data)
195    borefield.set_borefield(borefield_gt)
196
197    # set temperature boundaries
198    borefield.set_max_avg_fluid_temperature(16)  # maximum temperature
199    borefield.set_min_avg_fluid_temperature(0)  # minimum temperature
200
201    # size according to L3 method
202    for i in range(number_of_iterations):
203        borefield.set_baseload_cooling(monthly_load_cooling_array[i])
204        borefield.set_baseload_heating(monthly_load_heating_array[i])
205        borefield.set_peak_cooling(peak_load_cooling_array[i])
206        borefield.set_peak_heating(peak_load_heating_array[i])
207        borefield.size(100, L3_sizing=True)
208
209
210def size_L4() -> None:
211    """
212    This is a benchmark for the L4 sizing method.
213
214    Returns
215    -------
216    None
217    """
218    # initiate ground data
219    data = GroundData(3, 10, 0.2)
220
221    # initiate pygfunction borefield model
222    borefield_gt = gt.boreholes.rectangle_field(10, 10, 6, 6, 110, 1, 0.075)
223
224    # initiate borefield
225    borefield = Borefield()
226
227    # set ground data in borefield
228    borefield.set_ground_parameters(data)
229
230    # set pygfunction borefield
231    borefield.set_borefield(borefield_gt)
232
233    # load the hourly profile
234    load = HourlyGeothermalLoad()
235    load.load_hourly_profile("hourly_profile.csv", header=True, separator=";")
236    borefield.load = load
237
238    borefield.size(L4_sizing=True)
239
240
241def benchmark(callable, name: str) -> None:
242    """
243    This function calls the callable five times and outputs the time needed to run the callable.
244
245    Parameters
246    ----------
247    callable : Callable
248        Function to be called
249    name : str
250        Name of the function
251
252    Returns
253    -------
254    None
255    """
256    diff, diff_without = 0., 0.
257
258    for i in range(5):
259        GFunction.DEFAULT_STORE_PREVIOUS_VALUES = True
260
261        start_time = time.time()
262        run_without_messages(callable)
263        end_time = time.time()
264        diff = diff * i/(i+1) + (end_time - start_time)/(i+1)
265
266        GFunction.DEFAULT_STORE_PREVIOUS_VALUES = False
267
268        start_time_without = time.time()
269        run_without_messages(callable)
270        end_time_without = time.time()
271        diff_without = diff_without * i/(i+1) + (end_time_without - start_time_without)/(i+1)
272
273    print(f'{name} took  {round(diff_without, 2)} ms in v2.1.0 and '
274          f'{round(diff, 2)} ms in v2.1.1. This is an improvement of {round((diff_without-diff)/diff*100)}%.')
275
276
277# run examples
278benchmark(main_functionalities, "Main functionalities")
279benchmark(optimise_load_profile, "Optimise load profile")
280benchmark(sizing_with_Rb, "Sizing with Rb calculation")
281benchmark(effect_borefield_configuration, "Effect borehole configuration")
282
283# run benchmark sizing methods
284benchmark(size_L2, "Sizing with L2 method")
285benchmark(size_L3, "Sizing with L3 method")
286benchmark(size_L4, "Sizing with L4 (hourly) method")

References

1

Peere, W., Picard, D., Cupeiro Figueroa, I., Boydens, W., and Helsen, L. (2021) Validated combined first and last year borefield sizing methodology. In Proceedings of International Building Simulation Conference 2021. Brugge (Belgium), 1-3 September 2021. https://doi.org/10.26868/25222708.2021.30180