diff --git a/CHANGELOG.md b/CHANGELOG.md index 2996834..7bfb392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [Issue 215](https://github.com/MassimoCimmino/pygfunction/issues/215) - Implemented variable fluid mass flow rate g-functions. Bore fields with series-connected boreholes and reversible flow direction can now be simulated. * [Issue 282](https://github.com/MassimoCimmino/pygfunction/issues/282) - Enabled the use of negative mass flow rates in `Pipe` and `Network` classes to model reversed flow direction. +* [Pull Request 308](https://github.com/MassimoCimmino/pygfunction/pull/308) - Introduced a new `borefield` module. The new `Borefield` class replaces lists of `Borehole` objects as the preferred way to configure bore fields. The `Borefield.evaluate_g_function` method evaluates g-functions using the 'UHTR' and 'UBWT' boundary conditions. Deprecated bore field creation functions in the `boreholes` module (e.g. `boreholes.rectangle_field()`). These functions are replaced by methods of the new `Borefield` class. They will be removed in `v3.0.0`. ## Version 2.2.3 (2024-07-01) diff --git a/doc/requirements.txt b/doc/requirements.txt index 41f98c3..1f428e6 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -5,3 +5,4 @@ numpydoc >= 1.2.0 recommonmark >= 0.6.0 sphinx >= 4.4.0 secondarycoolantprops >= 1.1.0 +typing_extensions >= 4.0.1 diff --git a/doc/source/install.rst b/doc/source/install.rst index ce54062..383b688 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -4,11 +4,12 @@ Setting up pygfunction ********************** -*pygfunction* uses Python 3.7, along with the following packages: +*pygfunction* uses Python 3.8, along with the following packages: - matplotlib (>= 3.5.1), - numpy (>= 1.21.5) - scipy (>= 1.7.3) - SecondaryCoolantProps (>= 1.1) + - typing_extensions (>= 4.0.1) *pygfunction*'s- documentation is built using: - sphinx (>= 4.4.0) diff --git a/examples/bore_field_thermal_resistance.py b/examples/bore_field_thermal_resistance.py index 54a7079..f8b3193 100644 --- a/examples/bore_field_thermal_resistance.py +++ b/examples/bore_field_thermal_resistance.py @@ -9,7 +9,6 @@ import matplotlib.pyplot as plt import numpy as np from matplotlib.ticker import AutoMinorLocator -from scipy import pi import pygfunction as gt @@ -61,15 +60,11 @@ def main(): # Borehole field # ------------------------------------------------------------------------- - boreField = [] - bore_connectivity = [] - for i in range(nBoreholes): - x = i*B - borehole = gt.boreholes.Borehole(H, D, r_b, x, 0.) - boreField.append(borehole) - # Boreholes are connected in series: The index of the upstream - # borehole is that of the previous borehole - bore_connectivity.append(i - 1) + x = np.arange(nBoreholes) * B + borefield = gt.borefield.Borefield(H, D, r_b, x, 0.) + # Boreholes are connected in series: The index of the upstream + # borehole is that of the previous borehole + bore_connectivity = [i - 1 for i in range(nBoreholes)] # ------------------------------------------------------------------------- # Evaluate the effective bore field thermal resistance @@ -91,16 +86,16 @@ def main(): # Fluid to inner pipe wall thermal resistance (Single U-tube) h_f = gt.pipes.convective_heat_transfer_coefficient_circular_pipe( m_flow_pipe, r_in, mu_f, rho_f, k_f, cp_f, epsilon) - R_f = 1.0/(h_f*2*pi*r_in) + R_f = 1.0 / (h_f * 2 * np.pi * r_in) # Single U-tube, same for all boreholes in the bore field UTubes = [] - for borehole in boreField: + for borehole in borefield: SingleUTube = gt.pipes.SingleUTube( pos_pipes, r_in, r_out, borehole, k_s, k_g, R_f + R_p) UTubes.append(SingleUTube) network = gt.networks.Network( - boreField[:nBoreholes], + borefield[:nBoreholes], UTubes[:nBoreholes], bore_connectivity=bore_connectivity[:nBoreholes]) diff --git a/examples/comparison_gfunction_solvers.py b/examples/comparison_gfunction_solvers.py index f31528e..89ad678 100644 --- a/examples/comparison_gfunction_solvers.py +++ b/examples/comparison_gfunction_solvers.py @@ -58,22 +58,23 @@ def main(): # Field of 6x4 (n=24) boreholes N_1 = 6 N_2 = 4 - field = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) + borefield = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) # ------------------------------------------------------------------------- # Evaluate g-functions # ------------------------------------------------------------------------- t0 = perf_counter() gfunc_detailed = gt.gfunction.gFunction( - field, alpha, time=time, options=options, method='detailed') + borefield, alpha, time=time, options=options, method='detailed') t1 = perf_counter() t_detailed = t1 - t0 gfunc_similarities = gt.gfunction.gFunction( - field, alpha, time=time, options=options, method='similarities') + borefield, alpha, time=time, options=options, method='similarities') t2 = perf_counter() t_similarities = t2 - t1 gfunc_equivalent = gt.gfunction.gFunction( - field, alpha, time=time, options=options, method='equivalent') + borefield, alpha, time=time, options=options, method='equivalent') t3 = perf_counter() t_equivalent = t3 - t2 diff --git a/examples/comparison_load_aggregation.py b/examples/comparison_load_aggregation.py index 37833c7..2825f49 100644 --- a/examples/comparison_load_aggregation.py +++ b/examples/comparison_load_aggregation.py @@ -16,7 +16,6 @@ import matplotlib.pyplot as plt import numpy as np -from scipy.constants import pi from scipy.interpolate import interp1d from scipy.signal import fftconvolve @@ -64,12 +63,12 @@ def main(): # ------------------------------------------------------------------------- # The field contains only one borehole - boreField = [gt.boreholes.Borehole(H, D, r_b, x=0., y=0.)] + borehole = gt.boreholes.Borehole(H, D, r_b, x=0., y=0.) # Evaluate the g-function on a geometrically expanding time grid time_gFunc = gt.utilities.time_geometric(dt, tmax, 50) # Calculate g-function gFunc = gt.gfunction.gFunction( - boreField, alpha, time=time_gFunc, options=options) + borehole, alpha, time=time_gFunc, options=options) # ------------------------------------------------------------------------- # Simulation @@ -88,7 +87,7 @@ def main(): bounds_error=False, fill_value=(0., gFunc.gFunc[-1]))(time_req) # Initialize load aggregation scheme - LoadAgg.initialize(gFunc_int/(2*pi*k_s)) + LoadAgg.initialize(gFunc_int / (2 * np.pi * k_s)) tic = perf_counter() for i in range(Nt): @@ -116,7 +115,8 @@ def main(): g = interp1d(time_gFunc, gFunc.gFunc)(time) # Convolution in Fourier domain - T_b_exact = T_g - fftconvolve(dQ, g/(2.0*pi*k_s*H), mode='full')[0:Nt] + T_b_exact = T_g - fftconvolve( + dQ, g / (2.0 * np.pi * k_s * H), mode='full')[0:Nt] # ------------------------------------------------------------------------- # plot results @@ -186,14 +186,14 @@ def synthetic_load(x): func = (168.0-C)/168.0 for i in [1, 2, 3]: - func += 1.0/(i*pi)*(np.cos(C*pi*i/84.0) - 1.0) \ - *(np.sin(pi*i/84.0*(x - B))) - func = func*A*np.sin(pi/12.0*(x - B)) \ - *np.sin(pi/4380.0*(x - B)) + func += 1.0/(i*np.pi)*(np.cos(C*np.pi*i/84.0) - 1.0) \ + *(np.sin(np.pi*i/84.0*(x - B))) + func = func*A*np.sin(np.pi/12.0*(x - B)) \ + *np.sin(np.pi/4380.0*(x - B)) y = func + (-1.0)**np.floor(D/8760.0*(x - B))*abs(func) \ + E*(-1.0)**np.floor(D/8760.0*(x - B)) \ - /np.sign(np.cos(D*pi/4380.0*(x - F)) + G) + /np.sign(np.cos(D*np.pi/4380.0*(x - F)) + G) return -y diff --git a/examples/custom_bore_field.py b/examples/custom_bore_field.py index 96108c8..55444c0 100644 --- a/examples/custom_bore_field.py +++ b/examples/custom_bore_field.py @@ -2,6 +2,8 @@ """ Example of definition of a bore field using custom borehole positions. """ +import numpy as np + import pygfunction as gt @@ -20,32 +22,27 @@ def main(): # Position 1 has a borehole that is directly on top of another bore # Position 2 has a borehole with radius inside of another bore # The duplicates will be removed with the remove_duplicates function - pos = [(0.0, 0.0), - (0.0, 0.0), # Duplicate (for example purposes) - (0.03, 0.0), # Duplicate (for example purposes) - (5.0, 0.), - (3.5, 4.0), - (1.0, 7.0), - (5.5, 5.5)] + x = np.array([0., 0., 0.03, 5., 3.5, 1., 5.5]) + y = np.array([0., 0., 0.00, 0., 4.0, 7., 5.5]) # ------------------------------------------------------------------------- # Borehole field # ------------------------------------------------------------------------- # Build list of boreholes - field = [gt.boreholes.Borehole(H, D, r_b, x, y) for (x, y) in pos] + borefield = gt.borefield.Borefield(H, D, r_b, x, y) # ------------------------------------------------------------------------- # Find and remove duplicates from borehole field # ------------------------------------------------------------------------- - - field = gt.boreholes.remove_duplicates(field, disp=True) + borefield = gt.borefield.Borefield.from_boreholes( + gt.boreholes.remove_duplicates(borefield, disp=True)) # ------------------------------------------------------------------------- # Draw bore field # ------------------------------------------------------------------------- - gt.boreholes.visualize_field(field) + borefield.visualize_field() return diff --git a/examples/custom_bore_field_from_file.py b/examples/custom_bore_field_from_file.py index 576431c..f97a469 100644 --- a/examples/custom_bore_field_from_file.py +++ b/examples/custom_bore_field_from_file.py @@ -18,13 +18,13 @@ def main(): # ------------------------------------------------------------------------- # Build list of boreholes - field = gt.boreholes.field_from_file(filename) + borefield = gt.borefield.Borefield.from_file(filename) # ------------------------------------------------------------------------- # Draw bore field # ------------------------------------------------------------------------- - gt.boreholes.visualize_field(field) + borefield.visualize_field() return diff --git a/examples/discretize_boreholes.py b/examples/discretize_boreholes.py index 95b98ed..f4a03f3 100644 --- a/examples/discretize_boreholes.py +++ b/examples/discretize_boreholes.py @@ -88,9 +88,10 @@ def main(): # Field of 6x4 (n=24) boreholes N_1 = 6 N_2 = 4 - boreField = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) - gt.boreholes.visualize_field(boreField) - nBoreholes = len(boreField) + borefield = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) + gt.boreholes.visualize_field(borefield) + nBoreholes = len(borefield) # ------------------------------------------------------------------------- # Initialize pipe model @@ -107,14 +108,14 @@ def main(): # Single U-tube, same for all boreholes in the bore field UTubes = [] - for borehole in boreField: + for borehole in borefield: SingleUTube = gt.pipes.SingleUTube( pos_pipes, r_in, r_out, borehole, k_s, k_g, R_f + R_p) UTubes.append(SingleUTube) m_flow_network = m_flow_borehole*nBoreholes # Network of boreholes connected in parallel - network = gt.networks.Network(boreField, UTubes) + network = gt.networks.Network(borefield, UTubes) # ------------------------------------------------------------------------- # Evaluate the g-functions for the borefield @@ -128,7 +129,7 @@ def main(): # Calculate the g-function for uniform borehole wall temperature gfunc_UBWT_uniform = gt.gfunction.gFunction( - boreField, alpha, time=time, boundary_condition='UBWT', + borefield, alpha, time=time, boundary_condition='UBWT', options=options_uniform) # Compute g-function for the MIFT case with equal number of segments per @@ -139,7 +140,7 @@ def main(): # Calculate the g-function for uniform borehole wall temperature gfunc_UBWT_unequal = gt.gfunction.gFunction( - boreField, alpha, time=time, boundary_condition='UBWT', + borefield, alpha, time=time, boundary_condition='UBWT', options=options_unequal) # Compute the rmse between the reference cases and the discretized diff --git a/examples/equal_inlet_temperature.py b/examples/equal_inlet_temperature.py index b39cd65..2dab61a 100644 --- a/examples/equal_inlet_temperature.py +++ b/examples/equal_inlet_temperature.py @@ -10,7 +10,6 @@ import matplotlib.pyplot as plt import numpy as np from matplotlib.ticker import AutoMinorLocator -from scipy import pi import pygfunction as gt @@ -74,8 +73,9 @@ def main(): # Field of 6x4 (n=24) boreholes N_1 = 6 N_2 = 4 - boreField = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) - nBoreholes = len(boreField) + borefield = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) + nBoreholes = len(borefield) # ------------------------------------------------------------------------- # Initialize pipe model @@ -88,16 +88,16 @@ def main(): m_flow_pipe = m_flow_borehole h_f = gt.pipes.convective_heat_transfer_coefficient_circular_pipe( m_flow_pipe, r_in, mu_f, rho_f, k_f, cp_f, epsilon) - R_f = 1.0/(h_f*2*pi*r_in) + R_f = 1.0 / (h_f * 2 * np.pi * r_in) # Single U-tube, same for all boreholes in the bore field UTubes = [] - for borehole in boreField: + for borehole in borefield: SingleUTube = gt.pipes.SingleUTube( pos_pipes, r_in, r_out, borehole, k_s, k_g, R_f + R_p) UTubes.append(SingleUTube) m_flow_network = m_flow_borehole * nBoreholes - network = gt.networks.Network(boreField, UTubes) + network = gt.networks.Network(borefield, UTubes) # ------------------------------------------------------------------------- # Evaluate the g-functions for the borefield @@ -105,12 +105,12 @@ def main(): # Calculate the g-function for uniform heat extraction rate gfunc_uniform_Q = gt.gfunction.gFunction( - boreField, alpha, time=time, boundary_condition='UHTR', + borefield, alpha, time=time, boundary_condition='UHTR', options=options) # Calculate the g-function for uniform borehole wall temperature gfunc_uniform_T = gt.gfunction.gFunction( - boreField, alpha, time=time, boundary_condition='UBWT', + borefield, alpha, time=time, boundary_condition='UBWT', options=options) # Calculate the g-function for equal inlet fluid temperature diff --git a/examples/fluid_temperature.py b/examples/fluid_temperature.py index 77bdc30..4055fd0 100644 --- a/examples/fluid_temperature.py +++ b/examples/fluid_temperature.py @@ -13,7 +13,6 @@ """ import matplotlib.pyplot as plt import numpy as np -from scipy.constants import pi import pygfunction as gt @@ -85,16 +84,13 @@ def main(): # The field contains only one borehole borehole = gt.boreholes.Borehole(H, D, r_b, x=0., y=0.) - boreField = [borehole] # Get time values needed for g-function evaluation time_req = LoadAgg.get_times_for_simulation() # Calculate g-function gFunc = gt.gfunction.gFunction( - boreField, alpha, time=time_req, options=options) - # gt.gfunction.uniform_temperature(boreField, time_req, alpha, - # nSegments=nSegments) + borehole, alpha, time=time_req, options=options) # Initialize load aggregation scheme - LoadAgg.initialize(gFunc.gFunc/(2*pi*k_s)) + LoadAgg.initialize(gFunc.gFunc / (2 * np.pi * k_s)) # ------------------------------------------------------------------------- # Initialize pipe models @@ -107,11 +103,11 @@ def main(): # U-tube in series) h_f = gt.pipes.convective_heat_transfer_coefficient_circular_pipe( m_flow, rp_in, visc_f, den_f, k_f, cp_f, epsilon) - R_f_ser = 1.0/(h_f*2*pi*rp_in) + R_f_ser = 1.0 / (h_f * 2 * np.pi * rp_in) # Fluid to inner pipe wall thermal resistance (Double U-tube in parallel) h_f = gt.pipes.convective_heat_transfer_coefficient_circular_pipe( m_flow/2, rp_in, visc_f, den_f, k_f, cp_f, epsilon) - R_f_par = 1.0/(h_f*2*pi*rp_in) + R_f_par = 1.0 / (h_f * 2 * np.pi * rp_in) # Single U-tube SingleUTube = gt.pipes.SingleUTube(pos_single, rp_in, rp_out, @@ -284,13 +280,13 @@ def synthetic_load(x): func = (168.0-C)/168.0 for i in [1, 2, 3]: - func += 1.0/(i*pi)*(np.cos(C*pi*i/84.0)-1.0) \ - *(np.sin(pi*i/84.0*(x-B))) - func = func*A*np.sin(pi/12.0*(x-B)) \ - *np.sin(pi/4380.0*(x-B)) + func += 1.0/(i*np.pi)*(np.cos(C*np.pi*i/84.0)-1.0) \ + *(np.sin(np.pi*i/84.0*(x-B))) + func = func*A*np.sin(np.pi/12.0*(x-B)) \ + *np.sin(np.pi/4380.0*(x-B)) y = func + (-1.0)**np.floor(D/8760.0*(x-B))*abs(func) \ - + E*(-1.0)**np.floor(D/8760.0*(x-B))/np.sign(np.cos(D*pi/4380.0*(x-F))+G) + + E*(-1.0)**np.floor(D/8760.0*(x-B))/np.sign(np.cos(D*np.pi/4380.0*(x-F))+G) return -y diff --git a/examples/fluid_temperature_multiple_boreholes.py b/examples/fluid_temperature_multiple_boreholes.py index 4b8685c..cf5b8e9 100644 --- a/examples/fluid_temperature_multiple_boreholes.py +++ b/examples/fluid_temperature_multiple_boreholes.py @@ -11,7 +11,6 @@ """ import matplotlib.pyplot as plt import numpy as np -from scipy.constants import pi import pygfunction as gt @@ -82,8 +81,9 @@ def main(): # ------------------------------------------------------------------------- # The field is a retangular array - boreField = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) - nBoreholes = len(boreField) + borefield = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) + nBoreholes = len(borefield) # Pipe thermal resistance R_p = gt.pipes.conduction_thermal_resistance_circular_pipe( @@ -93,17 +93,17 @@ def main(): m_flow_pipe = m_flow_borehole/2 h_f = gt.pipes.convective_heat_transfer_coefficient_circular_pipe( m_flow_pipe, r_in, mu_f, rho_f, k_f, cp_f, epsilon) - R_f = 1.0/(h_f*2*pi*r_in) + R_f = 1.0 / (h_f * 2 * np.pi * r_in) # Double U-tube (parallel), same for all boreholes in the bore field UTubes = [] - for borehole in boreField: + for borehole in borefield: UTube = gt.pipes.MultipleUTube( pos, r_in, r_out, borehole, k_s, k_g, R_f + R_p, nPipes=2, config='parallel') UTubes.append(UTube) # Build a network object from the list of UTubes - network = gt.networks.Network(boreField, UTubes) + network = gt.networks.Network(borefield, UTubes) # ------------------------------------------------------------------------- # Calculate g-function @@ -116,7 +116,7 @@ def main(): network, alpha, time=time_req, m_flow_network=m_flow_network, cp_f=cp_f, boundary_condition='MIFT', options=options) # Initialize load aggregation scheme - LoadAgg.initialize(gFunc.gFunc / (2 * pi * k_s)) + LoadAgg.initialize(gFunc.gFunc / (2 * np.pi * k_s)) # ------------------------------------------------------------------------- # Simulation @@ -231,13 +231,13 @@ def synthetic_load(x): func = (168.0-C)/168.0 for i in [1, 2, 3]: - func += 1.0/(i*pi)*(np.cos(C*pi*i/84.0)-1.0) \ - *(np.sin(pi*i/84.0*(x-B))) - func = func*A*np.sin(pi/12.0*(x-B)) \ - *np.sin(pi/4380.0*(x-B)) + func += 1.0/(i*np.pi)*(np.cos(C*np.pi*i/84.0)-1.0) \ + *(np.sin(np.pi*i/84.0*(x-B))) + func = func*A*np.sin(np.pi/12.0*(x-B)) \ + *np.sin(np.pi/4380.0*(x-B)) y = func + (-1.0)**np.floor(D/8760.0*(x-B))*abs(func) \ - + E*(-1.0)**np.floor(D/8760.0*(x-B))/np.sign(np.cos(D*pi/4380.0*(x-F))+G) + + E*(-1.0)**np.floor(D/8760.0*(x-B))/np.sign(np.cos(D*np.pi/4380.0*(x-F))+G) return -y diff --git a/examples/fluid_temperature_reversible_flow_direction.py b/examples/fluid_temperature_reversible_flow_direction.py index e722f41..d8dca41 100644 --- a/examples/fluid_temperature_reversible_flow_direction.py +++ b/examples/fluid_temperature_reversible_flow_direction.py @@ -13,7 +13,6 @@ """ import matplotlib.pyplot as plt import numpy as np -from scipy.constants import pi import pygfunction as gt @@ -93,9 +92,10 @@ def main(): # ------------------------------------------------------------------------- # The field is a retangular array - boreField = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) - nBoreholes = len(boreField) - H_tot = np.sum([b.H for b in boreField]) + borefield = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) + nBoreholes = len(borefield) + H_tot = np.sum(borefield.H) # Boreholes are connected in series bore_connectivity = [i-1 for i in range(nBoreholes)] @@ -108,17 +108,17 @@ def main(): m_flow_pipe = np.max(np.abs(m_flow_borehole)) h_f = gt.pipes.convective_heat_transfer_coefficient_circular_pipe( m_flow_pipe, r_in, mu_f, rho_f, k_f, cp_f, epsilon) - R_f = 1.0 / (h_f * 2 * pi * r_in) + R_f = 1.0 / (h_f * 2 * np.pi * r_in) # Double U-tube (parallel), same for all boreholes in the bore field UTubes = [] - for borehole in boreField: + for borehole in borefield: UTube = gt.pipes.SingleUTube( pos_pipes, r_in, r_out, borehole, k_s, k_g, R_f + R_p) UTubes.append(UTube) # Build a network object from the list of UTubes network = gt.networks.Network( - boreField, UTubes, bore_connectivity=bore_connectivity) + borefield, UTubes, bore_connectivity=bore_connectivity) # ------------------------------------------------------------------------- # Calculate g-function @@ -131,7 +131,7 @@ def main(): network, alpha, time=time_req, boundary_condition='MIFT', m_flow_network=m_flow_network, cp_f=cp_f, options=options) # Initialize load aggregation scheme - LoadAgg.initialize(gFunc.gFunc / (2 * pi * k_s)) + LoadAgg.initialize(gFunc.gFunc / (2 * np.pi * k_s)) # ------------------------------------------------------------------------- # Simulation diff --git a/examples/inclined_boreholes.py b/examples/inclined_boreholes.py index b2b9a0e..d25ac2f 100644 --- a/examples/inclined_boreholes.py +++ b/examples/inclined_boreholes.py @@ -67,14 +67,15 @@ def main(): gt.utilities.cardinal_point('E')] # "Optimal" field of 8 boreholes - boreField1 = [] + boreholes = [] for i, orientation in enumerate(borehole_orientations): borehole = gt.boreholes.Borehole( H, D, r_b, i * B, 0., tilt=tilt, orientation=orientation) - boreField1.append(borehole) + boreholes.append(borehole) + borefield1 = gt.borefield.Borefield.from_boreholes(boreholes) # Visualize the borehole field - fig1 = gt.boreholes.visualize_field(boreField1) + fig1 = gt.boreholes.visualize_field(borefield1) """ Bore field #2 @@ -87,23 +88,24 @@ def main(): R = 3. # Borehole spacing from the center of the field (m) # Field of 6 boreholes in a circle - boreField2 = gt.boreholes.circle_field(N, R, H, D, r_b, tilt=tilt) + borefield2 = gt.borefield.Borefield.circle_field( + N, R, H, D, r_b, tilt=tilt) # Visualize the borehole field - fig2 = gt.boreholes.visualize_field(boreField2) + fig2 = gt.boreholes.visualize_field(borefield2) # ------------------------------------------------------------------------- # Evaluate g-functions for all fields # ------------------------------------------------------------------------- # Bore field #1 gfunc1 = gt.gfunction.gFunction( - boreField1, alpha, time=time, options=options, method='similarities') + borefield1, alpha, time=time, options=options, method='similarities') fig3 = gfunc1.visualize_g_function() fig3.suptitle('"Optimal" field of 8 boreholes') fig3.tight_layout() # Bore field #2 gfunc2 = gt.gfunction.gFunction( - boreField2, alpha, time=time, options=options, method='similarities') + borefield2, alpha, time=time, options=options, method='similarities') fig4 = gfunc2.visualize_g_function() fig4.suptitle(f'Field of {N} boreholes in a circle') fig4.tight_layout() diff --git a/examples/load_aggregation.py b/examples/load_aggregation.py index 1da0cb6..c0604a9 100644 --- a/examples/load_aggregation.py +++ b/examples/load_aggregation.py @@ -9,7 +9,6 @@ """ import matplotlib.pyplot as plt import numpy as np -from scipy.constants import pi from scipy.interpolate import interp1d from scipy.signal import fftconvolve @@ -52,14 +51,14 @@ def main(): # ------------------------------------------------------------------------- # The field contains only one borehole - boreField = [gt.boreholes.Borehole(H, D, r_b, x=0., y=0.)] + borehole = gt.boreholes.Borehole(H, D, r_b, x=0., y=0.) # Get time values needed for g-function evaluation time_req = LoadAgg.get_times_for_simulation() # Calculate g-function gFunc = gt.gfunction.gFunction( - boreField, alpha, time=time_req, options=options) + borehole, alpha, time=time_req, options=options) # Initialize load aggregation scheme - LoadAgg.initialize(gFunc.gFunc/(2*pi*k_s)) + LoadAgg.initialize(gFunc.gFunc / (2 * np.pi * k_s)) # ------------------------------------------------------------------------- # Simulation @@ -89,7 +88,8 @@ def main(): g = interp1d(time_req, gFunc.gFunc)(time) # Convolution in Fourier domain - T_b_exact = T_g - fftconvolve(dQ, g/(2.0*pi*k_s*H), mode='full')[0:Nt] + T_b_exact = T_g - fftconvolve( + dQ, g / (2.0 * np.pi * k_s * H), mode='full')[0:Nt] # ------------------------------------------------------------------------- # plot results @@ -146,13 +146,13 @@ def synthetic_load(x): func = (168.0-C)/168.0 for i in [1,2,3]: - func += 1.0/(i*pi)*(np.cos(C*pi*i/84.0)-1.0) \ - *(np.sin(pi*i/84.0*(x-B))) - func = func*A*np.sin(pi/12.0*(x-B)) \ - *np.sin(pi/4380.0*(x-B)) + func += 1.0/(i*np.pi)*(np.cos(C*np.pi*i/84.0)-1.0) \ + *(np.sin(np.pi*i/84.0*(x-B))) + func = func*A*np.sin(np.pi/12.0*(x-B)) \ + *np.sin(np.pi/4380.0*(x-B)) y = func + (-1.0)**np.floor(D/8760.0*(x-B))*abs(func) \ - + E*(-1.0)**np.floor(D/8760.0*(x-B))/np.sign(np.cos(D*pi/4380.0*(x-F))+G) + + E*(-1.0)**np.floor(D/8760.0*(x-B))/np.sign(np.cos(D*np.pi/4380.0*(x-F))+G) return -y diff --git a/examples/mixed_inlet_conditions.py b/examples/mixed_inlet_conditions.py index b34fec2..32c6ba9 100644 --- a/examples/mixed_inlet_conditions.py +++ b/examples/mixed_inlet_conditions.py @@ -11,7 +11,6 @@ import matplotlib.pyplot as plt import numpy as np from matplotlib.ticker import AutoMinorLocator -from scipy import pi import pygfunction as gt @@ -81,15 +80,12 @@ def main(): # Borehole field # ------------------------------------------------------------------------- - boreField = [] - bore_connectivity = [] - for i, H in enumerate(H_boreholes): - x = i*B - borehole = gt.boreholes.Borehole(H, D, r_b, x, 0.) - boreField.append(borehole) - # Boreholes are connected in series: The index of the upstream - # borehole is that of the previous borehole - bore_connectivity.append(i - 1) + nBoreholes = len(H_boreholes) + x = np.arange(nBoreholes) * B + borefield = gt.borefield.Borefield(H_boreholes, D, r_b, x, 0.) + # Boreholes are connected in series: The index of the upstream + # borehole is that of the previous borehole + bore_connectivity = [i - 1 for i in range(nBoreholes)] # ------------------------------------------------------------------------- # Initialize pipe model @@ -102,16 +98,16 @@ def main(): m_flow_pipe = np.max(np.abs(m_flow_borehole)) h_f = gt.pipes.convective_heat_transfer_coefficient_circular_pipe( m_flow_pipe, r_in, mu_f, rho_f, k_f, cp_f, epsilon) - R_f = 1.0/(h_f*2*pi*r_in) + R_f = 1.0 / (h_f * 2 * np.pi * r_in) # Single U-tube, same for all boreholes in the bore field UTubes = [] - for borehole in boreField: + for borehole in borefield: SingleUTube = gt.pipes.SingleUTube( pos_pipes, r_in, r_out, borehole, k_s, k_g, R_f + R_p) UTubes.append(SingleUTube) network = gt.networks.Network( - boreField, UTubes, bore_connectivity=bore_connectivity) + borefield, UTubes, bore_connectivity=bore_connectivity) # ------------------------------------------------------------------------- # Evaluate the g-functions for the borefield @@ -119,7 +115,7 @@ def main(): # Calculate the g-function for uniform temperature gfunc_Tb = gt.gfunction.gFunction( - boreField, alpha, time=time, boundary_condition='UBWT', + borefield, alpha, time=time, boundary_condition='UBWT', options=options, method=method) # Calculate the g-function for mixed inlet fluid conditions diff --git a/examples/multiple_independent_Utubes.py b/examples/multiple_independent_Utubes.py index ebb0444..658242a 100644 --- a/examples/multiple_independent_Utubes.py +++ b/examples/multiple_independent_Utubes.py @@ -13,7 +13,6 @@ import matplotlib.pyplot as plt import numpy as np from matplotlib.ticker import AutoMinorLocator -from scipy import pi import pygfunction as gt @@ -134,11 +133,11 @@ def main(): def _pipePositions(Ds, nPipes): """ Positions pipes in an axisymetric configuration. """ - dt = pi / float(nPipes) + dt = np.pi / float(nPipes) pos = [(0., 0.) for i in range(2*nPipes)] for i in range(nPipes): - pos[i] = (Ds*np.cos(2.0*i*dt+pi), Ds*np.sin(2.0*i*dt+pi)) - pos[i+nPipes] = (Ds*np.cos(2.0*i*dt+pi+dt), Ds*np.sin(2.0*i*dt+pi+dt)) + pos[i] = (Ds*np.cos(2.0*i*dt+np.pi), Ds*np.sin(2.0*i*dt+np.pi)) + pos[i+nPipes] = (Ds*np.cos(2.0*i*dt+np.pi+dt), Ds*np.sin(2.0*i*dt+np.pi+dt)) return pos diff --git a/examples/multipole_temperature.py b/examples/multipole_temperature.py index 3182948..7637538 100644 --- a/examples/multipole_temperature.py +++ b/examples/multipole_temperature.py @@ -13,7 +13,6 @@ import matplotlib.pyplot as plt import numpy as np from matplotlib.ticker import AutoMinorLocator -from scipy import pi import pygfunction as gt @@ -43,7 +42,7 @@ def main(): # Fluid properties # Fluid to outer pipe wall thermal resistance (m.K/W) - R_fp = 1.2/(2*pi*k_g)*np.ones(n_p) + R_fp = 1.2 / (2 * np.pi * k_g) * np.ones(n_p) # Borehole wall temperature (degC) T_b = 0.0 diff --git a/examples/regular_bore_field.py b/examples/regular_bore_field.py index 5213024..8012d56 100644 --- a/examples/regular_bore_field.py +++ b/examples/regular_bore_field.py @@ -29,35 +29,40 @@ def main(): # ------------------------------------------------------------------------- # Rectangular field of 4 x 3 boreholes - rectangularField = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) + rectangle_field = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) # Rectangular field triangular field of 4 x 3 borehole rows - staggeredRectangularField = gt.boreholes.staggered_rectangle_field( + staggered_rectangle_field = gt.borefield.Borefield.staggered_rectangle_field( N_1, N_2, B, B, H, D, r_b, False) # Dense field triangular field of 4 x 3 borehole rows - denseRectangularField = gt.boreholes.dense_rectangle_field( + dense_rectangle_field = gt.borefield.Borefield.dense_rectangle_field( N_1, N_2, B, H, D, r_b, False) # Box-shaped field of 4 x 3 boreholes - boxField = gt.boreholes.box_shaped_field(N_1, N_2, B, B, H, D, r_b) + box_shaped_field = gt.borefield.Borefield.box_shaped_field( + N_1, N_2, B, B, H, D, r_b) # U-shaped field of 4 x 3 boreholes - UField = gt.boreholes.U_shaped_field(N_1, N_2, B, B, H, D, r_b) + U_shaped_field = gt.borefield.Borefield.U_shaped_field( + N_1, N_2, B, B, H, D, r_b) # L-shaped field of 4 x 3 boreholes - LField = gt.boreholes.L_shaped_field(N_1, N_2, B, B, H, D, r_b) + L_shaped_field = gt.borefield.Borefield.L_shaped_field( + N_1, N_2, B, B, H, D, r_b) # Circular field of 8 boreholes - circleField = gt.boreholes.circle_field(N_b, R, H, D, r_b) + circle_field = gt.borefield.Borefield.circle_field( + N_b, R, H, D, r_b) # ------------------------------------------------------------------------- # Draw bore fields # ------------------------------------------------------------------------- - for field in [ - rectangularField, staggeredRectangularField, denseRectangularField, - boxField, UField, LField, circleField]: - gt.boreholes.visualize_field(field) + for borefield in [ + rectangle_field, staggered_rectangle_field, dense_rectangle_field, + box_shaped_field, U_shaped_field, L_shaped_field, circle_field]: + borefield.visualize_field() plt.show() return diff --git a/examples/unequal_segments.py b/examples/unequal_segments.py index cc7dbf8..1746abb 100644 --- a/examples/unequal_segments.py +++ b/examples/unequal_segments.py @@ -40,8 +40,9 @@ def main(): # Field of 6x4 (n=24) boreholes N_1 = 6 N_2 = 4 - boreField = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) - gt.boreholes.visualize_field(boreField) + borefield = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) + gt.boreholes.visualize_field(borefield) # ------------------------------------------------------------------------- # Evaluate g-functions with different segment options @@ -60,7 +61,7 @@ def main(): 'disp': True} gfunc_equal = gt.gfunction.gFunction( - boreField, alpha, time=time, options=options, method=method) + borefield, alpha, time=time, options=options, method=method) # Calculate g-function with predefined number of segments for each # borehole, the segment lengths will be uniform along each borehole, but @@ -68,7 +69,7 @@ def main(): # Boreholes 12, 14 and 18 have more segments than the others and their # heat extraction rate profiles are plotted. - nSegments = [12] * len(boreField) + nSegments = [12] * len(borefield) nSegments[12] = 24 nSegments[14] = 24 nSegments[18] = 24 @@ -78,7 +79,7 @@ def main(): 'profiles': True} gfunc_unequal = gt.gfunction.gFunction( - boreField, alpha, time=time, options=options, method=method) + borefield, alpha, time=time, options=options, method=method) # Calculate g-function with equal number of segments for each borehole, # unequal segment lengths along the length of the borehole defined by @@ -94,7 +95,7 @@ def main(): 'profiles': True} g_func_predefined = gt.gfunction.gFunction( - boreField, alpha, time=time, options=options, method=method) + borefield, alpha, time=time, options=options, method=method) # ------------------------------------------------------------------------- # Plot g-functions diff --git a/examples/uniform_heat_extraction_rate.py b/examples/uniform_heat_extraction_rate.py index 9c1d30e..8a8eda2 100644 --- a/examples/uniform_heat_extraction_rate.py +++ b/examples/uniform_heat_extraction_rate.py @@ -67,17 +67,20 @@ def main(): # Field of 3x2 (n=6) boreholes N_1 = 3 N_2 = 2 - boreField1 = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) + borefield1 = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) # Field of 6x4 (n=24) boreholes N_1 = 6 N_2 = 4 - boreField2 = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) + borefield2 = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) # Field of 10x10 (n=100) boreholes N_1 = 10 N_2 = 10 - boreField3 = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) + borefield3 = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) # ------------------------------------------------------------------------- # Load data from Cimmino and Bernier (2014) @@ -87,7 +90,7 @@ def main(): # ------------------------------------------------------------------------- # Evaluate g-functions for all fields # ------------------------------------------------------------------------- - for i, field in enumerate([boreField1, boreField2, boreField3]): + for i, field in enumerate([borefield1, borefield2, borefield3]): gfunc = gt.gfunction.gFunction( field, alpha, time=time, boundary_condition='UHTR', options=options[i], method=method) diff --git a/examples/uniform_temperature.py b/examples/uniform_temperature.py index 5cc01b9..6936dbe 100644 --- a/examples/uniform_temperature.py +++ b/examples/uniform_temperature.py @@ -54,17 +54,20 @@ def main(): # Field of 3x2 (n=6) boreholes N_1 = 3 N_2 = 2 - boreField1 = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) + borefield1 = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) # Field of 6x4 (n=24) boreholes N_1 = 6 N_2 = 4 - boreField2 = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) + borefield2 = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) # Field of 10x10 (n=100) boreholes N_1 = 10 N_2 = 10 - boreField3 = gt.boreholes.rectangle_field(N_1, N_2, B, B, H, D, r_b) + borefield3 = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B, B, H, D, r_b) # ------------------------------------------------------------------------- # Load data from Cimmino and Bernier (2014) @@ -74,7 +77,7 @@ def main(): # ------------------------------------------------------------------------- # Evaluate g-functions for all fields # ------------------------------------------------------------------------- - for i, field in enumerate([boreField1, boreField2, boreField3]): + for i, field in enumerate([borefield1, borefield2, borefield3]): nBoreholes = len(field) # Compare 'similarities' and 'equivalent' solvers t0 = perf_counter() diff --git a/pygfunction/__init__.py b/pygfunction/__init__.py index 0309fbb..61ca2ba 100644 --- a/pygfunction/__init__.py +++ b/pygfunction/__init__.py @@ -1,4 +1,5 @@ from . import boreholes +from . import borefield from . import gfunction from . import heat_transfer from . import load_aggregation diff --git a/pygfunction/borefield.py b/pygfunction/borefield.py new file mode 100644 index 0000000..8641e56 --- /dev/null +++ b/pygfunction/borefield.py @@ -0,0 +1,1100 @@ +# -*- coding: utf-8 -*- +from typing import Union, List, Dict, Tuple +from typing_extensions import Self # for compatibility with Python <= 3.10 + +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +import numpy as np +import numpy.typing as npt + +from .boreholes import Borehole +from .utilities import _initialize_figure, _format_axes, _format_axes_3d + + +class Borefield: + """ + Contains information regarding the dimensions and positions of boreholes within a borefield. + + Attributes + ---------- + H : float or (nBoreholes,) array + Borehole lengths (in meters). + D : float or (nBoreholes,) array + Borehole buried depths (in meters). + r_b : float or (nBoreholes,) array + Borehole radii (in meters). + x : float or (nBoreholes,) array + Position (in meters) of the head of the boreholes along the x-axis. + y : float or (nBoreholes,) array + Position (in meters) of the head of the boreholes along the y-axis. + tilt : float or (nBoreholes,) array, optional + Angle (in radians) from vertical of the axis of the boreholes. + Default is 0. + orientation : float or (nBoreholes,) array, optional + Direction (in radians) of the tilt of the boreholes. Defaults to zero + if the borehole is vertical. + Default is 0. + + Notes + ----- + Parameters that are equal for all boreholes can be provided as scalars. + These parameters are then broadcasted to (nBoreholes,) arrays. + + """ + def __init__( + self, H: npt.ArrayLike, D: npt.ArrayLike, r_b: npt.ArrayLike, + x: npt.ArrayLike, y: npt.ArrayLike, tilt: npt.ArrayLike = 0., + orientation: npt.ArrayLike = 0.): + # Convert x and y coordinates to arrays + x = np.atleast_1d(x) + y = np.atleast_1d(y) + self.nBoreholes = np.maximum(len(x), len(y)) + + # Broadcast all variables to arrays of length `nBoreholes` + self.H = np.broadcast_to(H, self.nBoreholes) + self.D = np.broadcast_to(D, self.nBoreholes) + self.r_b = np.broadcast_to(r_b, self.nBoreholes) + self.x = np.broadcast_to(x, self.nBoreholes) + self.y = np.broadcast_to(y, self.nBoreholes) + self.tilt = np.broadcast_to(tilt, self.nBoreholes) + + # Identify tilted boreholes + self._is_tilted = np.broadcast_to( + np.greater(np.abs(tilt), 1e-6), + self.nBoreholes) + # Vertical boreholes default to an orientation of zero + if not np.any(self._is_tilted): + self.orientation = np.broadcast_to(0., self.nBoreholes) + elif np.all(self._is_tilted): + self.orientation = np.broadcast_to(orientation, self.nBoreholes) + else: + self.orientation = np.multiply(orientation, self._is_tilted) + + def __getitem__(self, key): + if isinstance(key, (int, np.integer)): + # Returns a borehole object if only one borehole is indexed + output_class = Borehole + else: + # Returns a borefield object for slices and lists of indexes + output_class = Borefield + return output_class( + self.H[key], self.D[key], self.r_b[key], self.x[key], self.y[key], + tilt=self.tilt[key], orientation=self.orientation[key]) + + def __eq__( + self, other_field: Union[Borehole, List[Borehole], Self]) -> bool: + """Return True if other_field is the same as self.""" + # Convert other_field into Borefield object + if isinstance(other_field, (Borehole, list)): + other_field = Borefield.from_boreholes(other_field) + check = bool( + self.nBoreholes == other_field.nBoreholes + and np.allclose(self.H, other_field.H) + and np.allclose(self.D, other_field.D) + and np.allclose(self.r_b, other_field.r_b) + and np.allclose(self.x, other_field.x) + and np.allclose(self.y, other_field.y) + and np.allclose(self.tilt, other_field.tilt) + and np.allclose(self.orientation, other_field.orientation) + ) + return check + + def __len__(self) -> int: + """Return the number of boreholes.""" + return self.nBoreholes + + def __ne__( + self, other_field: Union[Borehole, List[Borehole], Self]) -> bool: + """Return True if other_field is not the same as self.""" + check = not self == other_field + return check + + def evaluate_g_function( + self, + alpha: float, + time: npt.ArrayLike, + method: str = "equivalent", + boundary_condition: str = "UBWT", + options: Union[Dict[str, str], None] = None): + """ + Evaluate the g-function of the bore field. + + Parameters + ---------- + alpha : float + Soil thermal diffusivity (in m2/s). + time : float or array + Values of time (in seconds) for which the g-function is evaluated. + method : str, optional + Method for the evaluation of the g-function. Should be one of + + - 'similarities' : + The accelerated method of Cimmino (2018) + [#borefield-Cimmin2018]_, using similarities in the bore + field to decrease the number of evaluations of the FLS + solution. + - 'detailed' : + The classical superposition of the FLS solution. The FLS + solution is evaluated for all pairs of segments in the bore + field. + - 'equivalent' : + The equivalent borehole method of Prieto and Cimmino (2021) + [#borefield-PriCim2021]_. Boreholes are assembled into + groups of boreholes that share similar borehole wall + temperatures and heat extraction rates. Each group is + represented by an equivalent borehole and the + group-to-group thermal interactions are calculated by the + FLS solution. This is an approximation of the + 'similarities' method. + + Default is 'equivalent'. + boundary_condition : str, optional + Boundary condition for the evaluation of the g-function. Should be + one of + + - 'UHTR' : + **Uniform heat transfer rate**. This corresponds to + boundary condition *BC-I* as defined by Cimmino and Bernier + (2014) [#borefield-CimBer2014]_. + - 'UBWT' : + **Uniform borehole wall temperature**. This corresponds to + boundary condition *BC-III* as defined by Cimmino and + Bernier (2014) [#borefield-CimBer2014]_. + + Default is 'UBWT'. + options : dict, optional + A dictionary of solver options. All methods accept the following + generic options: + + nSegments : int or list, optional + Number of line segments used per borehole, or list of + number of line segments used for each borehole. + Default is 8. + segment_ratios : array, list of arrays, or callable, optional + Ratio of the borehole length represented by each segment. + The sum of ratios must be equal to 1. The shape of the + array is of (nSegments,) or list of (nSegments[i],). If + segment_ratios==None, segments of equal lengths are + considered. If a callable is provided, it must return an + array of size (nSegments,) when provided with nSegments (of + type int) as an argument, or an array of size + (nSegments[i],) when provided with an element of nSegments + (of type list). + Default is :func:`utilities.segment_ratios`. + approximate_FLS : bool, optional + Set to true to use the approximation of the FLS solution of + Cimmino (2021) [#borefield-Cimmin2021]_. This approximation + does not require the numerical evaluation of any integral. + When using the 'equivalent' solver, the approximation is + only applied to the thermal response at the borehole + radius. Thermal interaction between boreholes is evaluated + using the FLS solution. + Default is False. + nFLS : int, optional + Number of terms in the approximation of the FLS solution. + This parameter is unused if `approximate_FLS` is set to + False. + Default is 10. Maximum is 25. + mQuad : int, optional + Number of Gauss-Legendre sample points for the integral + over :math:`u` in the inclined FLS solution. + Default is 11. + linear_threshold : float, optional + Threshold time (in seconds) under which the g-function is + linearized. The g-function value is then interpolated + between 0 and its value at the threshold. If + `linear_threshold==None`, the g-function is linearized for + times `t < r_b**2 / (25 * self.alpha)`. + Default is None. + disp : bool, optional + Set to true to print progression messages. + Default is False. + kind : str, optional + Interpolation method used for segment-to-segment thermal + response factors. See documentation for + scipy.interpolate.interp1d. + Default is 'linear'. + dtype : numpy dtype, optional + numpy data type used for matrices and vectors. Should be + one of numpy.single or numpy.double. + Default is numpy.double. + + The 'similarities' solver accepts the following method-specific + options: + + disTol : float, optional + Relative tolerance on radial distance. Two distances + (d1, d2) between two pairs of boreholes are considered + equal if the difference between the two distances + (abs(d1-d2)) is below tolerance. + Default is 0.01. + tol : float, optional + Relative tolerance on length and depth. Two lengths H1, H2 + (or depths D1, D2) are considered equal if + abs(H1 - H2)/H2 < tol. + Default is 1.0e-6. + + The 'equivalent' solver accepts the following method-specific + options: + + disTol : float, optional + Relative tolerance on radial distance. Two distances + (d1, d2) between two pairs of boreholes are considered + equal if the difference between the two distances + (abs(d1-d2)) is below tolerance. + Default is 0.01. + tol : float, optional + Relative tolerance on length and depth. Two lengths H1, H2 + (or depths D1, D2) are considered equal if + abs(H1 - H2)/H2 < tol. + Default is 1.0e-6. + kClusters : int, optional + Increment on the minimum number of equivalent boreholes + determined by cutting the dendrogram of the bore field + given by the hierarchical agglomerative clustering method. + Increasing the value of this parameter increases the + accuracy of the method. + Default is 1. + + Notes + ----- + - The 'equivalent' solver does not support inclined boreholes. + - The g-function is linearized for times + `t < r_b**2 / (25 * self.alpha)`. The g-function value is then + interpolated between 0 and its value at the threshold. + - This method only returns the values of the g-functions. The + :class:`gfunction.gFunction` class provides additional + capabilities for visualization boundary conditions. + + References + ---------- + .. [#borefield-CimBer2014] Cimmino, M., & Bernier, M. (2014). A + semi-analytical method to generate g-functions for geothermal bore + fields. International Journal of Heat and Mass Transfer, 70, + 641-650. + .. [#borefield-Cimmin2018] Cimmino, M. (2018). Fast calculation of the + g-functions of geothermal borehole fields using similarities in the + evaluation of the finite line source solution. Journal of Building + Performance Simulation, 11 (6), 655-668. + .. [#borefield-PriCim2021] Prieto, C., & Cimmino, M. + (2021). Thermal interactions in large irregular fields of geothermal + boreholes: the method of equivalent borehole. Journal of Building + Performance Simulation, 14 (4), 446-460. + .. [#borefield-Cimmin2021] Cimmino, M. (2021). An approximation of the + finite line source solution to model thermal interactions between + geothermal boreholes. International Communications in Heat and Mass + Transfer, 127, 105496. + + Returns + ------- + gFunction : float or array + Values of the g-function. + + """ + from .gfunction import gFunction + if options is None: + options = {} + + gfunc = gFunction( + self, + alpha, + time=time, + method=method, + boundary_condition=boundary_condition, + options=options, + ) + + return gfunc.gFunc + + def visualize_field( + self, viewTop: bool = True, view3D: bool = True, + labels: bool = True, showTilt: bool = True) -> Figure: + """ + Plot the top view and 3D view of borehole positions. + + Parameters + ---------- + viewTop : bool, optional + Set to True to plot top view. + Default is True + view3D : bool, optional + Set to True to plot 3D view. + Default is True + labels : bool, optional + Set to True to annotate borehole indices to top view plot. + Default is True + showTilt : bool, optional + Set to True to show borehole inclination on top view plot. + Default is True + + Returns + ------- + fig : figure + Figure object (matplotlib). + + """ + # Configure figure and axes + fig = _initialize_figure() + if viewTop and view3D: + ax1 = fig.add_subplot(121) + ax2 = fig.add_subplot(122, projection='3d') + elif viewTop: + ax1 = fig.add_subplot(111) + elif view3D: + ax2 = fig.add_subplot(111, projection='3d') + if viewTop: + ax1.set_xlabel(r'$x$ [m]') + ax1.set_ylabel(r'$y$ [m]') + ax1.axis('equal') + _format_axes(ax1) + if view3D: + ax2.set_xlabel(r'$x$ [m]') + ax2.set_ylabel(r'$y$ [m]') + ax2.set_zlabel(r'$z$ [m]') + _format_axes_3d(ax2) + ax2.invert_zaxis() + + # Bottom end of boreholes + x_H = self.x + self.H * np.sin(self.tilt) * np.cos(self.orientation) + y_H = self.y + self.H * np.sin(self.tilt) * np.sin(self.orientation) + z_H = self.D + self.H * np.cos(self.tilt) + + # ------------------------------------------------------------------------- + # Top view + # ------------------------------------------------------------------------- + if viewTop: + if showTilt: + ax1.plot( + np.stack((self.x, x_H), axis=0), + np.stack((self.y, y_H), axis=0), + 'k--') + ax1.plot(self.x, self.y, 'ko') + if labels: + for i, borehole in enumerate(self): + ax1.text( + borehole.x, + borehole.y, + f' {i}', + ha="left", + va="bottom") + + # ------------------------------------------------------------------------- + # 3D view + # ------------------------------------------------------------------------- + if view3D: + ax2.plot(self.x, self.y, self.D, 'ko') + for i in range(self.nBoreholes): + ax2.plot( + (self.x[i], x_H[i]), + (self.y[i], y_H[i]), + (self.D[i], z_H[i]), + 'k-') + + if viewTop and view3D: + plt.tight_layout(rect=[0, 0.0, 0.90, 1.0]) + else: + plt.tight_layout() + + return fig + + def to_boreholes(self) -> List[Borehole]: + """ + Return a list of boreholes in the bore field. + + Returns + ------- + boreholes : list of Borehole objects + List of boreholes in the bore field. + + """ + return list(self) + + def to_file(self, filename: str): + """ + Save the bore field into a text file. + + Parameters + ---------- + filename : str + The filename in which to save the bore field. + + """ + data = np.stack( + (self.x, + self.y, + self.H, + self.D, + self.r_b, + self.tilt, + self.orientation), + axis=-1) + np.savetxt( + filename, + data, + delimiter='\t', + header='x\ty\tH\tD\tr_b\ttilt\torientation') + + @classmethod + def from_boreholes( + cls, boreholes: Union[Borehole, List[Borehole]]) -> Self: + """ + Build a borefield given a list of Borehole objects. + + Parameters + ---------- + boreholes : list of Borehole objects + List of boreholes in the bore field. + + Returns + ------- + borefield : Borefield object + Borefield object. + + """ + if isinstance(boreholes, Borehole): + boreholes = [boreholes] + # Build parameter arrays from borehole objects + H = np.array([b.H for b in boreholes]) + D = np.array([b.D for b in boreholes]) + r_b = np.array([b.r_b for b in boreholes]) + tilt = np.array([b.tilt for b in boreholes]) + orientation = np.array([b.orientation for b in boreholes]) + x = np.array([b.x for b in boreholes]) + y = np.array([b.y for b in boreholes]) + # Create the borefield object + borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation) + return borefield + + @classmethod + def from_file(cls, filename: str) -> Self: + """ + Build a bore field given coordinates and dimensions provided in a text file. + + Parameters + ---------- + filename : str + Absolute path to the text file. + + Returns + ------- + borefield : Borefield object + Borefield object. + + Notes + ----- + The text file should be formatted as follows:: + + # x y H D r_b tilt orientation + 0. 0. 100. 2.5 0.075 0. 0. + 5. 0. 100. 2.5 0.075 0. 0. + 0. 5. 100. 2.5 0.075 0. 0. + 0. 10. 100. 2.5 0.075 0. 0. + 0. 20. 100. 2.5 0.075 0. 0. + + """ + # Load data from file + data = np.loadtxt(filename, ndmin=2) + # Build the bore field + x = data[:, 0] + y = data[:, 1] + H = data[:, 2] + D = data[:, 3] + r_b = data[:, 4] + if np.shape(data)[1] == 7: + tilt = data[:, 5] + orientation = data[:, 6] + else: + tilt = 0. + orientation = 0. + # Create the bore field + borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation) + return borefield + + @classmethod + def rectangle_field( + cls, N_1: int, N_2: int, B_1: float, B_2: float, H: float, + D: float, r_b: float, tilt: float = 0., + origin: Union[Tuple[float, float], None] = None) -> Self: + """ + Build a bore field in a rectangular configuration. + + Parameters + ---------- + N_1 : int + Number of boreholes in the x direction. + N_2 : int + Number of boreholes in the y direction. + B_1 : float + Distance (in meters) between adjacent boreholes in the x direction. + B_2 : float + Distance (in meters) between adjacent boreholes in the y direction. + H : float + Borehole length (in meters). + D : float + Borehole buried depth (in meters). + r_b : float + Borehole radius (in meters). + tilt : float, optional + Angle (in radians) from vertical of the axis of the borehole. The + orientation of the tilt is orthogonal to the origin coordinate. + Default is 0. + origin : tuple, optional + A coordinate indicating the origin of reference for orientation of + boreholes. + Default is the center of the rectangle. + + Returns + ------- + borefield : Borefield object + The rectangular bore field. + + Notes + ----- + Boreholes located at the origin will remain vertical. + + Examples + -------- + >>> borefield = gt.borefield.Borefield.rectangle_field( + N_1=3, N_2=2, B_1=5., B_2=5., H=100., D=2.5, r_b=0.05) + + The bore field is constructed line by line. For N_1=3 and N_2=2, the + bore field layout is as follows:: + + 3 4 5 + + 0 1 2 + + """ + if origin is None: + # When no origin is supplied, compute the origin to be at the + # center of the rectangle + x0 = (N_1 - 1) / 2 * B_1 + y0 = (N_2 - 1) / 2 * B_2 + else: + x0, y0 = origin + + # Borehole positions and orientation + x = np.tile(np.arange(N_1), N_2) * B_1 + y = np.repeat(np.arange(N_2), N_1) * B_2 + orientation = np.arctan2(y - y0, x - x0) + # Boreholes are inclined only if they do not lie on the origin + dis0 = np.sqrt((x - x0)**2 + (y - y0)**2) + if np.any(dis0 < r_b): + tilt = np.full(N_1 * N_2, tilt) + tilt[dis0 < r_b] = 0. + + # Create the bore field + borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation) + return borefield + + @classmethod + def staggered_rectangle_field( + cls, N_1: int, N_2: int, B_1: float, B_2: float, H: float, + D: float, r_b: float, include_last_borehole: bool, + tilt: float = 0., + origin: Union[Tuple[float, float], None] = None) -> Self: + """ + Build a bore field in a staggered rectangular bore field configuration. + + Parameters + ---------- + N_1 : int + Number of borehole in the x direction. + N_2 : int + Number of borehole in the y direction. + B_1 : float + Distance (in meters) between adjacent boreholes in the x direction. + B_2 : float + Distance (in meters) between adjacent boreholes in the y direction. + H : float + Borehole length (in meters). + D : float + Borehole buried depth (in meters). + r_b : float + Borehole radius (in meters). + include_last_borehole : bool + If True, then each row of boreholes has equal numbers of boreholes. + If False, then the staggered rows have one borehole less so they + are contained within the imaginary 'box' around the borefield. + tilt : float, optional + Angle (in radians) from vertical of the axis of the borehole. The + orientation of the tilt is orthogonal to the origin coordinate. + Default is 0. + origin : tuple, optional + A coordinate indicating the origin of reference for orientation of + boreholes. + Default is the center of the rectangle. + + Returns + ------- + borefield : Borefield object + The staggered rectangular bore field. + + Notes + ----- + Boreholes located at the origin will remain vertical. + + Examples + -------- + >>> borefield = gt.borefield.Borefield.staggered_rectangle_field( + N_1=3, N_2=2, B_1=5., B_2=5., H=100., D=2.5, r_b=0.05, + include_last_borehole=True) + + The bore field is constructed line by line. For N_1=3 and N_2=3, the + bore field layout is as follows, if `include_last_borehole` is True:: + + 6 7 8 + 3 4 5 + 0 1 2 + + and if `include_last_borehole` is False:: + + 5 6 7 + 3 4 + 0 1 2 + + """ + if N_1 == 1 or N_2 == 1: + borefield = cls.rectangle_field( + N_1, N_2, B_1, B_2, H, D, r_b, tilt, origin) + return borefield + + if origin is None: + # When no origin is supplied, compute the origin to be at the + # center of the rectangle + if include_last_borehole: + x0 = (N_1 - 0.5) / 2 * B_1 + y0 = (N_2 - 1) / 2 * B_2 + else: + x0 = (N_1 - 1) / 2 * B_1 + y0 = (N_2 - 1) / 2 * B_2 + else: + x0, y0 = origin + + # Borehole positions and orientation + x = np.array( + [i + (0.5 if j % 2 == 1 else 0.) + for j in range(N_2) + for i in range(N_1) + if i < (N_1 - 1) or include_last_borehole or (j % 2 == 0)]) * B_1 + y = np.array( + [j + for j in range(N_2) + for i in range(N_1) + if i < (N_1 - 1) or include_last_borehole or (j % 2 == 0)]) * B_2 + orientation = np.arctan2(y - y0, x - x0) + nBoreholes = len(x) + # Boreholes are inclined only if they do not lie on the origin + dis0 = np.sqrt((x - x0)**2 + (y - y0)**2) + if np.any(dis0 < r_b): + tilt = np.full(nBoreholes, tilt) + tilt[dis0 < r_b] = 0. + + # Create the bore field + borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation) + return borefield + + @classmethod + def dense_rectangle_field( + cls, N_1: int, N_2: int, B: float, H: float, D: float, + r_b: float, include_last_borehole: bool, tilt: float = 0., + origin: Union[Tuple[float, float], None] = None) -> Self: + """ + Build a bore field in a dense rectangular bore field configuration. + + Parameters + ---------- + N_1 : int + Number of borehole in the x direction. + N_2 : int + Number of borehole in the y direction. + B : float + Distance (in meters) between adjacent boreholes. + H : float + Borehole length (in meters). + D : float + Borehole buried depth (in meters). + r_b : float + Borehole radius (in meters). + include_last_borehole : bool + If True, then each row of boreholes has equal numbers of boreholes. + If False, then the staggered rows have one borehole less so they + are contained within the imaginary 'box' around the borefield. + tilt : float, optional + Angle (in radians) from vertical of the axis of the borehole. The + orientation of the tilt is orthogonal to the origin coordinate. + Default is 0. + origin : tuple, optional + A coordinate indicating the origin of reference for orientation of + boreholes. + Default is the center of the rectangle. + + Returns + ------- + borefield : Borefield object + The dense rectangular bore field. + + Notes + ----- + Boreholes located at the origin will remain vertical. + + Examples + -------- + >>> borefield = gt.borefield.Borefield.dense_rectangle_field( + N_1=3, N_2=2, B=5., H=100., D=2.5, r_b=0.05, + include_last_borehole=True) + + The bore field is constructed line by line. For N_1=3 and N_2=3, the + bore field layout is as follows, if `include_last_borehole` is True:: + + 6 7 8 + 3 4 5 + 0 1 2 + + and if `include_last_borehole` is False:: + + 5 6 7 + 3 4 + 0 1 2 + + """ + if N_1 > 1: + B_2 = np.sqrt(3)/2 * B + else: + B_2 = B + borefield = cls.staggered_rectangle_field( + N_1, N_2, B, B_2, H, D, r_b, include_last_borehole, + tilt=tilt, origin=origin) + return borefield + + @classmethod + def L_shaped_field( + cls, N_1: int, N_2: int, B_1: float, B_2: float, H: float, + D: float, r_b: float, tilt: float = 0., + origin: Union[Tuple[float, float], None] = None) -> Self: + """ + Build a bore field in a L-shaped configuration. + + Parameters + ---------- + N_1 : int + Number of borehole in the x direction. + N_2 : int + Number of borehole in the y direction. + B_1 : float + Distance (in meters) between adjacent boreholes in the x direction. + B_2 : float + Distance (in meters) between adjacent boreholes in the y direction. + H : float + Borehole length (in meters). + D : float + Borehole buried depth (in meters). + r_b : float + Borehole radius (in meters). + tilt : float, optional + Angle (in radians) from vertical of the axis of the borehole. The + orientation of the tilt is orthogonal to the origin coordinate. + Default is 0. + origin : tuple, optional + A coordinate indicating the origin of reference for orientation of + boreholes. + Default is origin is placed at the center of an assumed rectangle. + + Returns + ------- + borefield : Borefield object + The L-shaped bore field. + + Notes + ----- + Boreholes located at the origin will remain vertical. + + Examples + -------- + >>> borefield = gt.borefield.Borefield.L_shaped_field( + N_1=3, N_2=2, B_1=5., B_2=5., H=100., D=2.5, r_b=0.05) + + The bore field is constructed line by line. For N_1=3 and N_2=2, the + bore field layout is as follows:: + + 3 + + 0 1 2 + + """ + if origin is None: + # When no origin is supplied, compute the origin to be at the + # center of the rectangle + x0 = (N_1 - 1) / 2 * B_1 + y0 = (N_2 - 1) / 2 * B_2 + else: + x0, y0 = origin + + # Borehole positions and orientation + x = np.concatenate((np.arange(N_1), np.zeros(N_2 - 1))) * B_1 + y = np.concatenate((np.zeros(N_1), np.arange(1, N_2))) * B_2 + orientation = np.arctan2(y - y0, x - x0) + nBoreholes = len(x) + # Boreholes are inclined only if they do not lie on the origin + dis0 = np.sqrt((x - x0)**2 + (y - y0)**2) + if np.any(dis0 < r_b): + tilt = np.full(nBoreholes, tilt) + tilt[dis0 < r_b] = 0. + + # Create the bore field + borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation) + return borefield + + @classmethod + def U_shaped_field( + cls, N_1: int, N_2: int, B_1: float, B_2: float, H: float, + D: float, r_b: float, tilt: float = 0., + origin: Union[Tuple[float, float], None] = None) -> Self: + """ + Build a bore field in a U-shaped configuration. + + Parameters + ---------- + N_1 : int + Number of borehole in the x direction. + N_2 : int + Number of borehole in the y direction. + B_1 : float + Distance (in meters) between adjacent boreholes in the x direction. + B_2 : float + Distance (in meters) between adjacent boreholes in the y direction. + H : float + Borehole length (in meters). + D : float + Borehole buried depth (in meters). + r_b : float + Borehole radius (in meters). + tilt : float, optional + Angle (in radians) from vertical of the axis of the borehole. The + orientation of the tilt is orthogonal to the origin coordinate. + Default is 0. + origin : tuple, optional + A coordinate indicating the origin of reference for orientation of + boreholes. + Default is the center considering an outer rectangle. + + Returns + ------- + borefield : Borefield object + The U-shaped bore field. + + Notes + ----- + Boreholes located at the origin will remain vertical. + + Examples + -------- + >>> boreField = gt.borefield.Borefield.U_shaped_field( + N_1=3, N_2=2, B_1=5., B_2=5., H=100., D=2.5, r_b=0.05) + + The bore field is constructed line by line. For N_1=3 and N_2=2, the + bore field layout is as follows:: + + 3 4 + + 0 1 2 + + """ + if origin is None: + # When no origin is supplied, compute the origin to be at the + # center of the rectangle + x0 = (N_1 - 1) / 2 * B_1 + y0 = (N_2 - 1) / 2 * B_2 + else: + x0, y0 = origin + + # Borehole positions and orientation + n_vertical = np.minimum(N_1, 2) + x = np.concatenate( + (np.arange(N_1), + np.tile(np.arange(n_vertical), N_2 - 1) * (N_1 - 1) + )) * B_1 + y = np.concatenate( + (np.zeros(N_1), + np.repeat(np.arange(1, N_2), n_vertical) + )) * B_2 + orientation = np.arctan2(y - y0, x - x0) + nBoreholes = len(x) + # Boreholes are inclined only if they do not lie on the origin + dis0 = np.sqrt((x - x0)**2 + (y - y0)**2) + if np.any(dis0 < r_b): + tilt = np.full(nBoreholes, tilt) + tilt[dis0 < r_b] = 0. + + # Create the bore field + borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation) + return borefield + + @classmethod + def box_shaped_field( + cls, N_1: int, N_2: int, B_1: float, B_2: float, H: float, + D: float, r_b: float, tilt: float = 0., + origin: Union[Tuple[float, float], None] = None) -> Self: + """ + Build a bore field in a box-shaped configuration. + + Parameters + ---------- + N_1 : int + Number of borehole in the x direction. + N_2 : int + Number of borehole in the y direction. + B_1 : float + Distance (in meters) between adjacent boreholes in the x direction. + B_2 : float + Distance (in meters) between adjacent boreholes in the y direction. + H : float + Borehole length (in meters). + D : float + Borehole buried depth (in meters). + r_b : float + Borehole radius (in meters). + tilt : float, optional + Angle (in radians) from vertical of the axis of the borehole. The + orientation of the tilt is orthogonal to the origin coordinate. + Default is 0. + origin : tuple, optional + A coordinate indicating the origin of reference for orientation of + boreholes. + Default is the center of the box. + + Returns + ------- + borefield : Borefield object + The box-shaped bore field. + + Notes + ----- + Boreholes located at the origin will remain vertical. + + Examples + -------- + >>> boreField = gt.borefield.Borefield.box_shaped_field( + N_1=4, N_2=3, B_1=5., B_2=5., H=100., D=2.5, r_b=0.05) + + The bore field is constructed line by line. For N_1=4 and N_2=3, the + bore field layout is as follows:: + + 6 7 8 9 + + 4 5 + + 0 1 2 3 + + """ + if origin is None: + # When no origin is supplied, compute the origin to be at the + # center of the rectangle + x0 = (N_1 - 1) / 2 * B_1 + y0 = (N_2 - 1) / 2 * B_2 + else: + x0, y0 = origin + + # Borehole positions and orientation + n_vertical = np.minimum(N_1, 2) + n_middle = np.maximum(0, N_2 - 2) + n_top = N_1 if N_2 > 1 else 0 + x = np.concatenate( + (np.arange(N_1), + np.tile(np.arange(n_vertical), n_middle) * (N_1 - 1), + np.arange(n_top) + )) * B_1 + y = np.concatenate( + (np.zeros(N_1), + np.repeat(np.arange(1, N_2 - 1), n_vertical), + np.full(n_top, N_2 - 1) + )) * B_2 + orientation = np.arctan2(y - y0, x - x0) + nBoreholes = len(x) + # Boreholes are inclined only if they do not lie on the origin + dis0 = np.sqrt((x - x0)**2 + (y - y0)**2) + if np.any(dis0 < r_b): + tilt = np.full(nBoreholes, tilt) + tilt[dis0 < r_b] = 0. + + # Create the bore field + borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation) + return borefield + + @classmethod + def circle_field( + cls, N: int, R: float, H: float, D: float, r_b: float, + tilt: float = 0., + origin: Union[Tuple[float, float], None] = None) -> Self: + """ + Build a list of boreholes in a circular field configuration. + + Parameters + ---------- + N : int + Number of boreholes in the bore field. + R : float + Distance (in meters) of the boreholes from the center of the field. + H : float + Borehole length (in meters). + D : float + Borehole buried depth (in meters). + r_b : float + Borehole radius (in meters). + tilt : float, optional + Angle (in radians) from vertical of the axis of the borehole. The + orientation of the tilt is towards the exterior of the bore field. + Default is 0. + origin : tuple + A coordinate indicating the origin of reference for orientation of + boreholes. + Default is the origin (0, 0). + + Returns + ------- + borefield : Borefield object + The circular shaped bore field. + + Notes + ----- + Boreholes located at the origin will remain vertical. + + Examples + -------- + >>> boreField = gt.borefield.Borefield.circle_field( + N=8, R = 5., H=100., D=2.5, r_b=0.05) + + The bore field is constructed counter-clockwise. For N=8, the bore + field layout is as follows:: + + 2 + 3 1 + + 4 0 + + 5 7 + 6 + + """ + if origin is None: + # When no origin is supplied, the origin is the center of the + # circle + x0 = 0. + y0 = 0. + else: + x0, y0 = origin + + # Borehole positions and orientation + x = R * np.cos(2 * np.pi * np.arange(N) / N) + y = R * np.sin(2 * np.pi * np.arange(N) / N) + orientation = np.arctan2(y - y0, x - x0) + nBoreholes = len(x) + # Boreholes are inclined only if they do not lie on the origin + dis0 = np.sqrt((x - x0)**2 + (y - y0)**2) + if np.any(dis0 < r_b): + tilt = np.full(nBoreholes, tilt) + tilt[dis0 < r_b] = 0. + + # Create the bore field + borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation) + return borefield diff --git a/pygfunction/boreholes.py b/pygfunction/boreholes.py index 25655eb..0cc8e70 100644 --- a/pygfunction/boreholes.py +++ b/pygfunction/boreholes.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +import warnings + import matplotlib.pyplot as plt import numpy as np -from scipy.constants import pi from scipy.spatial.distance import pdist from .utilities import _initialize_figure, _format_axes, _format_axes_3d @@ -693,6 +694,11 @@ def rectangle_field(N_1, N_2, B_1, B_2, H, D, r_b, tilt=0., origin=None): 0 1 2 """ + # This function is deprecated as of v2.3. It will be removed in v3.0. + warnings.warn("`pygfunction.boreholes.rectangle_field` is " + "deprecated as of v2.3. It will be removed in v3.0. " + "Use the `pygfunction.borefield.Borefield` class instead.", + DeprecationWarning) borefield = [] if origin is None: @@ -782,6 +788,11 @@ def staggered_rectangle_field( 0 1 2 """ + # This function is deprecated as of v2.3. It will be removed in v3.0. + warnings.warn("`pygfunction.boreholes.staggered_rectangle_field` is " + "deprecated as of v2.3. It will be removed in v3.0. " + "Use the `pygfunction.borefield.Borefield` class instead.", + DeprecationWarning) borefield = [] if N_1 == 1 or N_2 == 1: @@ -880,6 +891,11 @@ def dense_rectangle_field( 0 1 2 """ + # This function is deprecated as of v2.3. It will be removed in v3.0. + warnings.warn("`pygfunction.boreholes.dense_rectangle_field` is " + "deprecated as of v2.3. It will be removed in v3.0. " + "Use the `pygfunction.borefield.Borefield` class instead.", + DeprecationWarning) if N_1 == 1: # line field return rectangle_field(N_1, N_2, B, B, H, D, r_b, tilt, origin) @@ -939,6 +955,11 @@ def L_shaped_field(N_1, N_2, B_1, B_2, H, D, r_b, tilt=0., origin=None): 0 1 2 """ + # This function is deprecated as of v2.3. It will be removed in v3.0. + warnings.warn("`pygfunction.boreholes.L_shaped_field` is " + "deprecated as of v2.3. It will be removed in v3.0. " + "Use the `pygfunction.borefield.Borefield` class instead.", + DeprecationWarning) borefield = [] if origin is None: @@ -1026,6 +1047,11 @@ def U_shaped_field(N_1, N_2, B_1, B_2, H, D, r_b, tilt=0., origin=None): 0 1 2 """ + # This function is deprecated as of v2.3. It will be removed in v3.0. + warnings.warn("`pygfunction.boreholes.U_shaped_field` is " + "deprecated as of v2.3. It will be removed in v3.0. " + "Use the `pygfunction.borefield.Borefield` class instead.", + DeprecationWarning) borefield = [] if origin is None: @@ -1129,6 +1155,11 @@ def box_shaped_field(N_1, N_2, B_1, B_2, H, D, r_b, tilt=0, origin=None): 0 1 2 3 """ + # This function is deprecated as of v2.3. It will be removed in v3.0. + warnings.warn("`pygfunction.boreholes.box_shaped_field` is " + "deprecated as of v2.3. It will be removed in v3.0. " + "Use the `pygfunction.borefield.Borefield` class instead.", + DeprecationWarning) borefield = [] if origin is None: @@ -1240,6 +1271,11 @@ def circle_field(N, R, H, D, r_b, tilt=0., origin=None): 6 """ + # This function is deprecated as of v2.3. It will be removed in v3.0. + warnings.warn("`pygfunction.boreholes.circle_field` is " + "deprecated as of v2.3. It will be removed in v3.0. " + "Use the `pygfunction.borefield.Borefield` class instead.", + DeprecationWarning) borefield = [] if origin is None: @@ -1251,8 +1287,8 @@ def circle_field(N, R, H, D, r_b, tilt=0., origin=None): x0, y0 = origin for i in range(N): - x = R * np.cos(2 * pi * i / N) - y = R * np.sin(2 * pi * i / N) + x = R * np.cos(2 * np.pi * i / N) + y = R * np.sin(2 * np.pi * i / N) orientation = np.arctan2(y - y0, x - x0) # The borehole is inclined only if it does not lie on the origin if np.sqrt((x - x0)**2 + (y - y0)**2) > r_b: @@ -1345,7 +1381,11 @@ def visualize_field( Figure object (matplotlib). """ - from mpl_toolkits.mplot3d import Axes3D + # This function is deprecated as of v2.3. It will be removed in v3.0. + warnings.warn("`pygfunction.boreholes.visualize_field` is " + "deprecated as of v2.3. It will be removed in v3.0. " + "Use the `pygfunction.borefield.Borefield` class instead.", + DeprecationWarning) # Configure figure and axes fig = _initialize_figure() diff --git a/pygfunction/gfunction.py b/pygfunction/gfunction.py index e387795..20cc29b 100644 --- a/pygfunction/gfunction.py +++ b/pygfunction/gfunction.py @@ -9,6 +9,7 @@ from scipy.interpolate import interp1d as interp1d from .boreholes import Borehole, _EquivalentBorehole, find_duplicates +from .borefield import Borefield from .heat_transfer import finite_line_source, finite_line_source_vectorized, \ finite_line_source_equivalent_boreholes_vectorized, \ finite_line_source_inclined_vectorized @@ -65,11 +66,11 @@ class gFunction(object): of - 'UHTR' : - **Uniform heat transfer rate**. This is corresponds to boundary + **Uniform heat transfer rate**. This corresponds to boundary condition *BC-I* as defined by Cimmino and Bernier (2014) [#gFunction-CimBer2014]_. - 'UBWT' : - **Uniform borehole wall temperature**. This is corresponds to + **Uniform borehole wall temperature**. This corresponds to boundary condition *BC-III* as defined by Cimmino and Bernier (2014) [#gFunction-CimBer2014]_. - 'MIFT' : @@ -1284,7 +1285,7 @@ def _check_inputs(self): are what is expected. """ - assert isinstance(self.boreholes, list), \ + assert isinstance(self.boreholes, (list, Borefield)), \ "Boreholes must be provided in a list." assert len(self.boreholes) > 0, \ "The list of boreholes is empty." @@ -2320,7 +2321,7 @@ def _check_inputs(self): are what is expected. """ - assert isinstance(self.boreholes, list), \ + assert isinstance(self.boreholes, (list, Borefield)), \ "Boreholes must be provided in a list." assert len(self.boreholes) > 0, \ "The list of boreholes is empty." diff --git a/requirements.txt b/requirements.txt index c889b5d..ccceff3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ numpy scipy matplotlib SecondaryCoolantProps +typing_extensions diff --git a/setup.cfg b/setup.cfg index a68ac29..8cffabf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ install_requires = numpy >= 1.21.5 scipy >= 1.7.3 secondarycoolantprops >= 1.1 + typing_extensions >= 4.0.1 python_requires = >=3.8 [options.extras_require] diff --git a/tests/borefield_test.py b/tests/borefield_test.py new file mode 100644 index 0000000..d95e5c8 --- /dev/null +++ b/tests/borefield_test.py @@ -0,0 +1,510 @@ +import numpy as np +import pytest + +import pygfunction as gt + + +# ============================================================================= +# Test Borefield +# ============================================================================= +# Compare __init__ and Borefield.from_boreholes for the initialization of +# Borefield objects +@pytest.mark.parametrize("field", [ + ('single_borehole'), + ('single_borehole_short'), + ('ten_boreholes_rectangular'), + ('two_boreholes_inclined'), + ]) +def test_borefield_init(field, request): + # Extract the bore field from the fixture + boreholes = request.getfixturevalue(field).to_boreholes() + # Borefield.from_boreholes + borefield_from_boreholes = gt.borefield.Borefield.from_boreholes(boreholes) + # Borefield.__init__ + H = np.array([b.H for b in boreholes]) + D = np.array([b.D for b in boreholes]) + r_b = np.array([b.r_b for b in boreholes]) + x = np.array([b.x for b in boreholes]) + y = np.array([b.y for b in boreholes]) + tilt = np.array([b.tilt for b in boreholes]) + orientation = np.array([b.orientation for b in boreholes]) + borefield = gt.borefield.Borefield( + H, D, r_b, x, y, tilt=tilt, orientation=orientation) + assert borefield == borefield_from_boreholes + +# Test borefield comparison using __eq__ +@pytest.mark.parametrize("field, other_field, expected", [ + # Fields that are equal + ('single_borehole', 'single_borehole', True), + ('single_borehole_short', 'single_borehole_short', True), + ('ten_boreholes_rectangular', 'ten_boreholes_rectangular', True), + ('two_boreholes_inclined', 'two_boreholes_inclined', True), + # Fields that are not equal + ('single_borehole', 'single_borehole_short', False), + ('single_borehole', 'ten_boreholes_rectangular', False), + ('single_borehole', 'two_boreholes_inclined', False), + ('single_borehole_short', 'ten_boreholes_rectangular', False), + ('single_borehole_short', 'two_boreholes_inclined', False), + ('ten_boreholes_rectangular', 'two_boreholes_inclined', False), + ]) +def test_borefield_eq(field, other_field, expected, request): + # Extract the bore field from the fixture + borefield = request.getfixturevalue(field) + other_field = request.getfixturevalue(other_field) + assert (borefield == other_field) == expected + +# Test borefield comparison using __ne__ +@pytest.mark.parametrize("field, other_field, expected", [ + # Fields that are equal + ('single_borehole', 'single_borehole', False), + ('single_borehole_short', 'single_borehole_short', False), + ('ten_boreholes_rectangular', 'ten_boreholes_rectangular', False), + ('two_boreholes_inclined', 'two_boreholes_inclined', False), + # Fields that are not equal + ('single_borehole', 'single_borehole_short', True), + ('single_borehole', 'ten_boreholes_rectangular', True), + ('single_borehole', 'two_boreholes_inclined', True), + ('single_borehole_short', 'ten_boreholes_rectangular', True), + ('single_borehole_short', 'two_boreholes_inclined', True), + ('ten_boreholes_rectangular', 'two_boreholes_inclined', True), + ]) +def test_borefield_ne(field, other_field, expected, request): + # Extract the bore field from the fixture + borefield = request.getfixturevalue(field) + other_field = request.getfixturevalue(other_field) + assert (borefield != other_field) == expected + + +# ============================================================================= +# Test evaluate_g_function (vertical boreholes) +# ============================================================================= +# Test 'UBWT' g-functions for different bore fields using all solvers, +# unequal/uniform segments, and with/without the FLS approximation +@pytest.mark.parametrize("field, method, opts, expected", [ + # 'equivalent' solver - unequal segments + ('single_borehole', 'equivalent', 'unequal_segments', np.array([5.59717446, 6.36257605, 6.60517223])), + ('single_borehole_short', 'equivalent', 'unequal_segments', np.array([4.15784411, 4.98477603, 5.27975732])), + ('ten_boreholes_rectangular', 'equivalent', 'unequal_segments', np.array([10.89935004, 17.09864925, 19.0795435])), + # 'equivalent' solver - uniform segments + ('single_borehole', 'equivalent', 'uniform_segments', np.array([5.6057331, 6.37369288, 6.61659795])), + ('single_borehole_short', 'equivalent', 'uniform_segments', np.array([4.16941861, 4.99989722, 5.29557193])), + ('ten_boreholes_rectangular', 'equivalent', 'uniform_segments', np.array([10.96118694, 17.24496533, 19.2536638])), + # 'equivalent' solver - unequal segments, FLS approximation + ('single_borehole', 'equivalent', 'unequal_segments_approx', np.array([5.59717101, 6.36259907, 6.6050007])), + ('single_borehole_short', 'equivalent', 'unequal_segments_approx', np.array([4.15784584, 4.98478735, 5.27961509])), + ('ten_boreholes_rectangular', 'equivalent', 'unequal_segments_approx', np.array([10.8993464, 17.09872924, 19.0794071])), + # 'equivalent' solver - uniform segments, FLS approximation + ('single_borehole', 'equivalent', 'uniform_segments_approx', np.array([5.60572735, 6.37371464, 6.61642409])), + ('single_borehole_short', 'equivalent', 'uniform_segments_approx', np.array([4.16941691, 4.99990922, 5.29542863])), + ('ten_boreholes_rectangular', 'equivalent', 'uniform_segments_approx', np.array([10.96117468, 17.2450427, 19.25351959])), + # 'similarities' solver - unequal segments + ('single_borehole', 'similarities', 'unequal_segments', np.array([5.59717446, 6.36257605, 6.60517223])), + ('single_borehole_short', 'similarities', 'unequal_segments', np.array([4.15784411, 4.98477603, 5.27975732])), + ('ten_boreholes_rectangular', 'similarities', 'unequal_segments', np.array([10.89935004, 17.09864925, 19.0795435])), + # 'similarities' solver - uniform segments + ('single_borehole', 'similarities', 'uniform_segments', np.array([5.6057331, 6.37369288, 6.61659795])), + ('single_borehole_short', 'similarities', 'uniform_segments', np.array([4.16941861, 4.99989722, 5.29557193])), + ('ten_boreholes_rectangular', 'similarities', 'uniform_segments', np.array([10.96118694, 17.24496533, 19.2536638])), + # 'similarities' solver - unequal segments, FLS approximation + ('single_borehole', 'similarities', 'unequal_segments_approx', np.array([5.59717101, 6.36259907, 6.6050007])), + ('single_borehole_short', 'similarities', 'unequal_segments_approx', np.array([4.15784584, 4.98478735, 5.27961509])), + ('ten_boreholes_rectangular', 'similarities', 'unequal_segments_approx', np.array([10.89852244, 17.09793569, 19.07814962])), + # 'similarities' solver - uniform segments, FLS approximation + ('single_borehole', 'similarities', 'uniform_segments_approx', np.array([5.60572735, 6.37371464, 6.61642409])), + ('single_borehole_short', 'similarities', 'uniform_segments_approx', np.array([4.16941691, 4.99990922, 5.29542863])), + ('ten_boreholes_rectangular', 'similarities', 'uniform_segments_approx', np.array([10.96035847, 17.24419784, 19.25220421])), + # 'detailed' solver - unequal segments + ('single_borehole', 'detailed', 'unequal_segments', np.array([5.59717446, 6.36257605, 6.60517223])), + ('single_borehole_short', 'detailed', 'unequal_segments', np.array([4.15784411, 4.98477603, 5.27975732])), + ('ten_boreholes_rectangular', 'detailed', 'unequal_segments', np.array([10.89935004, 17.09864925, 19.0795435])), + # 'detailed' solver - uniform segments + ('single_borehole', 'detailed', 'uniform_segments', np.array([5.6057331, 6.37369288, 6.61659795])), + ('single_borehole_short', 'detailed', 'uniform_segments', np.array([4.16941861, 4.99989722, 5.29557193])), + ('ten_boreholes_rectangular', 'detailed', 'uniform_segments', np.array([10.96118694, 17.24496533, 19.2536638])), + # 'detailed' solver - unequal segments, FLS approximation + ('single_borehole', 'detailed', 'unequal_segments_approx', np.array([5.59717101, 6.36259907, 6.6050007])), + ('single_borehole_short', 'detailed', 'unequal_segments_approx', np.array([4.15784584, 4.98478735, 5.27961509])), + ('ten_boreholes_rectangular', 'detailed', 'unequal_segments_approx', np.array([10.89852244, 17.09793569, 19.07814962])), + # 'detailed' solver - uniform segments, FLS approximation + ('single_borehole', 'detailed', 'uniform_segments_approx', np.array([5.60572735, 6.37371464, 6.61642409])), + ('single_borehole_short', 'detailed', 'uniform_segments_approx', np.array([4.16941691, 4.99990922, 5.29542863])), + ('ten_boreholes_rectangular', 'detailed', 'uniform_segments_approx', np.array([10.96035847, 17.24419784, 19.25220421])), + ]) +def test_gfunctions_UBWT(field, method, opts, expected, request): + # Extract the bore field from the fixture + borefield = request.getfixturevalue(field) + # Extract the g-function options from the fixture + options = request.getfixturevalue(opts) + # Mean borehole length [m] + H_mean = np.mean(borefield.H) + alpha = 1e-6 # Ground thermal diffusivity [m2/s] + # Bore field characteristic time [s] + ts = H_mean**2 / (9 * alpha) + # Times for the g-function [s] + time = np.array([0.1, 1., 10.]) * ts + # g-Function + gFunc = borefield.evaluate_g_function( + alpha, time, method=method, options=options, boundary_condition='UBWT') + assert np.allclose(gFunc, expected) + + +# Test 'UHTR' g-functions for different bore fields using all solvers, +# unequal/uniform segments, and with/without the FLS approximation +@pytest.mark.parametrize("field, method, opts, expected", [ + # 'equivalent' solver - unequal segments + ('single_borehole', 'equivalent', 'unequal_segments', np.array([5.61855789, 6.41336758, 6.66933682])), + ('single_borehole_short', 'equivalent', 'unequal_segments', np.array([4.18276733, 5.03671562, 5.34369772])), + ('ten_boreholes_rectangular', 'equivalent', 'unequal_segments', np.array([11.27831804, 18.48075762, 21.00669237])), + # 'equivalent' solver - uniform segments + ('single_borehole', 'equivalent', 'uniform_segments', np.array([5.61855789, 6.41336758, 6.66933682])), + ('single_borehole_short', 'equivalent', 'uniform_segments', np.array([4.18276733, 5.03671562, 5.34369772])), + ('ten_boreholes_rectangular', 'equivalent', 'uniform_segments', np.array([11.27831804, 18.48075762, 21.00669237])), + # 'equivalent' solver - unequal segments, FLS approximation + ('single_borehole', 'equivalent', 'unequal_segments_approx', np.array([5.61855411, 6.41337915, 6.66915329])), + ('single_borehole_short', 'equivalent', 'unequal_segments_approx', np.array([4.18276637, 5.03673008, 5.34353657])), + ('ten_boreholes_rectangular', 'equivalent', 'unequal_segments_approx', np.array([11.27831426, 18.48076919, 21.00650885])), + # 'equivalent' solver - uniform segments, FLS approximation + ('single_borehole', 'equivalent', 'uniform_segments_approx', np.array([5.61855411, 6.41337915, 6.66915329])), + ('single_borehole_short', 'equivalent', 'uniform_segments_approx', np.array([4.18276637, 5.03673008, 5.34353657])), + ('ten_boreholes_rectangular', 'equivalent', 'uniform_segments_approx', np.array([11.27831426, 18.48076919, 21.00650885])), + # 'similarities' solver - unequal segments + ('single_borehole', 'similarities', 'unequal_segments', np.array([5.61855789, 6.41336758, 6.66933682])), + ('single_borehole_short', 'similarities', 'unequal_segments', np.array([4.18276733, 5.03671562, 5.34369772])), + ('ten_boreholes_rectangular', 'similarities', 'unequal_segments', np.array([11.27831804, 18.48075762, 21.00669237])), + # 'similarities' solver - uniform segments + ('single_borehole', 'similarities', 'uniform_segments', np.array([5.61855789, 6.41336758, 6.66933682])), + ('single_borehole_short', 'similarities', 'uniform_segments', np.array([4.18276733, 5.03671562, 5.34369772])), + ('ten_boreholes_rectangular', 'similarities', 'uniform_segments', np.array([11.27831804, 18.48075762, 21.00669237])), + # 'similarities' solver - unequal segments, FLS approximation + ('single_borehole', 'similarities', 'unequal_segments_approx', np.array([5.61855411, 6.41337915, 6.66915329])), + ('single_borehole_short', 'similarities', 'unequal_segments_approx', np.array([4.18276637, 5.03673008, 5.34353657])), + ('ten_boreholes_rectangular', 'similarities', 'unequal_segments_approx', np.array([11.27751418, 18.47964006, 21.00475366])), + # 'similarities' solver - uniform segments, FLS approximation + ('single_borehole', 'similarities', 'uniform_segments_approx', np.array([5.61855411, 6.41337915, 6.66915329])), + ('single_borehole_short', 'similarities', 'uniform_segments_approx', np.array([4.18276637, 5.03673008, 5.34353657])), + ('ten_boreholes_rectangular', 'similarities', 'uniform_segments_approx', np.array([11.27751418, 18.47964006, 21.00475366])), + # 'detailed' solver - unequal segments + ('single_borehole', 'detailed', 'unequal_segments', np.array([5.61855789, 6.41336758, 6.66933682])), + ('single_borehole_short', 'detailed', 'unequal_segments', np.array([4.18276733, 5.03671562, 5.34369772])), + ('ten_boreholes_rectangular', 'detailed', 'unequal_segments', np.array([11.27831804, 18.48075762, 21.00669237])), + # 'detailed' solver - uniform segments + ('single_borehole', 'detailed', 'uniform_segments', np.array([5.61855789, 6.41336758, 6.66933682])), + ('single_borehole_short', 'detailed', 'uniform_segments', np.array([4.18276733, 5.03671562, 5.34369772])), + ('ten_boreholes_rectangular', 'detailed', 'uniform_segments', np.array([11.27831804, 18.48075762, 21.00669237])), + # 'detailed' solver - unequal segments, FLS approximation + ('single_borehole', 'detailed', 'unequal_segments_approx', np.array([5.61855411, 6.41337915, 6.66915329])), + ('single_borehole_short', 'detailed', 'unequal_segments_approx', np.array([4.18276637, 5.03673008, 5.34353657])), + ('ten_boreholes_rectangular', 'detailed', 'unequal_segments_approx', np.array([11.27751418, 18.47964006, 21.00475366])), + # 'detailed' solver - uniform segments, FLS approximation + ('single_borehole', 'detailed', 'uniform_segments_approx', np.array([5.61855411, 6.41337915, 6.66915329])), + ('single_borehole_short', 'detailed', 'uniform_segments_approx', np.array([4.18276637, 5.03673008, 5.34353657])), + ('ten_boreholes_rectangular', 'detailed', 'uniform_segments_approx', np.array([11.27751418, 18.47964006, 21.00475366])), + ]) +def test_gfunctions_UHTR(field, method, opts, expected, request): + # Extract the bore field from the fixture + borefield = request.getfixturevalue(field) + # Extract the g-function options from the fixture + options = request.getfixturevalue(opts) + # Mean borehole length [m] + H_mean = np.mean(borefield.H) + alpha = 1e-6 # Ground thermal diffusivity [m2/s] + # Bore field characteristic time [s] + ts = H_mean**2 / (9 * alpha) + # Times for the g-function [s] + time = np.array([0.1, 1., 10.]) * ts + # g-Function + gFunc = borefield.evaluate_g_function( + alpha, time, method=method, options=options, boundary_condition='UHTR') + assert np.allclose(gFunc, expected) + + +# ============================================================================= +# Test evaluate_g_function (inclined boreholes) +# ============================================================================= +# Test 'UBWT' g-functions for a field of inclined boreholes using all solvers, +# unequal/uniform segments, and with/without the FLS approximation +@pytest.mark.parametrize("method, opts, expected", [ + # 'similarities' solver - unequal segments + ('similarities', 'unequal_segments', np.array([5.67249989, 6.72866814, 7.15134705])), + # 'similarities' solver - uniform segments + ('similarities', 'uniform_segments', np.array([5.68324619, 6.74356205, 7.16738741])), + # 'similarities' solver - unequal segments, FLS approximation + ('similarities', 'unequal_segments_approx', np.array([5.66984803, 6.72564218, 7.14826009])), + # 'similarities' solver - uniform segments, FLS approximation + ('similarities', 'uniform_segments_approx', np.array([5.67916493, 6.7395222 , 7.16339216])), + # 'detailed' solver - unequal segments + ('detailed', 'unequal_segments', np.array([5.67249989, 6.72866814, 7.15134705])), + # 'detailed' solver - uniform segments + ('detailed', 'uniform_segments', np.array([5.68324619, 6.74356205, 7.16738741])), + # 'detailed' solver - unequal segments, FLS approximation + ('detailed', 'unequal_segments_approx', np.array([5.66984803, 6.72564218, 7.14826009])), + # 'detailed' solver - uniform segments, FLS approximation + ('detailed', 'uniform_segments_approx', np.array([5.67916493, 6.7395222 , 7.16339216])), + ]) +def test_gfunctions_inclined_UBWT(two_boreholes_inclined, method, opts, expected, request): + # Extract the bore field from the fixture + borefield = two_boreholes_inclined + # Extract the g-function options from the fixture + options = request.getfixturevalue(opts) + # Mean borehole length [m] + H_mean = np.mean(borefield.H) + alpha = 1e-6 # Ground thermal diffusivity [m2/s] + # Bore field characteristic time [s] + ts = H_mean**2 / (9 * alpha) + # Times for the g-function [s] + time = np.array([0.1, 1., 10.]) * ts + # g-Function + gFunc = borefield.evaluate_g_function( + alpha, time, method=method, options=options, boundary_condition='UBWT') + assert np.allclose(gFunc, expected) + + +# ============================================================================= +# Test borefield creation methods +# ============================================================================= +# Test rectangle_field +@pytest.mark.parametrize("N_1, N_2, B_1, B_2", [ + (1, 1, 5., 5.), # 1 by 1 + (2, 1, 5., 5.), # 2 by 1 + (1, 2, 5., 5.), # 1 by 2 + (2, 2, 5., 7.5), # 2 by 2 (different x/y spacings) + (10, 9, 7.5, 5.), # 10 by 9 (different x/y spacings) + ]) +def test_rectangle_field(N_1, N_2, B_1, B_2): + H = 150. # Borehole length [m] + D = 4. # Borehole buried depth [m] + r_b = 0.075 # Borehole radius [m] + # Generate the bore field + borefield = gt.borefield.Borefield.rectangle_field( + N_1, N_2, B_1, B_2, H, D, r_b) + # Evaluate the borehole to borehole distances + x = borefield.x + y = borefield.y + dis = np.sqrt( + np.subtract.outer(x, x)**2 + np.subtract.outer(y, y)**2)[ + ~np.eye(len(borefield), dtype=bool)] + assert np.all( + [len(borefield) == N_1 * N_2, + np.allclose(H, borefield.H), + np.allclose(D, borefield.D), + np.allclose(r_b, borefield.r_b), + len(borefield) == 1 or np.isclose(np.min(dis), min(B_1, B_2)), + ]) + + +# Test staggered_rectangle_field +@pytest.mark.parametrize("N_1, N_2, B_1, B_2, include_last_element", [ + (1, 1, 5., 5., True), # 1 by 1 + (2, 1, 5., 5., True), # 2 by 1 + (1, 2, 5., 5., True), # 1 by 2 + (2, 2, 5., 7.5, True), # 2 by 2 (different x/y spacings) + (10, 9, 7.5, 5., True), # 10 by 9 (different x/y spacings), + (1, 1, 5., 5., False), # 1 by 1 + (2, 1, 5., 5., False), # 2 by 1 + (1, 2, 5., 5., False), # 1 by 2 + (2, 2, 5., 7.5, False), # 2 by 2 (different x/y spacings) + (10, 9, 7.5, 5., False), # 10 by 9 (different x/y spacings) +]) +def test_staggered_rectangular_field(N_1, N_2, B_1, B_2, include_last_element): + H = 150. # Borehole length [m] + D = 4. # Borehole buried depth [m] + r_b = 0.075 # Borehole radius [m] + # Generate the bore field + borefield = gt.borefield.Borefield.staggered_rectangle_field( + N_1, N_2, B_1, B_2, H, D, r_b, + include_last_borehole=include_last_element) + # Evaluate the borehole to borehole distances + x = borefield.x + y = borefield.y + dis = np.sqrt( + np.subtract.outer(x, x)**2 + np.subtract.outer(y, y)**2)[ + ~np.eye(len(borefield), dtype=bool)] + + if include_last_element or N_1 == 1 or N_2 == 1: + expected_nBoreholes = N_1 * N_2 + elif N_2 % 2 == 0: + expected_nBoreholes = N_2 * (2 * N_1 - 1) / 2 + else: + expected_nBoreholes = (N_2 - 1) * (2 * N_1 - 1) / 2 + N_1 + + assert np.all( + [len(borefield) == expected_nBoreholes, + np.allclose(H, borefield.H), + np.allclose(D, borefield.D), + np.allclose(r_b, borefield.r_b), + len(borefield) == 1 or np.isclose( + np.min(dis), min(B_1, np.sqrt(B_2**2 + 0.25 * B_1**2))), + ]) + + +# Test dense_rectangle_field +@pytest.mark.parametrize("N_1, N_2, B, include_last_element", [ + (1, 1, 5., True), # 1 by 1 + (2, 1, 5., True), # 2 by 1 + (1, 2, 5., True), # 1 by 2 + (2, 2, 5., True), # 2 by 2 + (10, 9, 7.5, True), # 10 by 9 + (10, 10, 7.5, True), # 10 by 10 + (1, 1, 5., False), # 1 by 1 + (2, 1, 5., False), # 2 by 1 + (1, 2, 5., False), # 1 by 2 + (2, 2, 5., False), # 2 by 2 + (10, 9, 7.5, False), # 10 by 9 + (10, 10, 7.5, False), # 10 by 10 +]) +def test_dense_rectangle_field(N_1, N_2, B, include_last_element): + H = 150. # Borehole length [m] + D = 4. # Borehole buried depth [m] + r_b = 0.075 # Borehole radius [m] + # Generate the bore field + borefield = gt.borefield.Borefield.dense_rectangle_field( + N_1, N_2, B, H, D, r_b, include_last_borehole=include_last_element) + # Evaluate the borehole to borehole distances + x = borefield.x + y = borefield.y + dis = np.sqrt( + np.subtract.outer(x, x)**2 + np.subtract.outer(y, y)**2)[ + ~np.eye(len(borefield), dtype=bool)] + + if include_last_element or N_1 == 1 or N_2 == 1: + expected_nBoreholes = N_1 * N_2 + elif N_2 % 2 == 0: + expected_nBoreholes = N_2 * (2 * N_1 - 1) / 2 + else: + expected_nBoreholes = (N_2 - 1) * (2 * N_1 - 1) / 2 + N_1 + + assert np.all( + [len(borefield) == expected_nBoreholes, + np.allclose(H, borefield.H), + np.allclose(D, borefield.D), + np.allclose(r_b, borefield.r_b), + len(borefield) == 1 or np.isclose(np.min(dis), B) + ]) + + +# Test L_shaped_field +@pytest.mark.parametrize("N_1, N_2, B_1, B_2", [ + (1, 1, 5., 5.), # 1 by 1 + (2, 1, 5., 5.), # 2 by 1 + (1, 2, 5., 5.), # 1 by 2 + (2, 2, 5., 7.5), # 2 by 2 (different x/y spacings) + (10, 9, 7.5, 5.), # 10 by 9 (different x/y spacings) + ]) +def test_L_shaped_field(N_1, N_2, B_1, B_2): + H = 150. # Borehole length [m] + D = 4. # Borehole buried depth [m] + r_b = 0.075 # Borehole radius [m] + # Generate the bore field + borefield = gt.borefield.Borefield.L_shaped_field( + N_1, N_2, B_1, B_2, H, D, r_b) + # Evaluate the borehole to borehole distances + x = borefield.x + y = borefield.y + dis = np.sqrt( + np.subtract.outer(x, x)**2 + np.subtract.outer(y, y)**2)[ + ~np.eye(len(borefield), dtype=bool)] + assert np.all( + [len(borefield) == N_1 + N_2 - 1, + np.allclose(H, borefield.H), + np.allclose(D, borefield.D), + np.allclose(r_b, borefield.r_b), + len(borefield) == 1 or np.isclose(np.min(dis), min(B_1, B_2)), + ]) + + +# Test U_shaped_field +@pytest.mark.parametrize("N_1, N_2, B_1, B_2", [ + (1, 1, 5., 5.), # 1 by 1 + (2, 1, 5., 5.), # 2 by 1 + (1, 2, 5., 5.), # 1 by 2 + (2, 2, 5., 7.5), # 2 by 2 (different x/y spacings) + (10, 9, 7.5, 5.), # 10 by 9 (different x/y spacings) + ]) +def test_U_shaped_field(N_1, N_2, B_1, B_2): + H = 150. # Borehole length [m] + D = 4. # Borehole buried depth [m] + r_b = 0.075 # Borehole radius [m] + # Generate the bore field + borefield = gt.borefield.Borefield.U_shaped_field( + N_1, N_2, B_1, B_2, H, D, r_b) + # Evaluate the borehole to borehole distances + x = borefield.x + y = borefield.y + dis = np.sqrt( + np.subtract.outer(x, x)**2 + np.subtract.outer(y, y)**2)[ + ~np.eye(len(borefield), dtype=bool)] + assert np.all( + [len(borefield) == N_1 + 2 * N_2 - 2 if N_1 > 1 else N_2, + np.allclose(H, borefield.H), + np.allclose(D, borefield.D), + np.allclose(r_b, borefield.r_b), + len(borefield) == 1 or np.isclose(np.min(dis), min(B_1, B_2)), + ]) + + +# Test box_shaped_field +@pytest.mark.parametrize("N_1, N_2, B_1, B_2", [ + (1, 1, 5., 5.), # 1 by 1 + (2, 1, 5., 5.), # 2 by 1 + (1, 2, 5., 5.), # 1 by 2 + (2, 2, 5., 7.5), # 2 by 2 (different x/y spacings) + (10, 9, 7.5, 5.), # 10 by 9 (different x/y spacings) + ]) +def test_box_shaped_field(N_1, N_2, B_1, B_2): + H = 150. # Borehole length [m] + D = 4. # Borehole buried depth [m] + r_b = 0.075 # Borehole radius [m] + # Generate the bore field + borefield = gt.borefield.Borefield.box_shaped_field( + N_1, N_2, B_1, B_2, H, D, r_b) + # Evaluate the borehole to borehole distances + x = borefield.x + y = borefield.y + dis = np.sqrt( + np.subtract.outer(x, x)**2 + np.subtract.outer(y, y)**2)[ + ~np.eye(len(borefield), dtype=bool)] + if N_1 == 1 and N_2 == 1: + nBoreholes_expected = 1 + elif N_1 == 1: + nBoreholes_expected = N_2 + elif N_2 == 1: + nBoreholes_expected = N_1 + else: + nBoreholes_expected = 2 * (N_1 - 1) + 2 * (N_2 - 1) + assert np.all( + [len(borefield) == nBoreholes_expected, + np.allclose(H, borefield.H), + np.allclose(D, borefield.D), + np.allclose(r_b, borefield.r_b), + len(borefield) == 1 or np.isclose(np.min(dis), min(B_1, B_2)), + ]) + + +# Test circle_field +@pytest.mark.parametrize("N, R", [ + (1, 5.), # 1 borehole + (2, 5.), # 2 boreholes + (3, 7.5), # 3 boreholes + (10, 9.), # 10 boreholes + ]) +def test_circle_field(N, R): + H = 150. # Borehole length [m] + D = 4. # Borehole buried depth [m] + r_b = 0.075 # Borehole radius [m] + # Generate the bore field + borefield = gt.borefield.Borefield.circle_field(N, R, H, D, r_b) + # Evaluate the borehole to borehole distances + x = borefield.x + y = borefield.y + dis = np.sqrt( + np.subtract.outer(x, x)**2 + np.subtract.outer(y, y)**2)[ + ~np.eye(len(borefield), dtype=bool)] + B_min = 2 * R * np.sin(np.pi / N) + assert np.all( + [len(borefield) == N, + np.allclose(H, borefield.H), + np.allclose(D, borefield.D), + np.allclose(r_b, borefield.r_b), + len(borefield) == 1 or np.isclose(np.min(dis), B_min), + len(borefield) == 1 or np.max(dis) <= (2 + 1e-6) * R, + ]) diff --git a/tests/conftest.py b/tests/conftest.py index d531e3c..6ba9eb3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ # ============================================================================= -# boreholes fixtures +# borefield fixtures # ============================================================================= @pytest.fixture def single_borehole(): @@ -14,7 +14,7 @@ def single_borehole(): r_b = 0.075 # Borehole radius [m] x = 0. # Borehole x-position [m] y = 0. # Borehole y-position [m] - return [gt.boreholes.Borehole(H, D, r_b, x, y)] + return gt.borefield.Borefield(H, D, r_b, x, y) @pytest.fixture @@ -24,7 +24,7 @@ def single_borehole_short(): r_b = 0.075 # Borehole radius [m] x = 3. # Borehole x-position [m] y = 5. # Borehole y-position [m] - return [gt.boreholes.Borehole(H, D, r_b, x, y)] + return gt.borefield.Borefield(H, D, r_b, x, y) @pytest.fixture @@ -36,8 +36,8 @@ def single_borehole_inclined(): y = 0. # Borehole y-position [m] tilt = np.pi/6 # Borehole inclination [rad] orientation = np.pi/3 # Borehole orientation [rad] - return [gt.boreholes.Borehole( - H, D, r_b, x, y, tilt=tilt, orientation=orientation)] + return gt.borefield.Borefield( + H, D, r_b, x, y, tilt=tilt, orientation=orientation) @pytest.fixture @@ -46,20 +46,26 @@ def ten_boreholes_rectangular(): D = 4. # Borehole buried depth [m] r_b = 0.075 # Borehole radius [m] B_1 = B_2 = 7.5 # Borehole spacing [m] - return gt.boreholes.rectangle_field(5, 2, B_1, B_2, H, D, r_b) + return gt.borefield.Borefield.rectangle_field(5, 2, B_1, B_2, H, D, r_b) @pytest.fixture def three_boreholes_unequal(): - return [gt.boreholes.Borehole(150., 4., 0.075, -1., -2.), - gt.boreholes.Borehole(88., 2., 0.065, 5., 3.), - gt.boreholes.Borehole(177., 5., 0.085, -1., 7.),] + boreholes = [ + gt.boreholes.Borehole(150., 4., 0.075, -1., -2.), + gt.boreholes.Borehole(88., 2., 0.065, 5., 3.), + gt.boreholes.Borehole(177., 5., 0.085, -1., 7.),] + borefield = gt.borefield.Borefield.from_boreholes(boreholes) + return borefield @pytest.fixture def two_boreholes_inclined(): - return [gt.boreholes.Borehole(150., 4., 0.075, 0., 0., tilt=np.radians(20.), orientation=np.pi/2), - gt.boreholes.Borehole(150., 4., 0.075, 15., 0., tilt=np.radians(20.), orientation=3*np.pi/2),] + boreholes = [ + gt.boreholes.Borehole(150., 4., 0.075, 0., 0., tilt=np.radians(20.), orientation=np.pi/2), + gt.boreholes.Borehole(150., 4., 0.075, 15., 0., tilt=np.radians(20.), orientation=3*np.pi/2),] + borefield = gt.borefield.Borefield.from_boreholes(boreholes) + return borefield # ============================================================================= diff --git a/tests/gfunction_test.py b/tests/gfunction_test.py index e60df9d..98255c1 100644 --- a/tests/gfunction_test.py +++ b/tests/gfunction_test.py @@ -64,11 +64,11 @@ ]) def test_gfunctions_UBWT(field, method, opts, expected, request): # Extract the bore field from the fixture - boreholes = request.getfixturevalue(field) + borefield = request.getfixturevalue(field) # Extract the g-function options from the fixture options = request.getfixturevalue(opts) # Mean borehole length [m] - H_mean = np.mean([b.H for b in boreholes]) + H_mean = np.mean(borefield.H) alpha = 1e-6 # Ground thermal diffusivity [m2/s] # Bore field characteristic time [s] ts = H_mean**2 / (9 * alpha) @@ -76,7 +76,7 @@ def test_gfunctions_UBWT(field, method, opts, expected, request): time = np.array([0.1, 1., 10.]) * ts # g-Function gFunc = gt.gfunction.gFunction( - boreholes, alpha, time=time, method=method, options=options, + borefield, alpha, time=time, method=method, options=options, boundary_condition='UBWT') assert np.allclose(gFunc.gFunc, expected) @@ -135,11 +135,11 @@ def test_gfunctions_UBWT(field, method, opts, expected, request): ]) def test_gfunctions_UHTR(field, method, opts, expected, request): # Extract the bore field from the fixture - boreholes = request.getfixturevalue(field) + borefield = request.getfixturevalue(field) # Extract the g-function options from the fixture options = request.getfixturevalue(opts) # Mean borehole length [m] - H_mean = np.mean([b.H for b in boreholes]) + H_mean = np.mean(borefield.H) alpha = 1e-6 # Ground thermal diffusivity [m2/s] # Bore field characteristic time [s] ts = H_mean**2 / (9 * alpha) @@ -147,7 +147,7 @@ def test_gfunctions_UHTR(field, method, opts, expected, request): time = np.array([0.1, 1., 10.]) * ts # g-Function gFunc = gt.gfunction.gFunction( - boreholes, alpha, time=time, method=method, options=options, + borefield, alpha, time=time, method=method, options=options, boundary_condition='UHTR') assert np.allclose(gFunc.gFunc, expected) @@ -301,11 +301,11 @@ def test_gfunctions_MIFT_variable_mass_flow_rate( ]) def test_gfunctions_UBWT(two_boreholes_inclined, method, opts, expected, request): # Extract the bore field from the fixture - boreholes = two_boreholes_inclined + borefield = two_boreholes_inclined # Extract the g-function options from the fixture options = request.getfixturevalue(opts) # Mean borehole length [m] - H_mean = np.mean([b.H for b in boreholes]) + H_mean = np.mean(borefield.H) alpha = 1e-6 # Ground thermal diffusivity [m2/s] # Bore field characteristic time [s] ts = H_mean**2 / (9 * alpha) @@ -313,7 +313,7 @@ def test_gfunctions_UBWT(two_boreholes_inclined, method, opts, expected, request time = np.array([0.1, 1., 10.]) * ts # g-Function gFunc = gt.gfunction.gFunction( - boreholes, alpha, time=time, method=method, options=options, + borefield, alpha, time=time, method=method, options=options, boundary_condition='UBWT') assert np.allclose(gFunc.gFunc, expected) @@ -328,14 +328,14 @@ def test_gfunctions_UBWT(two_boreholes_inclined, method, opts, expected, request ]) def test_gfunctions_UBWT_linearization(field, method, opts, expected, request): # Extract the bore field from the fixture - boreholes = request.getfixturevalue(field) + borefield = request.getfixturevalue(field) # Extract the g-function options from the fixture options = request.getfixturevalue(opts) alpha = 1e-6 # Ground thermal diffusivity [m2/s] # Times for the g-function [s] - time = np.array([0.1, 1., 10.]) * boreholes[0].r_b**2 / (25 * alpha) + time = np.array([0.1, 1., 10.]) * borefield[0].r_b**2 / (25 * alpha) # g-Function gFunc = gt.gfunction.gFunction( - boreholes, alpha, time=time, method=method, options=options, + borefield, alpha, time=time, method=method, options=options, boundary_condition='UBWT') assert np.allclose(gFunc.gFunc, expected) diff --git a/tests/heat_transfer_test.py b/tests/heat_transfer_test.py index bc0a263..27cd195 100644 --- a/tests/heat_transfer_test.py +++ b/tests/heat_transfer_test.py @@ -159,15 +159,15 @@ def test_finite_line_source_multiple_vertical_boreholes_single_time_step( three_boreholes_unequal, reaSource, imgSource, approximation, N, expected): # Extract boreholes from fixture - boreholes = three_boreholes_unequal + borefield = three_boreholes_unequal alpha = 1e-6 # Ground thermal diffusivity [m2/s] # Bore field characteristic time [s] - ts = np.mean([b.H for b in boreholes])**2 / (9 * alpha) + ts = np.mean(borefield.H)**2 / (9 * alpha) # Time for FLS calculation [s] time = ts # Evaluate FLS h = gt.heat_transfer.finite_line_source( - time, alpha, boreholes, boreholes, reaSource=reaSource, + time, alpha, borefield, borefield, reaSource=reaSource, imgSource=imgSource, approximation=approximation, N=N) assert np.allclose(h, expected) @@ -245,15 +245,15 @@ def test_finite_line_source_multiple_vertical_boreholes_multiple_time_steps( three_boreholes_unequal, reaSource, imgSource, approximation, N, expected): # Extract boreholes from fixture - boreholes = three_boreholes_unequal + borefield = three_boreholes_unequal alpha = 1e-6 # Ground thermal diffusivity [m2/s] # Bore field characteristic time [s] - ts = np.mean([b.H for b in boreholes])**2 / (9 * alpha) + ts = np.mean(borefield.H)**2 / (9 * alpha) # Times for FLS calculation [s] time = np.array([0.1, 1., 10.]) * ts # Evaluate FLS h = gt.heat_transfer.finite_line_source( - time, alpha, boreholes, boreholes, reaSource=reaSource, + time, alpha, borefield, borefield, reaSource=reaSource, imgSource=imgSource, approximation=approximation, N=N) assert np.allclose(h, expected) @@ -278,13 +278,13 @@ def test_finite_line_source_multiple_vertical_boreholes_multiple_time_steps( def test_finite_line_source_multiple_vertical_boreholes_steady_state( three_boreholes_unequal, reaSource, imgSource, expected): # Extract boreholes from fixture - boreholes = three_boreholes_unequal + borefield = three_boreholes_unequal alpha = 1e-6 # Ground thermal diffusivity [m2/s] # Time for FLS calculation [s] time = np.inf # Evaluate FLS h = gt.heat_transfer.finite_line_source( - time, alpha, boreholes, boreholes, reaSource=reaSource, + time, alpha, borefield, borefield, reaSource=reaSource, imgSource=imgSource) assert np.allclose(h, expected) @@ -514,14 +514,14 @@ def test_finite_line_source_multiple_inclined_to_multiple_inclined( two_boreholes_inclined, tts, reaSource, imgSource, approximation, M, N, expected): # Extract boreholes from fixture - boreholes = two_boreholes_inclined + borefield = two_boreholes_inclined alpha = 1e-6 # Ground thermal diffusivity [m2/s] # Bore field characteristic time [s] - ts = np.mean([b.H for b in boreholes])**2 / (9 * alpha) + ts = np.mean(borefield.H)**2 / (9 * alpha) # Times for FLS calculation [s] time = ts * tts # Evaluate FLS h = gt.heat_transfer.finite_line_source( - time, alpha, boreholes, boreholes, reaSource=reaSource, + time, alpha, borefield, borefield, reaSource=reaSource, imgSource=imgSource, approximation=approximation, M=M, N=N) assert np.allclose(h, expected) diff --git a/tests/media_test.py b/tests/media_test.py index e4d05e7..2cb1edd 100644 --- a/tests/media_test.py +++ b/tests/media_test.py @@ -55,7 +55,6 @@ def test_media_density(fluid_str, percent, temperature, expected): f = gt.media.Fluid(fluid_str, percent, temperature).fluid val = f.density(temperature) - print(val) assert np.isclose(val, expected, rtol=1e-03) @@ -107,7 +106,6 @@ def test_media_density(fluid_str, percent, temperature, expected): def test_media_dynamic_viscosity(fluid_str, percent, temperature, expected): f = gt.media.Fluid(fluid_str, percent, temperature).fluid val = f.viscosity(temperature) - print(val) assert np.isclose(val, expected, rtol=1e-03) # ============================================================================= @@ -158,7 +156,6 @@ def test_media_dynamic_viscosity(fluid_str, percent, temperature, expected): def test_media_specific_heat(fluid_str, percent, temperature, expected): f = gt.media.Fluid(fluid_str, percent, temperature).fluid val = f.specific_heat(temperature) - print(val) assert np.isclose(val, expected, rtol=1e-03) # ============================================================================= @@ -209,5 +206,4 @@ def test_media_specific_heat(fluid_str, percent, temperature, expected): def test_media_conductivity(fluid_str, percent, temperature, expected): f = gt.media.Fluid(fluid_str, percent, temperature).fluid val = f.conductivity(temperature) - print(val) assert np.isclose(val, expected, rtol=5e-03)