MassimoCimmino / pygfunction

An open-source toolbox for the evaluation of thermal response factors (g-functions) of geothermal borehole fields.
BSD 3-Clause "New" or "Revised" License
46 stars 20 forks source link

Enhance timing performance for initialization of pipe objects #183

Closed j-c-cook closed 2 years ago

j-c-cook commented 2 years ago

Computing a g-function using the equivalent borehole method appears to take longer for a multiple u-tube layout. The attached times are from examples in the soon to be open-source, Ground Heat Exchanger Design Tool,ghedt. I believe the timing differences are a result of the multiple U-tube being more complex to model, and not a programming error. I think it would be nice to make this known, though I'm not sure where the right place for it would be.

image

MassimoCimmino commented 2 years ago

This does not seem consistent with the timings observed in #170 for <pipe>.coefficients_borehole_heat_extraction_rates().

Initialization of pipe objects might be the issue:

>>> %timeit gt.pipes.SingleUTube(pos_single, rp_in, rp_out, borehole, k_s, k_g, R_fp)
6.74 ms ± 18.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit gt.pipes.MultipleUTube(pos_double, rp_in, rp_out, borehole, k_s, k_g, R_fp, nPipes=2, config='series')
63.4 ms ± 425 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

This seems to be due to the multipole method:

>>> pos_single = [(-0.052, 0.0), (0.052, 0.0)]
>>> pos_double = [(-0.052, 0.0), (0.0, -0.052), (0.052, 0.0), (0.0, 0.052)]
>>> r_out = 0.0211
>>> r_b = 0.075
>>> k_s = 2.0
>>> k_g = 1.0
>>> R_fp = 0.3205
>>> T_b = 0.
>>> Q_single = np.array([1., 1.])
>>> Q_double = np.array([1., 1., 1., 1.])
>>> %timeit gt.pipes.thermal_resistances(pos_single, r_out, r_b, k_s, k_g, R_fp, J=2)
6.76 ms ± 98.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit gt.pipes.multipole(pos_single, r_out, r_b, k_s, k_g, R_fp, T_b, Q_single, 0)
48.9 µs ± 234 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit gt.pipes.multipole(pos_single, r_out, r_b, k_s, k_g, R_fp, T_b, Q_single, 1)
318 µs ± 1.09 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit gt.pipes.multipole(pos_single, r_out, r_b, k_s, k_g, R_fp, T_b, Q_single, 2)
3.38 ms ± 12.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit gt.pipes.thermal_resistances(pos_double, r_out, r_b, k_s, k_g, R_fp, J=2)
63 ms ± 310 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
>>> %timeit gt.pipes.multipole(pos_double, r_out, r_b, k_s, k_g, R_fp, T_b, Q_double , 0)
139 µs ± 301 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit gt.pipes.multipole(pos_double, r_out, r_b, k_s, k_g, R_fp, T_b, Q_double , 1)
12.1 ms ± 54 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit gt.pipes.multipole(pos_double, r_out, r_b, k_s, k_g, R_fp, T_b, Q_double , 2)
15.9 ms ± 45.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

I could investigate how to make these functions run faster. There is no vectorization in the current implementation. Calls to binom() might also slow things down.

Also, after #120 I plan to also rework the networks module. Right now, array coefficients for calculations (such as borehole_heat_extraction_rates) are calculated for all boreholes in the field regardless of if they are the same or not. The refactored module should be able to evaluate arrays only once if all boreholes are the same.

j-c-cook commented 2 years ago

The timing difference I am seeing could be due to initialization alone, and not computation of a single effective borehole thermal resistance. I have a update_thermal_resistance function that performs re-initialization often. There is now an update thermal resistances function as of the merge of #169 (closed #148), so I'll see if your current implementation would be faster.

MassimoCimmino commented 2 years ago

c5488f1 is already much faster (I have not yet validated the results):

>>> pos_single = [(-0.052, 0.0), (0.052, 0.0)]
>>> pos_double = [(-0.052, 0.0), (0.0, -0.052), (0.052, 0.0), (0.0, 0.052)]
>>> r_out = 0.0211
>>> r_b = 0.075
>>> k_s = 2.0
>>> k_g = 1.0
>>> R_fp = 0.3205
>>> T_b = 0.
>>> Q_single = np.array([1., 1.])
>>> Q_double = np.array([1., 1., 1., 1.])
>>> %timeit gt.pipes.thermal_resistances(pos_single, r_out, r_b, k_s, k_g, R_fp, J=2)
4.2 ms ± 9.73 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit gt.pipes.multipole(pos_single, r_out, r_b, k_s, k_g, R_fp, T_b, Q_single, 0)
49.7 µs ± 303 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit gt.pipes.multipole(pos_single, r_out, r_b, k_s, k_g, R_fp, T_b, Q_single, 1)
712 µs ± 1.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit gt.pipes.multipole(pos_single, r_out, r_b, k_s, k_g, R_fp, T_b, Q_single, 2)
2.07 ms ± 9.04 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit gt.pipes.thermal_resistances(pos_double, r_out, r_b, k_s, k_g, R_fp, J=2)
10 ms ± 22.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit gt.pipes.multipole(pos_double, r_out, r_b, k_s, k_g, R_fp, T_b, Q_double , 0)
51.2 µs ± 317 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit gt.pipes.multipole(pos_double, r_out, r_b, k_s, k_g, R_fp, T_b, Q_double , 1)
1.08 ms ± 2.02 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit gt.pipes.multipole(pos_double, r_out, r_b, k_s, k_g, R_fp, T_b, Q_double , 2)
2.54 ms ± 69 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Please let me know how that affects your calculation times.

I will look into binom() next.

j-c-cook commented 2 years ago

By considering your changes in #184 up through c5488f10cdd96d99f2cc9b298dff2855112cd391, the computation time for the double U-tube field selection and sizing is about 3.6 times faster.

HighLevel/find_near_square.py results
=====================================
* Single U-tube
---------------
Calculation time: 18.09 seconds
Height: 130.1818 meters
Number of boreholes: 156
Total Drilling: 20308.4 meters

* Double U-tube
---------------
Calculation time: 28.34 seconds
Height: 133.2133 meters
Number of boreholes: 144
Total Drilling: 19182.7 meters

* Coaxial tube
--------------
Calculation time: 17.93 seconds
Height: 132.7505 meters
Number of boreholes: 144
Total Drilling: 19116.1 meters
MassimoCimmino commented 2 years ago

There does not seem to have much time saving potential in modifying the calculation of binomial coefficients.

I quickly implemented memoization for the pygfunction.pipes.thermal_resistances function in afd94dd. This makes it so that consecutive calls to the thermal_resistances function using the same inputs returns the same output without calculation. This way, when initializing a network of pipe objects, thermal resistances will be calculated once for the entire field, instead of being calculated for every borehole. This should greatly reduce the calculation time for your use case.

j-c-cook commented 2 years ago

You are correct. The changes have greatly reduced the calculation time for my use case.

HighLevel/find_near_square.py results
=====================================
* Single U-tube
---------------
Calculation time: 12.82 seconds
Height: 130.1818 meters
Number of boreholes: 156
Total Drilling: 20308.4 meters

* Double U-tube
---------------
Calculation time: 15.45 seconds
Height: 133.2133 meters
Number of boreholes: 144
Total Drilling: 19182.7 meters

* Coaxial tube
--------------
Calculation time: 16.70 seconds
Height: 132.7505 meters
Number of boreholes: 144
Total Drilling: 19116.1 meters

In total, this is about a 1.7x, 8.6x and 1.2x decrease in computation time for the single U-tube, double U-tube and coaxial tube near-square design routines respectively. Thank you.