|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +from time import perf_counter |
| 3 | + |
| 4 | +import numpy as np |
| 5 | +from scipy.interpolate import interp1d as interp1d |
| 6 | + |
| 7 | +from ._base_solver import _BaseSolver |
| 8 | +from ..heat_transfer import ( |
| 9 | + finite_line_source, |
| 10 | + finite_line_source_inclined_vectorized, |
| 11 | + finite_line_source_vectorized |
| 12 | + ) |
| 13 | + |
| 14 | + |
| 15 | +class Detailed(_BaseSolver): |
| 16 | + """ |
| 17 | + Detailed solver for the evaluation of the g-function. |
| 18 | +
|
| 19 | + This solver superimposes the finite line source (FLS) solution to |
| 20 | + estimate the g-function of a geothermal bore field. Each borehole is |
| 21 | + modeled as a series of finite line source segments, as proposed in |
| 22 | + [#Detailed-CimBer2014]_. |
| 23 | +
|
| 24 | + Parameters |
| 25 | + ---------- |
| 26 | + boreholes : list of Borehole objects |
| 27 | + List of boreholes included in the bore field. |
| 28 | + network : network object |
| 29 | + Model of the network. |
| 30 | + time : float or array |
| 31 | + Values of time (in seconds) for which the g-function is evaluated. |
| 32 | + boundary_condition : str |
| 33 | + Boundary condition for the evaluation of the g-function. Should be one |
| 34 | + of |
| 35 | +
|
| 36 | + - 'UHTR' : |
| 37 | + **Uniform heat transfer rate**. This is corresponds to boundary |
| 38 | + condition *BC-I* as defined by Cimmino and Bernier (2014) |
| 39 | + [#Detailed-CimBer2014]_. |
| 40 | + - 'UBWT' : |
| 41 | + **Uniform borehole wall temperature**. This is corresponds to |
| 42 | + boundary condition *BC-III* as defined by Cimmino and Bernier |
| 43 | + (2014) [#Detailed-CimBer2014]_. |
| 44 | + - 'MIFT' : |
| 45 | + **Mixed inlet fluid temperatures**. This boundary condition was |
| 46 | + introduced by Cimmino (2015) [#Detailed-Cimmin2015]_ for |
| 47 | + parallel-connected boreholes and extended to mixed |
| 48 | + configurations by Cimmino (2019) [#Detailed-Cimmin2019]_. |
| 49 | +
|
| 50 | + nSegments : int or list, optional |
| 51 | + Number of line segments used per borehole, or list of number of |
| 52 | + line segments used for each borehole. |
| 53 | + Default is 8. |
| 54 | + segment_ratios : array, list of arrays, or callable, optional |
| 55 | + Ratio of the borehole length represented by each segment. The |
| 56 | + sum of ratios must be equal to 1. The shape of the array is of |
| 57 | + (nSegments,) or list of (nSegments[i],). If segment_ratios==None, |
| 58 | + segments of equal lengths are considered. If a callable is provided, it |
| 59 | + must return an array of size (nSegments,) when provided with nSegments |
| 60 | + (of type int) as an argument, or an array of size (nSegments[i],) when |
| 61 | + provided with an element of nSegments (of type list). |
| 62 | + Default is :func:`utilities.segment_ratios`. |
| 63 | + m_flow_borehole : (nInlets,) array or (nMassFlow, nInlets,) array, optional |
| 64 | + Fluid mass flow rate into each circuit of the network. If a |
| 65 | + (nMassFlow, nInlets,) array is supplied, the |
| 66 | + (nMassFlow, nMassFlow,) variable mass flow rate g-functions |
| 67 | + will be evaluated using the method of Cimmino (2024) |
| 68 | + [#Detailed-Cimmin2024]_. Only required for the 'MIFT' boundary |
| 69 | + condition. Only one of 'm_flow_borehole' and 'm_flow_network' can be |
| 70 | + provided. |
| 71 | + Default is None. |
| 72 | + m_flow_network : float or (nMassFlow,) array, optional |
| 73 | + Fluid mass flow rate into the network of boreholes. If an array |
| 74 | + is supplied, the (nMassFlow, nMassFlow,) variable mass flow |
| 75 | + rate g-functions will be evaluated using the method of Cimmino |
| 76 | + (2024) [#Detailed-Cimmin2024]_. Only required for the 'MIFT' boundary |
| 77 | + condition. Only one of 'm_flow_borehole' and 'm_flow_network' can be |
| 78 | + provided. |
| 79 | + Default is None. |
| 80 | + cp_f : float, optional |
| 81 | + Fluid specific isobaric heat capacity (in J/kg.degC). Only required |
| 82 | + for the 'MIFT' boundary condition. |
| 83 | + Default is None. |
| 84 | + approximate_FLS : bool, optional |
| 85 | + Set to true to use the approximation of the FLS solution of Cimmino |
| 86 | + (2021) [#Detailed-Cimmin2021]_. This approximation does not require the |
| 87 | + numerical evaluation of any integral. |
| 88 | + Default is False. |
| 89 | + nFLS : int, optional |
| 90 | + Number of terms in the approximation of the FLS solution. This |
| 91 | + parameter is unused if `approximate_FLS` is set to False. |
| 92 | + Default is 10. Maximum is 25. |
| 93 | + mQuad : int, optional |
| 94 | + Number of Gauss-Legendre sample points for the integral over :math:`u` |
| 95 | + in the inclined FLS solution. |
| 96 | + Default is 11. |
| 97 | + linear_threshold : float, optional |
| 98 | + Threshold time (in seconds) under which the g-function is |
| 99 | + linearized. The g-function value is then interpolated between 0 |
| 100 | + and its value at the threshold. If linear_threshold==None, the |
| 101 | + g-function is linearized for times |
| 102 | + `t < r_b**2 / (25 * self.alpha)`. |
| 103 | + Default is None. |
| 104 | + disp : bool, optional |
| 105 | + Set to true to print progression messages. |
| 106 | + Default is False. |
| 107 | + profiles : bool, optional |
| 108 | + Set to true to keep in memory the temperatures and heat extraction |
| 109 | + rates. |
| 110 | + Default is False. |
| 111 | + kind : string, optional |
| 112 | + Interpolation method used for segment-to-segment thermal response |
| 113 | + factors. See documentation for scipy.interpolate.interp1d. |
| 114 | + Default is 'linear'. |
| 115 | + dtype : numpy dtype, optional |
| 116 | + numpy data type used for matrices and vectors. Should be one of |
| 117 | + numpy.single or numpy.double. |
| 118 | + Default is numpy.double. |
| 119 | +
|
| 120 | + References |
| 121 | + ---------- |
| 122 | + .. [#Detailed-CimBer2014] Cimmino, M., & Bernier, M. (2014). A |
| 123 | + semi-analytical method to generate g-functions for geothermal bore |
| 124 | + fields. International Journal of Heat and Mass Transfer, 70, 641-650. |
| 125 | + .. [#Detailed-Cimmin2015] Cimmino, M. (2015). The effects of borehole |
| 126 | + thermal resistances and fluid flow rate on the g-functions of geothermal |
| 127 | + bore fields. International Journal of Heat and Mass Transfer, 91, |
| 128 | + 1119-1127. |
| 129 | + .. [#Detailed-Cimmin2019] Cimmino, M. (2019). Semi-analytical method for |
| 130 | + g-function calculation of bore fields with series- and |
| 131 | + parallel-connected boreholes. Science and Technology for the Built |
| 132 | + Environment, 25 (8), 1007-1022. |
| 133 | + .. [#Detailed-Cimmin2021] Cimmino, M. (2021). An approximation of the |
| 134 | + finite line source solution to model thermal interactions between |
| 135 | + geothermal boreholes. International Communications in Heat and Mass |
| 136 | + Transfer, 127, 105496. |
| 137 | + .. [#Detailed-Cimmin2024] Cimmino, M. (2024). g-Functions for fields of |
| 138 | + series- and parallel-connected boreholes with variable fluid mass flow |
| 139 | + rate and reversible flow direction. Renewable Energy, 228, 120661. |
| 140 | +
|
| 141 | + """ |
| 142 | + def initialize(self, **kwargs): |
| 143 | + """ |
| 144 | + Split boreholes into segments. |
| 145 | +
|
| 146 | + Returns |
| 147 | + ------- |
| 148 | + nSources : int |
| 149 | + Number of finite line heat sources in the borefield used to |
| 150 | + initialize the matrix of segment-to-segment thermal response |
| 151 | + factors (of size: nSources x nSources). |
| 152 | +
|
| 153 | + """ |
| 154 | + # Split boreholes into segments |
| 155 | + self.boreSegments = self.borehole_segments() |
| 156 | + nSources = len(self.boreSegments) |
| 157 | + return nSources |
| 158 | + |
| 159 | + def thermal_response_factors(self, time, alpha, kind='linear'): |
| 160 | + """ |
| 161 | + Evaluate the segment-to-segment thermal response factors for all pairs |
| 162 | + of segments in the borefield at all time steps using the finite line |
| 163 | + source solution. |
| 164 | +
|
| 165 | + This method returns a scipy.interpolate.interp1d object of the matrix |
| 166 | + of thermal response factors, containing a copy of the matrix accessible |
| 167 | + by h_ij.y[:nSources,:nSources,:nt+1]. The first index along the |
| 168 | + third axis corresponds to time t=0. The interp1d object can be used to |
| 169 | + obtain thermal response factors at any intermediate time by |
| 170 | + h_ij(t)[:nSources,:nSources]. |
| 171 | +
|
| 172 | + Attributes |
| 173 | + ---------- |
| 174 | + time : float or array |
| 175 | + Values of time (in seconds) for which the g-function is evaluated. |
| 176 | + alpha : float |
| 177 | + Soil thermal diffusivity (in m2/s). |
| 178 | + kind : string, optional |
| 179 | + Interpolation method used for segment-to-segment thermal response |
| 180 | + factors. See documentation for scipy.interpolate.interp1d. |
| 181 | + Default is 'linear'. |
| 182 | +
|
| 183 | + Returns |
| 184 | + ------- |
| 185 | + h_ij : interp1d |
| 186 | + interp1d object (scipy.interpolate) of the matrix of |
| 187 | + segment-to-segment thermal response factors. |
| 188 | +
|
| 189 | + """ |
| 190 | + if self.disp: |
| 191 | + print('Calculating segment to segment response factors ...', |
| 192 | + end='') |
| 193 | + # Number of time values |
| 194 | + nt = len(np.atleast_1d(time)) |
| 195 | + # Initialize chrono |
| 196 | + tic = perf_counter() |
| 197 | + # Initialize segment-to-segment response factors |
| 198 | + h_ij = np.zeros((self.nSources, self.nSources, nt+1), dtype=self.dtype) |
| 199 | + nBoreholes = len(self.boreholes) |
| 200 | + segment_lengths = self.segment_lengths() |
| 201 | + |
| 202 | + # --------------------------------------------------------------------- |
| 203 | + # Segment-to-segment thermal response factors for same-borehole |
| 204 | + # thermal interactions |
| 205 | + # --------------------------------------------------------------------- |
| 206 | + h, i_segment, j_segment = \ |
| 207 | + self._thermal_response_factors_borehole_to_self(time, alpha) |
| 208 | + # Broadcast values to h_ij matrix |
| 209 | + h_ij[j_segment, i_segment, 1:] = h |
| 210 | + # --------------------------------------------------------------------- |
| 211 | + # Segment-to-segment thermal response factors for |
| 212 | + # borehole-to-borehole thermal interactions |
| 213 | + # --------------------------------------------------------------------- |
| 214 | + for i, (i0, i1) in enumerate(zip(self._i0Segments, self._i1Segments)): |
| 215 | + # Segments of the receiving borehole |
| 216 | + b2 = self.boreSegments[i0:i1] |
| 217 | + if i+1 < nBoreholes: |
| 218 | + # Segments of the emitting borehole |
| 219 | + b1 = self.boreSegments[i1:] |
| 220 | + h = finite_line_source( |
| 221 | + time, alpha, b1, b2, approximation=self.approximate_FLS, |
| 222 | + N=self.nFLS, M=self.mQuad) |
| 223 | + # Broadcast values to h_ij matrix |
| 224 | + h_ij[i0:i1, i1:, 1:] = h |
| 225 | + h_ij[i1:, i0:i1, 1:] = \ |
| 226 | + np.swapaxes(h, 0, 1) * np.divide.outer( |
| 227 | + segment_lengths[i0:i1], |
| 228 | + segment_lengths[i1:]).T[:,:,np.newaxis] |
| 229 | + |
| 230 | + # Return 2d array if time is a scalar |
| 231 | + if np.isscalar(time): |
| 232 | + h_ij = h_ij[:,:,1] |
| 233 | + |
| 234 | + # Interp1d object for thermal response factors |
| 235 | + h_ij = interp1d(np.hstack((0., time)), h_ij, |
| 236 | + kind=kind, copy=True, axis=2) |
| 237 | + toc = perf_counter() |
| 238 | + if self.disp: print(f' {toc - tic:.3f} sec') |
| 239 | + |
| 240 | + return h_ij |
| 241 | + |
| 242 | + def _thermal_response_factors_borehole_to_self(self, time, alpha): |
| 243 | + """ |
| 244 | + Evaluate the segment-to-segment thermal response factors for all pairs |
| 245 | + of segments between each borehole and itself. |
| 246 | +
|
| 247 | + Attributes |
| 248 | + ---------- |
| 249 | + time : float or array |
| 250 | + Values of time (in seconds) for which the g-function is evaluated. |
| 251 | + alpha : float |
| 252 | + Soil thermal diffusivity (in m2/s). |
| 253 | +
|
| 254 | + Returns |
| 255 | + ------- |
| 256 | + h : array |
| 257 | + Finite line source solution. |
| 258 | + i_segment : list |
| 259 | + Indices of the emitting segments in the bore field. |
| 260 | + j_segment : list |
| 261 | + Indices of the receiving segments in the bore field. |
| 262 | + """ |
| 263 | + # Indices of the thermal response factors into h_ij |
| 264 | + i_segment = np.concatenate( |
| 265 | + [np.repeat(np.arange(i0, i1), nSegments) |
| 266 | + for i0, i1, nSegments in zip( |
| 267 | + self._i0Segments, self._i1Segments, self.nBoreSegments) |
| 268 | + ]) |
| 269 | + j_segment = np.concatenate( |
| 270 | + [np.tile(np.arange(i0, i1), nSegments) |
| 271 | + for i0, i1, nSegments in zip( |
| 272 | + self._i0Segments, self._i1Segments, self.nBoreSegments) |
| 273 | + ]) |
| 274 | + # Unpack parameters |
| 275 | + x = np.array([b.x for b in self.boreSegments]) |
| 276 | + y = np.array([b.y for b in self.boreSegments]) |
| 277 | + H = np.array([b.H for b in self.boreSegments]) |
| 278 | + D = np.array([b.D for b in self.boreSegments]) |
| 279 | + r_b = np.array([b.r_b for b in self.boreSegments]) |
| 280 | + # Distances between boreholes |
| 281 | + dis = np.maximum( |
| 282 | + np.sqrt((x[i_segment] - x[j_segment])**2 + (y[i_segment] - y[j_segment])**2), |
| 283 | + r_b[i_segment]) |
| 284 | + # FLS solution |
| 285 | + if np.all([b.is_vertical() for b in self.boreholes]): |
| 286 | + h = finite_line_source_vectorized( |
| 287 | + time, alpha, |
| 288 | + dis, H[i_segment], D[i_segment], H[j_segment], D[j_segment], |
| 289 | + approximation=self.approximate_FLS, N=self.nFLS) |
| 290 | + else: |
| 291 | + tilt = np.array([b.tilt for b in self.boreSegments]) |
| 292 | + orientation = np.array([b.orientation for b in self.boreSegments]) |
| 293 | + h = finite_line_source_inclined_vectorized( |
| 294 | + time, alpha, |
| 295 | + r_b[i_segment], x[i_segment], y[i_segment], H[i_segment], |
| 296 | + D[i_segment], tilt[i_segment], orientation[i_segment], |
| 297 | + x[j_segment], y[j_segment], H[j_segment], D[j_segment], |
| 298 | + tilt[j_segment], orientation[j_segment], M=self.mQuad, |
| 299 | + approximation=self.approximate_FLS, N=self.nFLS) |
| 300 | + return h, i_segment, j_segment |
0 commit comments