diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b87ad5..a88de7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### New features * [Pull Request 325](https://github.com/MassimoCimmino/pygfunction/pull/325) - Borefields and boreholes can now be concatenated using the `+` operator, e.g. using `new_field = field_1 + field_2`. +* [Pull Request 326](https://github.com/MassimoCimmino/pygfunction/pull/326) - Introduced `gFunction.from_static_params` and `Network.from_static_params` methods. These methods facilitate the creation of `Network` objects and the evaluation of g-functions by automatically evaluating the required thermal resistances for the creation of `Pipe` objects. ### Other changes diff --git a/pygfunction/enums.py b/pygfunction/enums.py new file mode 100644 index 0000000..cf19f2f --- /dev/null +++ b/pygfunction/enums.py @@ -0,0 +1,21 @@ +from enum import Enum, auto +from typing import Annotated + + +class PipeType(Enum): + """Enumerator for pipe configuration type.""" + COAXIAL_ANNULAR_IN: Annotated[ + int, "Coaxial pipe (annular inlet)" + ] = auto() + COAXIAL_ANNULAR_OUT: Annotated[ + int, "Coaxial pipe (annular outlet)" + ] = auto() + DOUBLE_UTUBE_PARALLEL: Annotated[ + int, "Double U-tube (parallel)" + ] = auto() + DOUBLE_UTUBE_SERIES: Annotated[ + int, "Double U-tube (series)" + ] = auto() + SINGLE_UTUBE: Annotated[ + int, "Single U-tube" + ] = auto() diff --git a/pygfunction/gfunction.py b/pygfunction/gfunction.py index 658a2f5..f80a9fb 100644 --- a/pygfunction/gfunction.py +++ b/pygfunction/gfunction.py @@ -1,23 +1,26 @@ # -*- coding: utf-8 -*- import warnings from time import perf_counter +from typing import Union, List import numpy as np +import numpy.typing as npt from scipy.interpolate import interp1d as interp1d from .borefield import Borefield from .boreholes import Borehole, find_duplicates +from .media import Fluid from .networks import Network from .solvers import ( Detailed, Equivalent, Similarities - ) +) from .utilities import ( segment_ratios, _initialize_figure, _format_axes - ) +) class gFunction(object): @@ -282,6 +285,170 @@ def __init__(self, boreholes_or_network, alpha, time=None, if self.time is not None: self.gFunc = self.evaluate_g_function(self.time) + @classmethod + def from_static_params(cls, + H: npt.ArrayLike, + D: npt.ArrayLike, + r_b: npt.ArrayLike, + x: npt.ArrayLike, + y: npt.ArrayLike, + alpha: float, + time: npt.ArrayLike = None, + method: str = 'equivalent', + m_flow_network: float = None, + options={}, + tilt: npt.ArrayLike = 0., + orientation: npt.ArrayLike = 0., + boundary_condition: str = 'MIFT', + pipe_type_str: str = None, + pos: List[tuple] = None, + r_in: Union[float, tuple, npt.ArrayLike] = None, + r_out: Union[float, tuple, npt.ArrayLike] = None, + k_s: float = None, + k_g: float = None, + k_p: Union[float, tuple, npt.ArrayLike] = None, + fluid_str: str = None, + fluid_concentration_pct: float = None, + fluid_temperature: float = 20, + epsilon: float = None, + reversible_flow: bool = True, + bore_connectivity: list = None, + J: int = 2, + ): + + """ + Constructs the 'gFunction' class from static parameters. + + Parameters + ---------- + 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. + alpha : float + Soil thermal diffusivity (in m2/s). + time : float or array, optional + Values of time (in seconds) for which the g-function is evaluated. The + g-function is only evaluated at initialization if a value is provided. + Default is None. + method : str, optional + Method for the evaluation of the g-function. Should be one of 'similarities', 'detailed', or 'equivalent'. + Default is 'equivalent'. See 'gFunction' __init__ for more details. + m_flow_network : float, optional + Fluid mass flow rate into the network of boreholes (in kg/s). + Default is None. + options : dict, optional + A dictionary of solver options. See 'gFunction' __init__ for more details. + 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. + boundary_condition : str, optional + Boundary condition for the evaluation of the g-function. Should be one of 'UHTR', 'UBWT', or 'MIFT'. + Default is 'MIFT'. + pipe_type_str : str, optional + Pipe type used for 'MIFT' boundary condition. Should be one of 'COAXIAL_ANNULAR_IN', 'COAXIAL_ANNULAR_OUT', + 'DOUBLE_UTUBE_PARALLEL', 'DOUBLE_UTUBE_SERIES', or 'SINGLE_UTUBE'. + pos : list of tuples, optional + Position (x, y) (in meters) of the pipes inside the borehole. + r_in : float, optional + Inner radius (in meters) of the U-Tube pipes. + r_out : float, optional + Outer radius (in meters) of the U-Tube pipes. + k_s : float, optional + Soil thermal conductivity (in W/m-K). + k_g : float, optional + Grout thermal conductivity (in W/m-K). + k_p : float, optional + Pipe thermal conductivity (in W/m-K). + fluid_str: str, optional + The mixer for this application should be one of: + - 'Water' - Complete water solution + - 'MEG' - Ethylene glycol mixed with water + - 'MPG' - Propylene glycol mixed with water + - 'MEA' - Ethanol mixed with water + - 'MMA' - Methanol mixed with water + fluid_concentration_pct: float, optional + Mass fraction of the mixing fluid added to water (in %). + Lower bound = 0. Upper bound is dependent on the mixture. + fluid_temperature: float, optional + Temperature used for evaluating fluid properties (in degC). + Default is 20. + epsilon : float, optional + Pipe roughness (in meters). + reversible_flow : bool, optional + True to treat a negative mass flow rate as the reversal of flow + direction within the borehole. If False, the direction of flow is not + reversed when the mass flow rate is negative, and the absolute value is + used for calculations. + Default True. + bore_connectivity : list, optional + Index of fluid inlet into each borehole. -1 corresponds to a borehole + connected to the bore field inlet. If this parameter is not provided, + parallel connections between boreholes is used. + Default is None. + J : int, optional + Number of multipoles per pipe to evaluate the thermal resistances. + J=1 or J=2 usually gives sufficient accuracy. J=0 corresponds to the + line source approximation. + Default is 2. + + Returns + ------- + gFunction : 'gFunction' object + The g-function. + + Notes + ----- + - When using the 'MIFT' boundary condition, the parameters `pipe_type_str`, + `fluid_str`, `fluid_concentration_pct`, `fluid_temperature`, and + `epsilon` are required. + + """ + + if boundary_condition.upper() not in ['UBWT', 'UHTR', 'MIFT']: + raise ValueError(f"'{boundary_condition}' is not a valid boundary condition.") + + # construct all required pieces + borefield = Borefield(H, D, r_b, x, y, tilt, orientation) + + if boundary_condition.upper() == 'MIFT': + boreholes = borefield.to_boreholes() + cp_f = Fluid(fluid_str, fluid_concentration_pct).cp + boreholes_or_network= Network.from_static_params( + boreholes, + pipe_type_str, + pos, + r_in, + r_out, + k_s, + k_g, + k_p, + m_flow_network, + epsilon, + fluid_str, + fluid_concentration_pct, + fluid_temperature, + reversible_flow, + bore_connectivity, + J, + ) + else: + boreholes_or_network = borefield + cp_f = None + + return cls(boreholes_or_network, alpha, time=time, method=method, boundary_condition=boundary_condition, + m_flow_network=m_flow_network, cp_f=cp_f, options=options) + def evaluate_g_function(self, time): """ Evaluate the g-function. @@ -520,7 +687,7 @@ def visualize_heat_extraction_rates( # Adjust figure to window plt.tight_layout() - + return fig def visualize_heat_extraction_rate_profiles( diff --git a/pygfunction/networks.py b/pygfunction/networks.py index 9fa5884..719f364 100644 --- a/pygfunction/networks.py +++ b/pygfunction/networks.py @@ -1,7 +1,17 @@ # -*- coding: utf-8 -*- +from typing import Union, List + import numpy as np +import numpy.typing as npt from scipy.linalg import block_diag +from .borefield import Borefield +from .boreholes import Borehole +from .enums import PipeType +from .media import Fluid +from .pipes import SingleUTube, MultipleUTube, Coaxial +from .pipes import fluid_to_pipe_thermal_resistance, fluid_to_fluid_thermal_resistance + class Network(object): """ @@ -9,7 +19,7 @@ class Network(object): connections between the boreholes. Contains information regarding the physical dimensions and thermal - characteristics of the pipes and the grout material in each boreholes, the + characteristics of the pipes and the grout material in each borehole, the topology of the connections between boreholes, as well as methods to evaluate fluid temperatures and heat extraction rates based on the work of Cimmino (2018, 2019, 2024) [#Network-Cimmin2018]_, [#Network-Cimmin2019]_, @@ -93,6 +103,148 @@ def __init__(self, boreholes, pipes, bore_connectivity=None, self._initialize_stored_coefficients( m_flow_network, cp_f, nSegments, segment_ratios) + @classmethod + def from_static_params(cls, + boreholes: Union[List[Borehole], Borefield], + pipe_type_str: str, + pos: List[tuple], + r_in: Union[float, tuple, npt.ArrayLike], + r_out: Union[float, tuple, npt.ArrayLike], + k_s: float, + k_g: float, + k_p: Union[float, tuple, npt.ArrayLike], + m_flow_network: float, + epsilon: float, + fluid_str: str, + fluid_concentraton_percent: float, + fluid_temperature: float, + reversible_flow: bool = True, + bore_connectivity: list = None, + J: int = 2): + """ + Constructs the 'Network' class from static parameters. + + Parameters + ---------- + boreholes : list of Borehole objects + List of boreholes included in the bore field. + pipe_type_str : str + Should be one of 'COAXIAL_ANNULAR_IN', 'COAXIAL_ANNULAR_OUT', + 'DOUBLE_UTUBE_PARALLEL', 'DOUBLE_UTUBE_SERIES', or 'SINGLE_UTUBE'. + pos : list of tuples + Position (x, y) (in meters) of the pipes inside the borehole. + r_in : float + Inner radius (in meters) of the U-Tube pipes. + r_out : float + Outer radius (in meters) of the U-Tube pipes. + k_s : float + Soil thermal conductivity (in W/m-K). + k_g : float + Grout thermal conductivity (in W/m-K). + k_p : float, tuple, or (2,) array + Pipe thermal conductivity (in W/m-K). + m_flow_network : float + Fluid mass flow rate into the network of boreholes (in kg/s). + epsilon : float + Pipe roughness (in meters). + fluid_str: str + The mixer for this application should be one of: + - 'Water' - Complete water solution + - 'MEG' - Ethylene glycol mixed with water + - 'MPG' - Propylene glycol mixed with water + - 'MEA' - Ethanol mixed with water + - 'MMA' - Methanol mixed with water + fluid_concentration_pct: float + Mass fraction of the mixing fluid added to water (in %). + Lower bound = 0. Upper bound is dependent on the mixture. + fluid_temperature: float, optional + Temperature used for evaluating fluid properties (in degC). + Default is 20. + reversible_flow : bool, optional + True to treat a negative mass flow rate as the reversal of flow + direction within the borehole. If False, the direction of flow is not + reversed when the mass flow rate is negative, and the absolute value is + used for calculations. + Default is True. + bore_connectivity : list, optional + Index of fluid inlet into each borehole. -1 corresponds to a borehole + connected to the bore field inlet. If this parameter is not provided, + parallel connections between boreholes is used. + Default is None. + J : int, optional + Number of multipoles per pipe to evaluate the thermal resistances. + J=1 or J=2 usually gives sufficient accuracy. J=0 corresponds to the + line source approximation. + Default is 2. + + Returns + ------- + Network : 'Network' object. + The network. + + """ + # Convert borefield to list + if isinstance(boreholes, Borefield): + boreholes = boreholes.to_boreholes() + + # The total fluid mass flow rate is divided equally amongst inlets + if bore_connectivity is None: + m_flow_borehole = abs(m_flow_network / len(boreholes)) + else: + m_flow_borehole = abs(m_flow_network / bore_connectivity.count(-1)) + + # Pipe and fluid types + pipe_type = PipeType[pipe_type_str.upper()] + fluid = Fluid(fluid_str, fluid_concentraton_percent, fluid_temperature) + + if pipe_type == PipeType.SINGLE_UTUBE: + # Single U-tube borehole + R_fp = fluid_to_pipe_thermal_resistance( + pipe_type, m_flow_borehole, r_in, r_out, k_p, epsilon, fluid) + pipes = [ + SingleUTube( + pos, r_in, r_out, borehole, k_s, k_g, R_fp, J, reversible_flow) + for borehole in boreholes + ] + + elif pipe_type == PipeType.DOUBLE_UTUBE_PARALLEL: + # Double U-tube borehole (parallel) + R_fp = fluid_to_pipe_thermal_resistance( + pipe_type, m_flow_borehole, r_in, r_out, k_p, epsilon, fluid) + pipes = [ + MultipleUTube( + pos, r_in, r_out, borehole, k_s, k_g, R_fp, 2, 'parallel', J, reversible_flow) + for borehole in boreholes + ] + + elif pipe_type == PipeType.DOUBLE_UTUBE_SERIES: + # Double U-tube borehole (series) + R_fp = fluid_to_pipe_thermal_resistance( + pipe_type, m_flow_borehole, r_in, r_out, k_p, epsilon, fluid) + pipes = [ + MultipleUTube( + pos, r_in, r_out, borehole, k_s, k_g, R_fp, 2, 'series', J, reversible_flow) + for borehole in boreholes + ] + + elif pipe_type in [PipeType.COAXIAL_ANNULAR_IN, PipeType.COAXIAL_ANNULAR_OUT]: + # Coaxial borehole + R_fp = fluid_to_pipe_thermal_resistance( + pipe_type, m_flow_borehole, r_in, r_out, k_p, epsilon, fluid) + R_ff = fluid_to_fluid_thermal_resistance( + pipe_type, m_flow_borehole, r_in, r_out, k_p, epsilon, fluid) + pipes = [ + Coaxial( + pos, np.array(r_in), np.array(r_out), borehole, k_s, k_g, R_ff, R_fp, J, reversible_flow) + for borehole in boreholes + ] + + else: + raise ValueError(f"Unsupported pipe_type: '{pipe_type_str}'") + + return cls(boreholes=boreholes, pipes=pipes, m_flow_network=m_flow_network, bore_connectivity=bore_connectivity, + cp_f=fluid.cp) + def get_inlet_temperature( self, T_f_in, T_b, m_flow_network, cp_f, nSegments, segment_ratios=None): @@ -577,7 +729,7 @@ def coefficients_outlet_temperature( def coefficients_network_inlet_temperature( self, m_flow_network, cp_f, nSegments, segment_ratios=None): """ - Build coefficient matrices to evaluate intlet fluid temperature of the + Build coefficient matrices to evaluate inlet fluid temperature of the network. Returns coefficients for the relation: @@ -992,11 +1144,11 @@ def _initialize_coefficients_connectivity(self): def _initialize_stored_coefficients( self, m_flow_network, cp_f, nSegments, segment_ratios): nMethods = 7 # Number of class methods - self._stored_coefficients = [() for i in range(nMethods)] + self._stored_coefficients = [() for _ in range(nMethods)] self._stored_m_flow_cp = [np.empty(self.nInlets)*np.nan - for i in range(nMethods)] - self._stored_nSegments = [np.nan for i in range(nMethods)] - self._stored_segment_ratios = [np.nan for i in range(nMethods)] + for _ in range(nMethods)] + self._stored_nSegments = [np.nan for _ in range(nMethods)] + self._stored_segment_ratios = [np.nan for _ in range(nMethods)] self._m_flow_cp_model_variables = np.empty(self.nInlets)*np.nan self._nSegments_model_variables = np.nan self._segment_ratios_model_variables = np.nan diff --git a/pygfunction/pipes.py b/pygfunction/pipes.py index 0d86a52..c5ceb5e 100644 --- a/pygfunction/pipes.py +++ b/pygfunction/pipes.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- import warnings +from typing import Union import numpy as np +import numpy.typing as npt from scipy.constants import pi from scipy.special import binom +from .enums import PipeType +from .media import Fluid from .utilities import _initialize_figure, _format_axes - class _BasePipe(object): """ Template for pipe classes. @@ -3506,7 +3509,7 @@ def _F_mk(q_p, P, n_p, J, r_b, r_out, z, pikg, sigma): P : array Multipoles. n_p : int - Total numper of pipes. + Total number of pipes. J : int Number of multipoles per pipe to evaluate the thermal resistances. J=1 or J=2 usually gives sufficient accuracy. J=0 corresponds to the @@ -3613,3 +3616,224 @@ def _Nusselt_number_turbulent_flow(Re, Pr, fDarcy): Nu = 0.125 * fDarcy * (Re - 1.0e3) * Pr / \ (1.0 + 12.7 * np.sqrt(0.125*fDarcy) * (Pr**(2.0/3.0) - 1.0)) return Nu + + +def fluid_to_pipe_thermal_resistance( + pipe_type: PipeType, m_flow_borehole: float, + r_in: Union[float, tuple, npt.ArrayLike], r_out: Union[float, tuple, npt.ArrayLike], + k_p: Union[float, tuple, npt.ArrayLike], epsilon: float, + fluid: Fluid) -> float: + """ + Computes the fluid to pipe thermal resistance. + + Parameters + ---------- + pipe_type : PipeType + Should be one of 'PipeType.COAXIAL_ANNULAR_IN', 'PipeType.COAXIAL_ANNULAR_OUT', + 'PipeType.DOUBLE_UTUBE_PARALLEL', 'PipeType.DOUBLE_UTUBE_SERIES', or 'PipeType.SINGLE_UTUBE'. + m_flow_borehole : float + Fluid mass flow rate the borehole (in kg/s). + r_in : float + Inner radius (in meters) of the U-Tube pipes. + r_out : float + Outer radius (in meters) of the U-Tube pipes. + k_p : float + Pipe thermal conductivity (in W/m-K). + epsilon : float + Pipe roughness (in meters). + fluid : Fluid + 'Fluid' class object. Used for evaluating fluid properties + + Returns + ------- + float + fluid to pipe thermal resistance (in m-K/W) + + """ + + if pipe_type in [PipeType.SINGLE_UTUBE, PipeType.DOUBLE_UTUBE_SERIES]: + + # The fluid mass flow rate corresponds to the total flow + m_flow_pipe = m_flow_borehole + + # Pipe thermal resistance + R_p = conduction_thermal_resistance_circular_pipe( + r_in, r_out, k_p) + # Convection heat transfer coefficient [W/m2.K] + h_f = convective_heat_transfer_coefficient_circular_pipe( + m_flow_pipe, r_in, fluid.mu, fluid.rho, fluid.k, fluid.cp, + epsilon) + # Film thermal resistance [m.K/W] + R_f = 1.0 / (h_f * 2 * np.pi * r_in) + + return R_p + R_f + + elif pipe_type == PipeType.DOUBLE_UTUBE_PARALLEL: + + # The fluid mass flow rate is divided into the two parallel pipes + m_flow_pipe = m_flow_borehole / 2 + + # Pipe thermal resistance + R_p = conduction_thermal_resistance_circular_pipe( + r_in, r_out, k_p) + # Convection heat transfer coefficient [W/m2.K] + h_f = convective_heat_transfer_coefficient_circular_pipe( + m_flow_pipe, r_in, fluid.mu, fluid.rho, fluid.k, fluid.cp, + epsilon) + # Film thermal resistance [m.K/W] + R_f = 1.0 / (h_f * 2 * np.pi * r_in) + + return R_p + R_f + + elif pipe_type == PipeType.COAXIAL_ANNULAR_IN: + + # The fluid mass flow rate corresponds to the total flow + m_flow_pipe = m_flow_borehole + + # The annular channel is at index 0 + r_in_out = r_out[1] + r_out_in = r_in[0] + r_out_out = r_out[0] + k_p_out = k_p[0] + + # Outer pipe + R_p_out = conduction_thermal_resistance_circular_pipe( + r_out_in, r_out_out, k_p_out) + + # Outer pipe + h_f_a_in, h_f_a_out = \ + convective_heat_transfer_coefficient_concentric_annulus( + m_flow_pipe, r_in_out, r_out_in, fluid.mu, fluid.rho, fluid.k, + fluid.cp, epsilon) + + # Coaxial GHE in borehole + R_f_out_out = 1.0 / (h_f_a_out * 2 * np.pi * r_out_in) + return R_p_out + R_f_out_out + + elif pipe_type == PipeType.COAXIAL_ANNULAR_OUT: + + # The fluid mass flow rate corresponds to the total flow + m_flow_pipe = m_flow_borehole + + # The annular channel is at index 1 + r_in_out = r_out[0] + r_out_in = r_in[1] + r_out_out = r_out[1] + k_p_out = k_p[1] + + # Outer pipe + R_p_out = conduction_thermal_resistance_circular_pipe( + r_out_in, r_out_out, k_p_out) + # Fluid-to-fluid thermal resistance [m.K/W] + + # Outer pipe + h_f_a_in, h_f_a_out = \ + convective_heat_transfer_coefficient_concentric_annulus( + m_flow_pipe, r_in_out, r_out_in, fluid.mu, fluid.rho, fluid.k, + fluid.cp, epsilon) + + # Coaxial GHE in borehole + R_f_out_out = 1.0 / (h_f_a_out * 2 * np.pi * r_out_in) + + return R_p_out + R_f_out_out + + else: + raise ValueError(f"Unsupported pipe_type: '{pipe_type.name}'") + + +def fluid_to_fluid_thermal_resistance(pipe_type: PipeType, m_flow_borehole: float, + r_in: Union[float, tuple, npt.ArrayLike], + r_out: Union[float, tuple, npt.ArrayLike], + k_p: Union[float, tuple, npt.ArrayLike], epsilon: float, + fluid: Fluid) -> float: + """ + Computes the fluid to fluid thermal resistance. + + Parameters + ---------- + pipe_type : PipeType + Should be one of 'PipeType.COAXIAL_ANNULAR_IN', 'PipeType.COAXIAL_ANNULAR_OUT', + 'PipeType.DOUBLE_UTUBE_PARALLEL', 'PipeType.DOUBLE_UTUBE_SERIES', or 'PipeType.SINGLE_UTUBE'. + m_flow_borehole : float + Fluid mass flow rate the borehole (in kg/s). + r_in : float + Inner radius (in meters) of the U-Tube pipes. + r_out : float + Outer radius (in meters) of the U-Tube pipes. + k_p : float + Pipe thermal conductivity (in W/m-K). + epsilon : float + Pipe roughness (in meters). + fluid : Fluid + 'Fluid' class object. Used for evaluating fluid properties + + Returns + ------- + float + fluid to fluid thermal resistance (in m-K/W) + + """ + + if pipe_type == PipeType.COAXIAL_ANNULAR_IN: + + # The fluid mass flow rate corresponds to the total flow + m_flow_pipe = m_flow_borehole + + # The annular channel is at index 0 + r_in_in = r_in[1] + r_in_out = r_out[1] + r_out_in = r_in[0] + k_p_in = k_p[1] + + # Inner pipe + R_p_in = conduction_thermal_resistance_circular_pipe( + r_in_in, r_in_out, k_p_in) + + # Fluid-to-fluid thermal resistance [m.K/W] + # Inner pipe + h_f_in = convective_heat_transfer_coefficient_circular_pipe( + m_flow_pipe, r_in_in, fluid.mu, fluid.rho, fluid.k, fluid.cp, epsilon) + R_f_in = 1.0 / (h_f_in * 2 * np.pi * r_in_in) + + # Outer pipe + h_f_a_in, h_f_a_out = \ + convective_heat_transfer_coefficient_concentric_annulus( + m_flow_borehole, r_in_out, r_out_in, fluid.mu, fluid.rho, fluid.k, + fluid.cp, epsilon) + R_f_out_in = 1.0 / (h_f_a_in * 2 * np.pi * r_in_out) + + return R_f_in + R_p_in + R_f_out_in + + elif pipe_type == PipeType.COAXIAL_ANNULAR_OUT: + + # The fluid mass flow rate corresponds to the total flow + m_flow_pipe = m_flow_borehole + + # The annular channel is at index 1 + r_in_in = r_in[0] + r_in_out = r_out[0] + r_out_in = r_in[1] + k_p_in = k_p[0] + + # Pipe thermal resistances [m.K/W] + # Inner pipe + R_p_in = conduction_thermal_resistance_circular_pipe( + r_in_in, r_in_out, k_p_in) + + # Fluid-to-fluid thermal resistance [m.K/W] + # Inner pipe + h_f_in = convective_heat_transfer_coefficient_circular_pipe( + m_flow_pipe, r_in_in, fluid.mu, fluid.rho, fluid.k, fluid.cp, epsilon) + R_f_in = 1.0 / (h_f_in * 2 * np.pi * r_in_in) + + # Outer pipe + h_f_a_in, h_f_a_out = \ + convective_heat_transfer_coefficient_concentric_annulus( + m_flow_pipe, r_in_out, r_out_in, fluid.mu, fluid.rho, fluid.k, + fluid.cp, epsilon) + R_f_out_in = 1.0 / (h_f_a_in * 2 * np.pi * r_in_out) + + return R_f_in + R_p_in + R_f_out_in + + else: + raise ValueError(f"Unsupported pipe_type: '{pipe_type.name}'") diff --git a/pygfunction/solvers/detailed.py b/pygfunction/solvers/detailed.py index eb66e4b..ec9113f 100644 --- a/pygfunction/solvers/detailed.py +++ b/pygfunction/solvers/detailed.py @@ -34,11 +34,11 @@ class Detailed(_BaseSolver): 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) [#Detailed-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) [#Detailed-CimBer2014]_. - 'MIFT' : diff --git a/pygfunction/solvers/equivalent.py b/pygfunction/solvers/equivalent.py index 0a4129a..f23943c 100644 --- a/pygfunction/solvers/equivalent.py +++ b/pygfunction/solvers/equivalent.py @@ -45,11 +45,11 @@ class Equivalent(_BaseSolver): 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) [#Equivalent-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) [#Equivalent-CimBer2014]_. - 'MIFT' : diff --git a/pygfunction/solvers/similarities.py b/pygfunction/solvers/similarities.py index 904de2c..887d144 100644 --- a/pygfunction/solvers/similarities.py +++ b/pygfunction/solvers/similarities.py @@ -35,11 +35,11 @@ class Similarities(_BaseSolver): 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) [#Similarities-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) [#Similarities-CimBer2014]_. - 'MIFT' : @@ -186,7 +186,7 @@ def thermal_response_factors(self, time, alpha, kind='linear'): of thermal response factors, containing a copy of the matrix accessible by h_ij.y[:nSources,:nSources,:nt+1]. The first index along the third axis corresponds to time t=0. The interp1d object can be used to - obtain thermal response factors at any intermediat time by + obtain thermal response factors at any intermediate time by h_ij(t)[:nSources,:nSources]. Attributes diff --git a/tests/gfunction_test.py b/tests/gfunction_test.py index 98255c1..5c64d4f 100644 --- a/tests/gfunction_test.py +++ b/tests/gfunction_test.py @@ -80,6 +80,30 @@ def test_gfunctions_UBWT(field, method, opts, expected, request): boundary_condition='UBWT') assert np.allclose(gFunc.gFunc, expected) + # test from static parameters + + # convert to lists for testing + H = list(borefield.H) + D = list(borefield.D) + r_b = list(borefield.r_b) + x = list(borefield.x) + y = list(borefield.y) + + # get gFunction object with static parameters + gFunc_from_params = gt.gfunction.gFunction.from_static_params( + H=H, + D=D, + r_b=r_b, + x=x, + y=y, + alpha=alpha, + options=options, + method=method, + boundary_condition='UBWT', + ) + gFunc_from_params.evaluate_g_function(time) + assert np.allclose(gFunc_from_params.gFunc, expected) + # Test 'UHTR' g-functions for different bore fields using all solvers, # unequal/uniform segments, and with/without the FLS approximation @@ -151,6 +175,30 @@ def test_gfunctions_UHTR(field, method, opts, expected, request): boundary_condition='UHTR') assert np.allclose(gFunc.gFunc, expected) + # test from static parameters + + # convert to lists for testing + H = list(borefield.H) + D = list(borefield.D) + r_b = list(borefield.r_b) + x = list(borefield.x) + y = list(borefield.y) + + # get gFunction object with static parameters + gFunc_from_params = gt.gfunction.gFunction.from_static_params( + H=H, + D=D, + r_b=r_b, + x=x, + y=y, + alpha=alpha, + options=options, + method=method, + boundary_condition='UHTR', + ) + gFunc_from_params.evaluate_g_function(time) + assert np.allclose(gFunc_from_params.gFunc, expected) + # Test 'MIFT' g-functions for different bore fields using all solvers, # unequal/uniform segments, and with/without the FLS approximation @@ -234,6 +282,56 @@ def test_gfunctions_MIFT( cp_f=fluid.cp, method=method, options=options, boundary_condition='MIFT') assert np.allclose(gFunc.gFunc, expected) + # test from static parameters + + # convert to lists for testing + H = [bore.H for bore in network.b] + D = [bore.D for bore in network.b] + r_b = [bore.r_b for bore in network.b] + x = [bore.x for bore in network.b] + y = [bore.y for bore in network.b] + + # static params + k_s = 2.0 + k_g = 1.0 + k_p = 0.4 + epsilon = 1e-6 + fluid_name = 'MPG' + fluid_pct = 20. + + # Extract the pipe options from the fixture + pipe = network.p[0] + pos = pipe.pos + r_in = pipe.r_in + r_out = pipe.r_out + + # get gFunction object with static parameters + gFunc_from_params = gt.gfunction.gFunction.from_static_params( + H=H, + D=D, + r_b=r_b, + x=x, + y=y, + alpha=alpha, + options=options, + method=method, + boundary_condition='MIFT', + m_flow_network=m_flow_network, + fluid_str=fluid_name, + fluid_concentration_pct=fluid_pct, + pipe_type_str='single_utube', + pos=pos, + r_in=r_in, + r_out=r_out, + k_s=k_s, + k_g=k_g, + k_p=k_p, + epsilon=epsilon, + bore_connectivity=network.c, + ) + gFunc_from_params.evaluate_g_function(time) + assert np.allclose(gFunc_from_params.gFunc, expected) + # Test 'MIFT' g-functions for different bore fields using all solvers, unequal # segments, and with the FLS approximation for variable mass flow rate @@ -274,6 +372,7 @@ def test_gfunctions_MIFT_variable_mass_flow_rate( network, alpha, time=time, m_flow_network=m_flow_network, cp_f=fluid.cp, method=method, options=options, boundary_condition='MIFT') assert np.allclose(gFunc.gFunc, expected) + # variable mass flow not currently supported from gFunction.from_static_params # ============================================================================= @@ -299,7 +398,7 @@ def test_gfunctions_MIFT_variable_mass_flow_rate( # 'detailed' solver - uniform segments, FLS approximation ('detailed', 'uniform_segments_approx', np.array([5.67916493, 6.7395222 , 7.16339216])), ]) -def test_gfunctions_UBWT(two_boreholes_inclined, method, opts, expected, request): +def test_gfunctions_UBWT_inclined(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 @@ -317,6 +416,34 @@ def test_gfunctions_UBWT(two_boreholes_inclined, method, opts, expected, request boundary_condition='UBWT') assert np.allclose(gFunc.gFunc, expected) + # test from static parameters + + # convert to lists for testing + H = list(borefield.H) + D = list(borefield.D) + r_b = list(borefield.r_b) + x = list(borefield.x) + y = list(borefield.y) + tilt = list(borefield.tilt) + orientation = list(borefield.orientation) + + # get gFunction object with static parameters + gFunc_from_params = gt.gfunction.gFunction.from_static_params( + H=H, + D=D, + r_b=r_b, + x=x, + y=y, + tilt=tilt, + orientation=orientation, + alpha=alpha, + options=options, + method=method, + boundary_condition='UBWT', + ) + gFunc_from_params.evaluate_g_function(time) + assert np.allclose(gFunc_from_params.gFunc, expected) + # ============================================================================= # Test gFunction linearization @@ -339,3 +466,141 @@ def test_gfunctions_UBWT_linearization(field, method, opts, expected, request): borefield, alpha, time=time, method=method, options=options, boundary_condition='UBWT') assert np.allclose(gFunc.gFunc, expected) + + # test from static parameters + + # convert to lists for testing + H = list(borefield.H) + D = list(borefield.D) + r_b = list(borefield.r_b) + x = list(borefield.x) + y = list(borefield.y) + + # get gFunction object with static parameters + gFunc_from_params = gt.gfunction.gFunction.from_static_params( + H=H, + D=D, + r_b=r_b, + x=x, + y=y, + alpha=alpha, + options=options, + method=method, + boundary_condition='UBWT', + ) + gFunc_from_params.evaluate_g_function(time) + assert np.allclose(gFunc_from_params.gFunc, expected) + + +@pytest.mark.parametrize("field, boundary_condition, method, opts, pipe_type, m_flow_network, expected", [ + # 'equivalent' solver - unequal segments - UBWT - single u-tube + ('single_borehole', 'UBWT', 'equivalent', 'unequal_segments', 'single_Utube', 0.05, np.array([5.59717446, 6.36257605, 6.60517223])), + ('single_borehole_short', 'UBWT', 'equivalent', 'unequal_segments', 'single_Utube', 0.05, np.array([4.15784411, 4.98477603, 5.27975732])), + ('ten_boreholes_rectangular', 'UBWT', 'equivalent', 'unequal_segments', 'single_Utube', 0.25, np.array([10.89935004, 17.09864925, 19.0795435])), + # 'equivalent' solver - unequal segments - UHTR - single u-tube + ('single_borehole', 'UHTR', 'equivalent', 'unequal_segments', 'single_Utube', 0.05, np.array([5.61855789, 6.41336758, 6.66933682])), + ('single_borehole_short', 'UHTR', 'equivalent', 'unequal_segments', 'single_Utube', 0.05, np.array([4.18276733, 5.03671562, 5.34369772])), + ('ten_boreholes_rectangular', 'UHTR', 'equivalent', 'unequal_segments', 'single_Utube', 0.25, np.array([11.27831804, 18.48075762, 21.00669237])), + # 'equivalent' solver - unequal segments - MIFT - single u-tube + ('single_borehole', 'MIFT', 'equivalent', 'unequal_segments', 'single_Utube', 0.05, np.array([5.76597302, 6.51058473, 6.73746895])), + ('single_borehole_short', 'MIFT', 'equivalent', 'unequal_segments', 'single_Utube', 0.05, np.array([4.17105954, 5.00930075, 5.30832133])), + ('ten_boreholes_rectangular', 'MIFT', 'equivalent', 'unequal_segments', 'single_Utube', 0.25, np.array([12.66229998, 18.57852681, 20.33535907])), + # 'equivalent' solver - unequal segments - MIFT - double u-tube parallel + ('single_borehole', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_parallel', 0.05, np.array([6.47497545, 7.18728277, 7.39167598])), + ('single_borehole_short', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_parallel', 0.05, np.array([4.17080765, 5.00341368, 5.2989709])), + ('ten_boreholes_rectangular', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_parallel', 0.25, np.array([15.96448954, 21.43320976, 22.90761598])), + # 'equivalent' solver - unequal segments - MIFT - double u-tube series + ('single_borehole', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_series', 0.05, np.array([5.69118368, 6.44386342, 6.67721347])), + ('single_borehole_short', 'MIFT','equivalent', 'unequal_segments', 'double_Utube_series', 0.05, np.array([4.16750616, 5.00249502, 5.30038701])), + ('ten_boreholes_rectangular', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_series', 0.25, np.array([11.94256058, 17.97858109, 19.83460231])), + # 'equivalent' solver - unequal segments - MIFT - double u-tube series asymmetrical + ('single_borehole', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_series_asymmetrical', 0.05, np.array([5.69174709, 6.4441862 , 6.67709693])), + ('single_borehole_short', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_series_asymmetrical', 0.05, np.array([4.16851817, 5.00453267, 5.30282913])), + ('ten_boreholes_rectangular', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_series_asymmetrical', 0.25, np.array([11.96927941, 18.00481705, 19.856554])), + # 'equivalent' solver - unequal segments - MIFT - double u-tube series asymmetrical + ('single_borehole', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_series_asymmetrical', 0.05, np.array([5.69174709, 6.4441862, 6.67709693])), + ('single_borehole_short', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_series_asymmetrical', 0.05, np.array([4.16851817, 5.00453267, 5.30282913])), + ('ten_boreholes_rectangular', 'MIFT', 'equivalent', 'unequal_segments', 'double_Utube_series_asymmetrical', 0.25, np.array([11.96927941, 18.00481705, 19.856554])), + # 'equivalent' solver - unequal segments - MIFT - coaxial annular inlet + ('single_borehole', 'MIFT', 'equivalent', 'unequal_segments', 'coaxial_annular_in', 0.05, np.array([6.10236427, 6.77069069, 6.95941276])), + ('single_borehole_short', 'MIFT', 'equivalent', 'unequal_segments', 'coaxial_annular_in', 0.05, np.array([4.06874781, 4.89701125, 5.19157017])), + ('ten_boreholes_rectangular', 'MIFT', 'equivalent', 'unequal_segments', 'coaxial_annular_in', 0.25, np.array([16.03433989, 21.18241954, 22.49479982])), + # 'equivalent' solver - unequal segments - MIFT - coaxial annular outlet + ('single_borehole', 'MIFT', 'equivalent', 'unequal_segments', 'coaxial_annular_out', 0.05, np.array([6.10236427, 6.77069069, 6.95941276])), + ('single_borehole_short', 'MIFT', 'equivalent', 'unequal_segments', 'coaxial_annular_out', 0.05, np.array([4.06874781, 4.89701125, 5.19157017])), + ('ten_boreholes_rectangular', 'MIFT', 'equivalent', 'unequal_segments', 'coaxial_annular_out', 0.25, np.array([16.03433989, 21.18241954, 22.49510883])), + ]) +def test_gfunctions_from_static_params(field, boundary_condition, method, opts, pipe_type, m_flow_network, expected, + request): + # Extract the bore field from the fixture for convenience + borefield = request.getfixturevalue(field) + + # convert to lists for testing + H = list(borefield.H) + D = list(borefield.D) + r_b = list(borefield.r_b) + x = list(borefield.x) + y = list(borefield.y) + + # Extract the g-function options from the fixture + options = request.getfixturevalue(opts) + + # Extract the pipe options from the fixture, if needed + pipe = request.getfixturevalue(pipe_type) + pos = pipe.pos + r_in = pipe.r_in + r_out = pipe.r_out + + # replace pipe_type from fixture + if pipe_type in ['single_Utube', 'double_Utube_parallel', 'double_Utube_series', + 'double_Utube_series_asymmetrical']: + k_p = 0.4 + elif pipe_type in ['coaxial_annular_in', 'coaxial_annular_out']: + k_p = (0.4, 0.4) + else: + raise ValueError(f"test pipe_type not recognized: '{pipe_type}'") + + pipe_type = pipe_type.removesuffix('_asymmetrical') + + # Static params + k_s = 2.0 + k_g = 1.0 + epsilon = 1e-6 + fluid_name = 'MPG' + fluid_pct = 20. + + # Mean borehole length [m] + H_mean = np.mean(H) + alpha = 1e-6 # Ground thermal diffusivity [m2/s] + + gfunc = gt.gfunction.gFunction.from_static_params( + H=H, + D=D, + r_b=r_b, + x=x, + y=y, + alpha=alpha, + options=options, + method=method, + boundary_condition=boundary_condition, + k_p=k_p, + k_s=k_s, + k_g=k_g, + epsilon=epsilon, + fluid_str=fluid_name, + fluid_concentration_pct=fluid_pct, + pos=pos, + r_in=r_in, + r_out=r_out, + pipe_type_str=pipe_type, + m_flow_network=m_flow_network, + ) + + # 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 + + # evaluate g-function + gFunc = gfunc.evaluate_g_function(time=time) + assert np.allclose(gFunc, expected)