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.
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.
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% |
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