Line data Source code
1 : // EnergyPlus, Copyright (c) 1996-2025, The Board of Trustees of the University of Illinois,
2 : // The Regents of the University of California, through Lawrence Berkeley National Laboratory
3 : // (subject to receipt of any required approvals from the U.S. Dept. of Energy), Oak Ridge
4 : // National Laboratory, managed by UT-Battelle, Alliance for Sustainable Energy, LLC, and other
5 : // contributors. All rights reserved.
6 : //
7 : // NOTICE: This Software was developed under funding from the U.S. Department of Energy and the
8 : // U.S. Government consequently retains certain rights. As such, the U.S. Government has been
9 : // granted for itself and others acting on its behalf a paid-up, nonexclusive, irrevocable,
10 : // worldwide license in the Software to reproduce, distribute copies to the public, prepare
11 : // derivative works, and perform publicly and display publicly, and to permit others to do so.
12 : //
13 : // Redistribution and use in source and binary forms, with or without modification, are permitted
14 : // provided that the following conditions are met:
15 : //
16 : // (1) Redistributions of source code must retain the above copyright notice, this list of
17 : // conditions and the following disclaimer.
18 : //
19 : // (2) Redistributions in binary form must reproduce the above copyright notice, this list of
20 : // conditions and the following disclaimer in the documentation and/or other materials
21 : // provided with the distribution.
22 : //
23 : // (3) Neither the name of the University of California, Lawrence Berkeley National Laboratory,
24 : // the University of Illinois, U.S. Dept. of Energy nor the names of its contributors may be
25 : // used to endorse or promote products derived from this software without specific prior
26 : // written permission.
27 : //
28 : // (4) Use of EnergyPlus(TM) Name. If Licensee (i) distributes the software in stand-alone form
29 : // without changes from the version obtained under this License, or (ii) Licensee makes a
30 : // reference solely to the software portion of its product, Licensee must refer to the
31 : // software as "EnergyPlus version X" software, where "X" is the version number Licensee
32 : // obtained under this License and may not use a different name for the software. Except as
33 : // specifically required in this Section (4), Licensee shall not use in a company name, a
34 : // product name, in advertising, publicity, or other promotional activities any name, trade
35 : // name, trademark, logo, or other designation of "EnergyPlus", "E+", "e+" or confusingly
36 : // similar designation, without the U.S. Department of Energy's prior written consent.
37 : //
38 : // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
39 : // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
40 : // AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
41 : // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
42 : // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
43 : // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
44 : // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
45 : // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
46 : // POSSIBILITY OF SUCH DAMAGE.
47 :
48 : // C++ Headers
49 : #include <cmath>
50 : #include <iterator>
51 : #include <vector>
52 :
53 : // ObjexxFCL Headers
54 : #include <ObjexxFCL/Array.functions.hh>
55 : #include <ObjexxFCL/Fmath.hh>
56 :
57 : // cpgfunction Headers
58 : #include <cpgfunction/boreholes.h>
59 : #include <cpgfunction/gfunction.h>
60 : #include <cpgfunction/segments.h>
61 :
62 : // JSON Headers
63 : #include <nlohmann/json.hpp>
64 :
65 : // EnergyPlus Headers
66 : #include <EnergyPlus/BranchNodeConnections.hh>
67 : #include <EnergyPlus/Data/EnergyPlusData.hh>
68 : #include <EnergyPlus/DataEnvironment.hh>
69 : #include <EnergyPlus/DataHVACGlobals.hh>
70 : #include <EnergyPlus/DataLoopNode.hh>
71 : #include <EnergyPlus/DataStringGlobals.hh>
72 : #include <EnergyPlus/DataSystemVariables.hh>
73 : #include <EnergyPlus/DisplayRoutines.hh>
74 : #include <EnergyPlus/FileSystem.hh>
75 : #include <EnergyPlus/FluidProperties.hh>
76 : #include <EnergyPlus/General.hh>
77 : #include <EnergyPlus/GroundHeatExchangers.hh>
78 : #include <EnergyPlus/GroundTemperatureModeling/GroundTemperatureModelManager.hh>
79 : #include <EnergyPlus/InputProcessing/InputProcessor.hh>
80 : #include <EnergyPlus/NodeInputManager.hh>
81 : #include <EnergyPlus/OutputProcessor.hh>
82 : #include <EnergyPlus/Plant/DataPlant.hh>
83 : #include <EnergyPlus/PlantUtilities.hh>
84 : #include <EnergyPlus/UtilityRoutines.hh>
85 :
86 : namespace EnergyPlus::GroundHeatExchangers {
87 : // MODULE INFORMATION:
88 : // AUTHOR Arun Murugappan, Dan Fisher
89 : // DATE WRITTEN September 2000
90 : // MODIFIED B. Griffith, Sept 2010,plant upgrades
91 : // Matt Mitchell, February 2015. Added Slinky GHX.
92 : // Moved models to object-oriented design.
93 :
94 : // PURPOSE OF THIS MODULE:
95 : // The module contains the data structures and routines to simulate the
96 : // operation of vertical closed-loop ground heat exchangers (GLHE) typically
97 : // used in low temperature geothermal heat pump systems.
98 :
99 : // METHODOLOGY EMPLOYED:
100 : // The borehole and fluid temperatures are calculated from the response to
101 : // the current heat transfer rate and the response to the history of past
102 : // applied heat pulses. The response to each pulse is calculated from a non-
103 : // dimensionalized response function, or G-function, that is specific to the
104 : // given borehole field arrangement, depth and spacing. The data defining
105 : // this function is read from input.
106 : // The heat pulse histories need to be recorded over an extended period (months).
107 : // To aid computational efficiency past pulses are continuously aggregated into
108 : // equivalent heat pulses of longer duration, as each pulse becomes less recent.
109 :
110 : // REFERENCES:
111 : // Eskilson, P. 'Thermal Analysis of Heat Extraction Boreholes' Ph.D. Thesis:
112 : // Dept. of Mathematical Physics, University of Lund, Sweden, June 1987.
113 : // Yavuzturk, C., J.D. Spitler. 1999. 'A Short Time Step Response Factor Model
114 : // for Vertical Ground Loop Heat Exchangers. ASHRAE Transactions. 105(2): 475-485.
115 : // Xiong, Z., D.E. Fisher, J.D. Spitler. 2015. 'Development and Validation of a Slinky
116 : // Ground Heat Exchanger.' Applied Energy. Vol 114, 57-69.
117 :
118 : // MODULE PARAMETER DEFINITIONS
119 : constexpr Real64 hrsPerMonth = 730.0; // Number of hours in month
120 : constexpr Real64 maxTSinHr = 60.0; // Max number of time step in a hour
121 : static constexpr std::array<std::string_view, 2> GFuncCalcMethodsStrs = {"UHFCALC", "UBHWTCALC"};
122 :
123 : //******************************************************************************
124 :
125 3 : GLHESlinky::GLHESlinky(EnergyPlusData &state, std::string const &objName, nlohmann::json const &j)
126 : {
127 : // Check for duplicates
128 1 : for (auto &existingObj : state.dataGroundHeatExchanger->singleBoreholesVector) {
129 0 : if (objName == existingObj->name) {
130 0 : ShowFatalError(state, format("Invalid input for {} object: Duplicate name found: {}", this->moduleName, existingObj->name));
131 : }
132 1 : }
133 :
134 1 : bool errorsFound = false;
135 :
136 1 : this->name = objName;
137 :
138 1 : std::string inletNodeName = Util::makeUPPER(j["inlet_node_name"].get<std::string>());
139 1 : std::string outletNodeName = Util::makeUPPER(j["outlet_node_name"].get<std::string>());
140 :
141 : // get inlet node num
142 1 : this->inletNodeNum = NodeInputManager::GetOnlySingleNode(state,
143 : inletNodeName,
144 : errorsFound,
145 : DataLoopNode::ConnectionObjectType::GroundHeatExchangerSlinky,
146 1 : this->name,
147 : DataLoopNode::NodeFluidType::Water,
148 : DataLoopNode::ConnectionType::Inlet,
149 : NodeInputManager::CompFluidStream::Primary,
150 : DataLoopNode::ObjectIsNotParent);
151 :
152 : // get outlet node num
153 1 : this->outletNodeNum = NodeInputManager::GetOnlySingleNode(state,
154 : outletNodeName,
155 : errorsFound,
156 : DataLoopNode::ConnectionObjectType::GroundHeatExchangerSlinky,
157 1 : this->name,
158 : DataLoopNode::NodeFluidType::Water,
159 : DataLoopNode::ConnectionType::Outlet,
160 : NodeInputManager::CompFluidStream::Primary,
161 : DataLoopNode::ObjectIsNotParent);
162 :
163 1 : this->available = true;
164 1 : this->on = true;
165 :
166 1 : BranchNodeConnections::TestCompSet(state, this->moduleName, this->name, inletNodeName, outletNodeName, "Condenser Water Nodes");
167 :
168 : // load data
169 1 : this->designFlow = j["design_flow_rate"].get<Real64>();
170 1 : PlantUtilities::RegisterPlantCompDesignFlow(state, this->inletNodeNum, this->designFlow);
171 :
172 1 : this->soil.k = j["soil_thermal_conductivity"].get<Real64>();
173 1 : this->soil.rho = j["soil_density"].get<Real64>();
174 1 : this->soil.cp = j["soil_specific_heat"].get<Real64>();
175 1 : this->soil.rhoCp = this->soil.rho * this->soil.cp;
176 1 : this->pipe.k = j["pipe_thermal_conductivity"].get<Real64>();
177 1 : this->pipe.rho = j["pipe_density"].get<Real64>();
178 1 : this->pipe.cp = j["pipe_specific_heat"].get<Real64>();
179 1 : this->pipe.outDia = j["pipe_outer_diameter"].get<Real64>();
180 1 : this->pipe.outRadius = this->pipe.outDia / 2.0;
181 1 : this->pipe.thickness = j["pipe_thickness"].get<Real64>();
182 :
183 1 : std::string const hxConfig = Util::makeUPPER(j["heat_exchanger_configuration"].get<std::string>());
184 1 : if (Util::SameString(hxConfig, "VERTICAL")) {
185 1 : this->verticalConfig = true;
186 0 : } else if (Util::SameString(hxConfig, "HORIZONTAL")) {
187 0 : this->verticalConfig = false;
188 : }
189 :
190 1 : this->coilDiameter = j["coil_diameter"].get<Real64>();
191 1 : this->coilPitch = j["coil_pitch"].get<Real64>();
192 1 : this->trenchDepth = j["trench_depth"].get<Real64>();
193 1 : this->trenchLength = j["trench_length"].get<Real64>();
194 1 : this->numTrenches = j["number_of_trenches"].get<int>();
195 1 : this->trenchSpacing = j["horizontal_spacing_between_pipes"].get<Real64>();
196 1 : this->maxSimYears = j["maximum_length_of_simulation"].get<Real64>();
197 :
198 : // Need to add a response factor object for the slinky model
199 1 : std::shared_ptr<GLHEResponseFactors> thisRF(new GLHEResponseFactors);
200 1 : thisRF->name = "Response Factor Object Auto Generated No: " + fmt::to_string(state.dataGroundHeatExchanger->numAutoGeneratedResponseFactors + 1);
201 1 : this->myRespFactors = thisRF;
202 1 : state.dataGroundHeatExchanger->responseFactorsVector.push_back(thisRF);
203 :
204 : // Number of coils
205 1 : this->numCoils = static_cast<int>(this->trenchLength / this->coilPitch);
206 :
207 : // Total tube length
208 1 : this->totalTubeLength = Constant::Pi * this->coilDiameter * this->trenchLength * this->numTrenches / this->coilPitch;
209 :
210 : // Get g function data
211 1 : this->SubAGG = 15;
212 1 : this->AGG = 192;
213 :
214 : // Average coil depth
215 1 : if (this->verticalConfig) {
216 : // Vertical configuration
217 1 : if (this->trenchDepth - this->coilDiameter < 0.0) {
218 : // Error: part of the coil is above ground
219 0 : ShowSevereError(state, format("{}=\"{}\", invalid value in field.", this->moduleName, this->name));
220 0 : ShowContinueError(state, format("...{}=[{:.3R}].", "Trench Depth", this->trenchDepth));
221 0 : ShowContinueError(state, format("...{}=[{:.3R}].", "Coil Depth", this->coilDepth));
222 0 : ShowContinueError(state, "...Part of coil will be above ground.");
223 0 : errorsFound = true;
224 :
225 : } else {
226 : // Entire coil is below ground
227 1 : this->coilDepth = this->trenchDepth - (this->coilDiameter / 2.0);
228 : }
229 :
230 : } else {
231 : // Horizontal configuration
232 0 : this->coilDepth = this->trenchDepth;
233 : }
234 :
235 : // Thermal diffusivity of the ground
236 1 : this->soil.diffusivity = this->soil.k / this->soil.rhoCp;
237 :
238 1 : state.dataGroundHeatExchanger->prevTimeSteps.allocate(static_cast<int>((this->SubAGG + 1) * maxTSinHr + 1));
239 1 : state.dataGroundHeatExchanger->prevTimeSteps = 0.0;
240 :
241 1 : if (this->pipe.thickness >= this->pipe.outDia / 2.0) {
242 0 : ShowSevereError(state, format("{}=\"{}\", invalid value in field.", this->moduleName, this->name));
243 0 : ShowContinueError(state, format("...{}=[{:.3R}].", "Pipe Thickness", this->pipe.thickness));
244 0 : ShowContinueError(state, format("...{}=[{:.3R}].", "Pipe Outer Diameter", this->pipe.outDia));
245 0 : ShowContinueError(state, "...Radius will be <=0.");
246 0 : errorsFound = true;
247 : }
248 :
249 : // Initialize ground temperature model and get pointer reference
250 : GroundTemp::ModelType gtmType = static_cast<GroundTemp::ModelType>(
251 1 : getEnumValue(GroundTemp::modelTypeNamesUC, Util::makeUPPER(j["undisturbed_ground_temperature_model_type"].get<std::string>())));
252 1 : assert(gtmType != GroundTemp::ModelType::Invalid);
253 :
254 1 : std::string const gtmName = Util::makeUPPER(j["undisturbed_ground_temperature_model_name"].get<std::string>());
255 1 : this->groundTempModel = GroundTemp::GetGroundTempModelAndInit(state, gtmType, gtmName);
256 :
257 : // Check for Errors
258 1 : if (errorsFound) {
259 0 : ShowFatalError(state, format("Errors found in processing input for {}", this->moduleName));
260 : }
261 1 : }
262 :
263 : //******************************************************************************
264 :
265 66 : GLHEVert::GLHEVert(EnergyPlusData &state, std::string const &objName, nlohmann::json const &j)
266 : {
267 : // Check for duplicates
268 36 : for (auto &existingObj : state.dataGroundHeatExchanger->singleBoreholesVector) {
269 14 : if (objName == existingObj->name) {
270 0 : ShowFatalError(state, format("Invalid input for {} object: Duplicate name found: {}", this->moduleName, existingObj->name));
271 : }
272 22 : }
273 :
274 22 : bool errorsFound = false;
275 :
276 22 : this->name = objName;
277 :
278 : // get inlet node num
279 22 : std::string const inletNodeName = Util::makeUPPER(j["inlet_node_name"].get<std::string>());
280 :
281 22 : this->inletNodeNum = NodeInputManager::GetOnlySingleNode(state,
282 : inletNodeName,
283 : errorsFound,
284 : DataLoopNode::ConnectionObjectType::GroundHeatExchangerSystem,
285 : objName,
286 : DataLoopNode::NodeFluidType::Water,
287 : DataLoopNode::ConnectionType::Inlet,
288 : NodeInputManager::CompFluidStream::Primary,
289 : DataLoopNode::ObjectIsNotParent);
290 :
291 : // get outlet node num
292 22 : std::string const outletNodeName = Util::makeUPPER(j["outlet_node_name"].get<std::string>());
293 :
294 22 : this->outletNodeNum = NodeInputManager::GetOnlySingleNode(state,
295 : outletNodeName,
296 : errorsFound,
297 : DataLoopNode::ConnectionObjectType::GroundHeatExchangerSystem,
298 : objName,
299 : DataLoopNode::NodeFluidType::Water,
300 : DataLoopNode::ConnectionType::Outlet,
301 : NodeInputManager::CompFluidStream::Primary,
302 : DataLoopNode::ObjectIsNotParent);
303 22 : this->available = true;
304 22 : this->on = true;
305 :
306 22 : BranchNodeConnections::TestCompSet(state, this->moduleName, objName, inletNodeName, outletNodeName, "Condenser Water Nodes");
307 :
308 22 : this->designFlow = j["design_flow_rate"].get<Real64>();
309 22 : PlantUtilities::RegisterPlantCompDesignFlow(state, this->inletNodeNum, this->designFlow);
310 :
311 22 : this->soil.k = j["ground_thermal_conductivity"].get<Real64>();
312 22 : this->soil.rhoCp = j["ground_thermal_heat_capacity"].get<Real64>();
313 :
314 66 : if (j.find("ghe_vertical_responsefactors_object_name") != j.end()) {
315 : // Response factors come from IDF object
316 4 : this->myRespFactors = GetResponseFactor(state, Util::makeUPPER(j["ghe_vertical_responsefactors_object_name"].get<std::string>()));
317 4 : this->gFunctionsExist = true;
318 : }
319 :
320 : // no g-functions in the input file, so they need to be calculated
321 22 : if (!this->gFunctionsExist) {
322 :
323 : // g-function calculation method
324 54 : if (j.find("g_function_calculation_method") != j.end()) {
325 18 : std::string gFunctionMethodStr = Util::makeUPPER(j["g_function_calculation_method"].get<std::string>());
326 18 : if (gFunctionMethodStr == "UHFCALC") {
327 17 : this->gFuncCalcMethod = GFuncCalcMethod::UniformHeatFlux;
328 1 : } else if (gFunctionMethodStr == "UBHWTCALC") {
329 1 : this->gFuncCalcMethod = GFuncCalcMethod::UniformBoreholeWallTemp;
330 : } else {
331 0 : errorsFound = true;
332 0 : ShowSevereError(state, fmt::format("g-Function Calculation Method: \"{}\" is invalid", gFunctionMethodStr));
333 : }
334 18 : }
335 :
336 : // get borehole data from array or individual borehole instance objects
337 54 : if (j.find("ghe_vertical_array_object_name") != j.end()) {
338 : // Response factors come from array object
339 30 : this->myRespFactors = BuildAndGetResponseFactorObjectFromArray(
340 45 : state, GetVertArray(state, Util::makeUPPER(j["ghe_vertical_array_object_name"].get<std::string>())));
341 : } else {
342 9 : if (j.find("vertical_well_locations") == j.end()) {
343 : // No ResponseFactors, GHEArray, or SingleBH object are referenced
344 0 : ShowSevereError(state, "No GHE:ResponseFactors, GHE:Vertical:Array, or GHE:Vertical:Single objects found");
345 0 : ShowFatalError(state, format("Check references to these objects for GHE:System object: {}", this->name));
346 : }
347 :
348 3 : auto const &vars = j.at("vertical_well_locations");
349 :
350 : // Calculate response factors from individual boreholes
351 3 : std::vector<std::shared_ptr<GLHEVertSingle>> tempVectOfBHObjects;
352 :
353 15 : for (auto const &var : vars) {
354 24 : if (!var.at("ghe_vertical_single_object_name").empty()) {
355 : std::shared_ptr<GLHEVertSingle> tempBHptr =
356 12 : GetSingleBH(state, Util::makeUPPER(var.at("ghe_vertical_single_object_name").get<std::string>()));
357 12 : tempVectOfBHObjects.push_back(tempBHptr);
358 12 : } else {
359 0 : break;
360 : }
361 3 : }
362 :
363 3 : this->myRespFactors = BuildAndGetResponseFactorsObjectFromSingleBHs(state, tempVectOfBHObjects);
364 :
365 3 : if (!this->myRespFactors) {
366 0 : errorsFound = true;
367 0 : ShowSevereError(state, "GroundHeatExchanger:Vertical:Single objects not found.");
368 : }
369 3 : }
370 : }
371 :
372 22 : this->bhDiameter = this->myRespFactors->props->bhDiameter;
373 22 : this->bhRadius = this->bhDiameter / 2.0;
374 22 : this->bhLength = this->myRespFactors->props->bhLength;
375 22 : this->bhUTubeDist = this->myRespFactors->props->bhUTubeDist;
376 :
377 : // pull pipe and grout data up from response factor struct for simplicity
378 22 : this->pipe.outDia = this->myRespFactors->props->pipe.outDia;
379 22 : this->pipe.innerDia = this->myRespFactors->props->pipe.innerDia;
380 22 : this->pipe.outRadius = this->pipe.outDia / 2;
381 22 : this->pipe.innerRadius = this->pipe.innerDia / 2;
382 22 : this->pipe.thickness = this->myRespFactors->props->pipe.thickness;
383 22 : this->pipe.k = this->myRespFactors->props->pipe.k;
384 22 : this->pipe.rhoCp = this->myRespFactors->props->pipe.rhoCp;
385 :
386 22 : this->grout.k = this->myRespFactors->props->grout.k;
387 22 : this->grout.rhoCp = this->myRespFactors->props->grout.rhoCp;
388 :
389 22 : this->myRespFactors->gRefRatio = this->bhRadius / this->bhLength;
390 :
391 : // Number of simulation years from RunPeriod
392 22 : this->myRespFactors->maxSimYears = state.dataEnvrn->MaxNumberSimYears;
393 :
394 : // total tube length
395 22 : this->totalTubeLength = this->myRespFactors->numBoreholes * this->myRespFactors->props->bhLength;
396 :
397 : // ground thermal diffusivity
398 22 : this->soil.diffusivity = this->soil.k / this->soil.rhoCp;
399 :
400 : // multipole method constants
401 22 : this->theta_1 = this->bhUTubeDist / (2 * this->bhRadius);
402 22 : this->theta_2 = this->bhRadius / this->pipe.outRadius;
403 22 : this->theta_3 = 1 / (2 * this->theta_1 * this->theta_2);
404 22 : this->sigma = (this->grout.k - this->soil.k) / (this->grout.k + this->soil.k);
405 :
406 22 : this->SubAGG = 15;
407 22 : this->AGG = 192;
408 :
409 : // Allocation of all the dynamic arrays
410 22 : this->QnMonthlyAgg.dimension(static_cast<int>(this->myRespFactors->maxSimYears * 12), 0.0);
411 22 : this->QnHr.dimension(730 + this->AGG + this->SubAGG, 0.0);
412 22 : this->QnSubHr.dimension(static_cast<int>((this->SubAGG + 1) * maxTSinHr + 1), 0.0);
413 22 : this->LastHourN.dimension(this->SubAGG + 1, 0);
414 :
415 22 : state.dataGroundHeatExchanger->prevTimeSteps.allocate(static_cast<int>((this->SubAGG + 1) * maxTSinHr + 1));
416 22 : state.dataGroundHeatExchanger->prevTimeSteps = 0.0;
417 :
418 : GroundTemp::ModelType modelType = static_cast<GroundTemp::ModelType>(
419 22 : getEnumValue(GroundTemp::modelTypeNamesUC, Util::makeUPPER(j["undisturbed_ground_temperature_model_type"].get<std::string>())));
420 22 : assert(modelType != GroundTemp::ModelType::Invalid);
421 :
422 : // Initialize ground temperature model and get pointer reference
423 22 : this->groundTempModel =
424 22 : GroundTemp::GetGroundTempModelAndInit(state, modelType, Util::makeUPPER(j["undisturbed_ground_temperature_model_name"].get<std::string>()));
425 :
426 : // Check for Errors
427 22 : if (errorsFound) {
428 0 : ShowFatalError(state, format("Errors found in processing input for {}", this->moduleName));
429 : }
430 22 : }
431 :
432 : //******************************************************************************
433 :
434 42 : GLHEVertSingle::GLHEVertSingle(EnergyPlusData &state, std::string const &objName, nlohmann::json const &j)
435 : {
436 : // Check for duplicates
437 36 : for (auto &existingObj : state.dataGroundHeatExchanger->singleBoreholesVector) {
438 22 : if (objName == existingObj->name) {
439 0 : ShowFatalError(state, format("Invalid input for {} object: Duplicate name found: {}", this->moduleName, existingObj->name));
440 : }
441 14 : }
442 :
443 14 : this->name = objName;
444 14 : this->props = GetVertProps(state, Util::makeUPPER(j["ghe_vertical_properties_object_name"].get<std::string>()));
445 14 : this->xLoc = j["x_location"].get<Real64>();
446 14 : this->yLoc = j["y_location"].get<Real64>();
447 14 : this->dl_i = 0.0;
448 14 : this->dl_ii = 0.0;
449 14 : this->dl_j = 0.0;
450 14 : }
451 :
452 : //******************************************************************************
453 :
454 48 : GLHEVertArray::GLHEVertArray(EnergyPlusData &state, std::string const &objName, nlohmann::json const &j)
455 : {
456 : // Check for duplicates
457 16 : for (auto &existingObj : state.dataGroundHeatExchanger->vertArraysVector) {
458 0 : if (objName == existingObj->name) {
459 0 : ShowFatalError(state, format("Invalid input for {} object: Duplicate name found: {}", this->moduleName, existingObj->name));
460 : }
461 16 : }
462 :
463 16 : this->name = objName;
464 16 : this->props = GetVertProps(state, Util::makeUPPER(j["ghe_vertical_properties_object_name"].get<std::string>()));
465 16 : this->numBHinXDirection = j["number_of_boreholes_in_x_direction"].get<int>();
466 16 : this->numBHinYDirection = j["number_of_boreholes_in_y_direction"].get<int>();
467 16 : this->bhSpacing = j["borehole_spacing"].get<Real64>();
468 16 : }
469 :
470 : //******************************************************************************
471 :
472 18 : GLHEResponseFactors::GLHEResponseFactors(EnergyPlusData &state, std::string const &objName, nlohmann::json const &j)
473 : {
474 :
475 : // Check for duplicates
476 12 : for (auto &existingObj : state.dataGroundHeatExchanger->vertPropsVector) {
477 6 : if (objName == existingObj->name) {
478 0 : ShowFatalError(state, format("Invalid input for {} object: Duplicate name found: {}", this->moduleName, existingObj->name));
479 : }
480 6 : }
481 :
482 6 : this->name = objName;
483 6 : this->props = GetVertProps(state, Util::makeUPPER(j["ghe_vertical_properties_object_name"].get<std::string>()));
484 6 : this->numBoreholes = j["number_of_boreholes"].get<int>();
485 6 : this->gRefRatio = j["g_function_reference_ratio"].get<Real64>();
486 6 : this->maxSimYears = state.dataEnvrn->MaxNumberSimYears;
487 :
488 6 : auto const &vars = j.at("g_functions");
489 6 : std::vector<Real64> tmpLntts;
490 6 : std::vector<Real64> tmpGvals;
491 228 : for (auto const &var : vars) {
492 444 : tmpLntts.push_back(var.at("g_function_ln_t_ts_value").get<Real64>());
493 444 : tmpGvals.push_back(var.at("g_function_g_value").get<Real64>());
494 6 : }
495 :
496 6 : this->numGFuncPairs = static_cast<int>(tmpLntts.size());
497 :
498 228 : for (int i = 1; i <= (int)tmpLntts.size(); ++i) {
499 222 : this->LNTTS.push_back(tmpLntts[i - 1]);
500 222 : this->GFNC.push_back(tmpGvals[i - 1]);
501 : }
502 6 : }
503 :
504 : //******************************************************************************
505 :
506 75 : GLHEVertProps::GLHEVertProps(EnergyPlusData &state, std::string const &objName, nlohmann::json const &j)
507 : {
508 :
509 : // Check for duplicates
510 25 : for (auto &existingObj : state.dataGroundHeatExchanger->vertPropsVector) {
511 0 : if (objName == existingObj->name) {
512 0 : ShowFatalError(state, format("Invalid input for {} object: Duplicate name found: {}", this->moduleName, existingObj->name));
513 : }
514 25 : }
515 :
516 : // Load data from JSON
517 25 : this->name = objName;
518 25 : this->bhTopDepth = j["depth_of_top_of_borehole"].get<Real64>();
519 25 : this->bhLength = j["borehole_length"].get<Real64>();
520 25 : this->bhDiameter = j["borehole_diameter"].get<Real64>();
521 25 : this->grout.k = j["grout_thermal_conductivity"].get<Real64>();
522 25 : this->grout.rhoCp = j["grout_thermal_heat_capacity"].get<Real64>();
523 25 : this->pipe.k = j["pipe_thermal_conductivity"].get<Real64>();
524 25 : this->pipe.rhoCp = j["pipe_thermal_heat_capacity"].get<Real64>();
525 25 : this->pipe.outDia = j["pipe_outer_diameter"].get<Real64>();
526 25 : this->pipe.thickness = j["pipe_thickness"].get<Real64>();
527 25 : this->bhUTubeDist = j["u_tube_distance"].get<Real64>();
528 :
529 : // Verify u-tube spacing is valid
530 25 : if (this->bhUTubeDist < this->pipe.outDia) {
531 0 : ShowWarningError(state, "Borehole shank spacing is less than the pipe diameter. U-tube spacing is reference from the u-tube pipe center.");
532 0 : ShowWarningError(state, "Shank spacing is set to the outer pipe diameter.");
533 0 : this->bhUTubeDist = this->pipe.outDia;
534 : }
535 :
536 : // Set remaining data derived from previous inputs
537 25 : this->pipe.innerDia = this->pipe.outDia - 2 * this->pipe.thickness;
538 25 : this->pipe.outRadius = this->pipe.outDia / 2;
539 25 : this->pipe.innerRadius = this->pipe.innerDia / 2;
540 25 : }
541 :
542 : //******************************************************************************
543 :
544 98 : std::shared_ptr<GLHEVertProps> GetVertProps(EnergyPlusData &state, std::string const &objectName)
545 : {
546 : // Check if this instance of this model has already been retrieved
547 196 : auto thisObj = std::find_if(state.dataGroundHeatExchanger->vertPropsVector.begin(),
548 196 : state.dataGroundHeatExchanger->vertPropsVector.end(),
549 294 : [&objectName](const std::shared_ptr<GLHEVertProps> &myObj) { return myObj->name == objectName; });
550 98 : if (thisObj != state.dataGroundHeatExchanger->vertPropsVector.end()) {
551 97 : return *thisObj;
552 : }
553 :
554 2 : ShowSevereError(state, fmt::format("Object=GroundHeatExchanger:Vertical:Properties, Name={} - not found.", objectName));
555 3 : ShowFatalError(state, "Preceding errors cause program termination");
556 :
557 : // needed to silence compiler, but should never get here
558 0 : return nullptr;
559 98 : }
560 :
561 : //******************************************************************************
562 :
563 14 : std::shared_ptr<GLHEVertSingle> GetSingleBH(EnergyPlusData &state, std::string const &objectName)
564 : {
565 : // Check if this instance of this model has already been retrieved
566 28 : auto thisObj = std::find_if(state.dataGroundHeatExchanger->singleBoreholesVector.begin(),
567 28 : state.dataGroundHeatExchanger->singleBoreholesVector.end(),
568 64 : [&objectName](const std::shared_ptr<GLHEVertSingle> &myObj) { return myObj->name == objectName; });
569 14 : if (thisObj != state.dataGroundHeatExchanger->singleBoreholesVector.end()) {
570 13 : return *thisObj;
571 : }
572 :
573 2 : ShowSevereError(state, fmt::format("Object=GroundHeatExchanger:Vertical:Single, Name={} - not found.", objectName));
574 3 : ShowFatalError(state, "Preceding errors cause program termination");
575 :
576 : // needed to silence compiler, but should never get here
577 0 : return nullptr;
578 14 : }
579 :
580 : //******************************************************************************
581 :
582 17 : std::shared_ptr<GLHEVertArray> GetVertArray(EnergyPlusData &state, std::string const &objectName)
583 : {
584 : // Check if this instance of this model has already been retrieved
585 34 : auto thisObj = std::find_if(state.dataGroundHeatExchanger->vertArraysVector.begin(),
586 34 : state.dataGroundHeatExchanger->vertArraysVector.end(),
587 51 : [&objectName](const std::shared_ptr<GLHEVertArray> &myObj) { return myObj->name == objectName; });
588 17 : if (thisObj != state.dataGroundHeatExchanger->vertArraysVector.end()) {
589 16 : return *thisObj;
590 : }
591 :
592 2 : ShowSevereError(state, fmt::format("Object=GroundHeatExchanger:Vertical:Array, Name={} - not found.", objectName));
593 3 : ShowFatalError(state, "Preceding errors cause program termination");
594 :
595 : // needed to silence compiler, but should never get here
596 0 : return nullptr;
597 17 : }
598 :
599 : //******************************************************************************
600 :
601 8 : std::shared_ptr<GLHEResponseFactors> GetResponseFactor(EnergyPlusData &state, std::string const &objectName)
602 : {
603 : // Check if this instance of this model has already been retrieved
604 16 : auto thisObj = std::find_if(state.dataGroundHeatExchanger->responseFactorsVector.begin(),
605 16 : state.dataGroundHeatExchanger->responseFactorsVector.end(),
606 25 : [&objectName](const std::shared_ptr<GLHEResponseFactors> &myObj) { return myObj->name == objectName; });
607 8 : if (thisObj != state.dataGroundHeatExchanger->responseFactorsVector.end()) {
608 7 : return *thisObj;
609 : }
610 :
611 2 : ShowSevereError(state, fmt::format("Object=GroundHeatExchanger:ResponseFactors, Name={} - not found.", objectName));
612 3 : ShowFatalError(state, "Preceding errors cause program termination");
613 :
614 : // needed to silence compiler, but should never get here
615 0 : return nullptr;
616 8 : }
617 :
618 : //******************************************************************************
619 :
620 15 : std::shared_ptr<GLHEResponseFactors> BuildAndGetResponseFactorObjectFromArray(EnergyPlusData &state,
621 : std::shared_ptr<GLHEVertArray> const &arrayObjectPtr)
622 : {
623 : // Make new response factor object and store it for later use
624 15 : std::shared_ptr<GLHEResponseFactors> thisRF(new GLHEResponseFactors);
625 15 : thisRF->name = arrayObjectPtr->name;
626 15 : thisRF->props = arrayObjectPtr->props;
627 :
628 : // Build out new instances of the vertical BH objects which correspond to this object
629 15 : int xLoc = 0;
630 15 : int bhCounter = 0;
631 45 : for (int xBH = 1; xBH <= arrayObjectPtr->numBHinXDirection; ++xBH) {
632 30 : int yLoc = 0;
633 90 : for (int yBH = 1; yBH <= arrayObjectPtr->numBHinYDirection; ++yBH) {
634 60 : bhCounter += 1;
635 60 : std::shared_ptr<GLHEVertSingle> thisBH(new GLHEVertSingle);
636 60 : thisBH->name = format("{} BH {} loc: ({}, {})", thisRF->name, bhCounter, xLoc, yLoc);
637 60 : thisBH->props = GetVertProps(state, arrayObjectPtr->props->name);
638 60 : thisBH->xLoc = xLoc;
639 60 : thisBH->yLoc = yLoc;
640 60 : thisRF->myBorholes.push_back(thisBH);
641 60 : state.dataGroundHeatExchanger->singleBoreholesVector.push_back(thisBH);
642 60 : yLoc += arrayObjectPtr->bhSpacing;
643 60 : thisRF->numBoreholes += 1;
644 60 : }
645 30 : xLoc += arrayObjectPtr->bhSpacing;
646 : }
647 :
648 15 : SetupBHPointsForResponseFactorsObject(thisRF);
649 15 : state.dataGroundHeatExchanger->responseFactorsVector.push_back(thisRF);
650 30 : return thisRF;
651 0 : }
652 :
653 : //******************************************************************************
654 :
655 : std::shared_ptr<GLHEResponseFactors>
656 3 : BuildAndGetResponseFactorsObjectFromSingleBHs(EnergyPlusData &state, std::vector<std::shared_ptr<GLHEVertSingle>> const &singleBHsForRFVect)
657 : {
658 : // Make new response factor object and store it for later use
659 3 : std::shared_ptr<GLHEResponseFactors> thisRF(new GLHEResponseFactors);
660 3 : thisRF->name = format("Response Factor Object Auto Generated No: {}", state.dataGroundHeatExchanger->numAutoGeneratedResponseFactors + 1);
661 :
662 : // Make new props object which has the mean values of the other props objects referenced by the individual BH objects
663 3 : std::shared_ptr<GLHEVertProps> thisProps(new GLHEVertProps);
664 3 : thisProps->name = format("Response Factor Auto Generated Mean Props No: {}", state.dataGroundHeatExchanger->numAutoGeneratedResponseFactors + 1);
665 15 : for (auto &thisBH : singleBHsForRFVect) {
666 12 : thisProps->bhDiameter += thisBH->props->bhDiameter;
667 12 : thisProps->bhLength += thisBH->props->bhLength;
668 12 : thisProps->bhTopDepth += thisBH->props->bhTopDepth;
669 12 : thisProps->bhUTubeDist += thisBH->props->bhUTubeDist;
670 :
671 12 : thisProps->grout.cp += thisBH->props->grout.cp;
672 12 : thisProps->grout.diffusivity += thisBH->props->grout.diffusivity;
673 12 : thisProps->grout.k += thisBH->props->grout.k;
674 12 : thisProps->grout.rho += thisBH->props->grout.rho;
675 12 : thisProps->grout.rhoCp += thisBH->props->grout.rhoCp;
676 :
677 12 : thisProps->pipe.cp += thisBH->props->pipe.cp;
678 12 : thisProps->pipe.diffusivity += thisBH->props->pipe.diffusivity;
679 12 : thisProps->pipe.k += thisBH->props->pipe.k;
680 12 : thisProps->pipe.rho += thisBH->props->pipe.rho;
681 12 : thisProps->pipe.rhoCp += thisBH->props->pipe.rhoCp;
682 :
683 12 : thisProps->pipe.outDia += thisBH->props->pipe.outDia;
684 12 : thisProps->pipe.thickness += thisBH->props->pipe.thickness;
685 :
686 12 : thisProps->pipe.innerDia += (thisBH->props->pipe.outDia - 2 * thisBH->props->pipe.thickness);
687 :
688 12 : thisRF->myBorholes.push_back(thisBH);
689 3 : }
690 :
691 3 : int numBH = (int)singleBHsForRFVect.size();
692 :
693 : // normalize by number of bh
694 3 : thisProps->bhDiameter /= numBH;
695 3 : thisProps->bhLength /= numBH;
696 3 : thisProps->bhTopDepth /= numBH;
697 3 : thisProps->bhUTubeDist /= numBH;
698 :
699 3 : thisProps->grout.cp /= numBH;
700 3 : thisProps->grout.diffusivity /= numBH;
701 3 : thisProps->grout.k /= numBH;
702 3 : thisProps->grout.rho /= numBH;
703 3 : thisProps->grout.rhoCp /= numBH;
704 :
705 3 : thisProps->pipe.cp /= numBH;
706 3 : thisProps->pipe.diffusivity /= numBH;
707 3 : thisProps->pipe.k /= numBH;
708 3 : thisProps->pipe.rho /= numBH;
709 3 : thisProps->pipe.rhoCp /= numBH;
710 :
711 3 : thisProps->pipe.outDia /= numBH;
712 3 : thisProps->pipe.thickness /= numBH;
713 :
714 3 : thisProps->pipe.innerDia /= numBH;
715 :
716 3 : thisRF->props = thisProps;
717 3 : thisRF->numBoreholes = thisRF->myBorholes.size();
718 3 : state.dataGroundHeatExchanger->vertPropsVector.push_back(thisProps);
719 :
720 3 : SetupBHPointsForResponseFactorsObject(thisRF);
721 :
722 3 : state.dataGroundHeatExchanger->responseFactorsVector.push_back(thisRF);
723 :
724 3 : state.dataGroundHeatExchanger->numAutoGeneratedResponseFactors += 1;
725 :
726 6 : return thisRF;
727 3 : }
728 :
729 : //******************************************************************************
730 :
731 18 : void SetupBHPointsForResponseFactorsObject(const std::shared_ptr<GLHEResponseFactors> &thisRF)
732 : {
733 90 : for (auto const &thisBH : thisRF->myBorholes) {
734 :
735 : // Using Simpson's rule the number of points (n+1) must be odd, therefore an even number of panels is required
736 : // Starting from i = 0 to i <= NumPanels produces an odd number of points
737 72 : constexpr int numPanels_i = 50;
738 72 : constexpr int numPanels_ii = 50;
739 72 : constexpr int numPanels_j = 560;
740 :
741 72 : thisBH->dl_i = thisBH->props->bhLength / numPanels_i;
742 3744 : for (int i = 0; i <= numPanels_i; ++i) {
743 3672 : MyCartesian newPoint;
744 3672 : newPoint.x = thisBH->xLoc;
745 3672 : newPoint.y = thisBH->yLoc;
746 3672 : newPoint.z = thisBH->props->bhTopDepth + (i * thisBH->dl_i);
747 3672 : thisBH->pointLocations_i.push_back(newPoint);
748 : }
749 :
750 72 : thisBH->dl_ii = thisBH->props->bhLength / numPanels_ii;
751 3744 : for (int i = 0; i <= numPanels_ii; ++i) {
752 3672 : MyCartesian newPoint;
753 : // For case when bh is being compared to itself, shift points by 1 radius in the horizontal plane
754 3672 : newPoint.x = thisBH->xLoc + (thisBH->props->bhDiameter / 2.0) / sqrt(2.0);
755 3672 : newPoint.y = thisBH->yLoc + (thisBH->props->bhDiameter / 2.0) / (-sqrt(2.0));
756 3672 : newPoint.z = thisBH->props->bhTopDepth + (i * thisBH->dl_ii);
757 3672 : thisBH->pointLocations_ii.push_back(newPoint);
758 : }
759 :
760 72 : thisBH->dl_j = thisBH->props->bhLength / numPanels_j;
761 40464 : for (int i = 0; i <= numPanels_j; ++i) {
762 40392 : MyCartesian newPoint;
763 40392 : newPoint.x = thisBH->xLoc;
764 40392 : newPoint.y = thisBH->yLoc;
765 40392 : newPoint.z = thisBH->props->bhTopDepth + (i * thisBH->dl_j);
766 40392 : thisBH->pointLocations_j.push_back(newPoint);
767 : }
768 18 : }
769 18 : }
770 :
771 : //******************************************************************************
772 :
773 15 : void GLHEBase::onInitLoopEquip(EnergyPlusData &state, [[maybe_unused]] const PlantLocation &calledFromLocation)
774 : {
775 15 : this->initGLHESimVars(state);
776 15 : }
777 :
778 : //******************************************************************************
779 :
780 14248 : void GLHEBase::simulate(EnergyPlusData &state,
781 : [[maybe_unused]] const PlantLocation &calledFromLocation,
782 : [[maybe_unused]] bool const FirstHVACIteration,
783 : [[maybe_unused]] Real64 &CurLoad,
784 : [[maybe_unused]] bool const RunFlag)
785 : {
786 :
787 14248 : if (this->needToSetupOutputVars) {
788 3 : this->setupOutput(state);
789 3 : this->needToSetupOutputVars = false;
790 : }
791 :
792 14248 : if (state.dataGlobal->KickOffSimulation) {
793 112 : this->initGLHESimVars(state);
794 : } else {
795 14136 : this->initGLHESimVars(state);
796 14136 : this->calcGroundHeatExchanger(state);
797 14136 : this->updateGHX(state);
798 : }
799 14248 : }
800 :
801 : //******************************************************************************
802 :
803 13 : GLHEBase *GLHEBase::factory(EnergyPlusData &state, DataPlant::PlantEquipmentType objectType, std::string const &objectName)
804 : {
805 13 : if (state.dataGroundHeatExchanger->GetInput) {
806 13 : GetGroundHeatExchangerInput(state);
807 13 : state.dataGroundHeatExchanger->GetInput = false;
808 : }
809 13 : if (objectType == DataPlant::PlantEquipmentType::GrndHtExchgSystem) {
810 26 : auto thisObj = std::find_if(state.dataGroundHeatExchanger->verticalGLHE.begin(),
811 26 : state.dataGroundHeatExchanger->verticalGLHE.end(),
812 39 : [&objectName](const GLHEBase &myObj) { return myObj.name == objectName; });
813 13 : if (thisObj != state.dataGroundHeatExchanger->verticalGLHE.end()) {
814 13 : return &(*thisObj);
815 : }
816 13 : } else if (objectType == DataPlant::PlantEquipmentType::GrndHtExchgSlinky) {
817 0 : auto thisObj = std::find_if(state.dataGroundHeatExchanger->slinkyGLHE.begin(),
818 0 : state.dataGroundHeatExchanger->slinkyGLHE.end(),
819 0 : [&objectName](const GLHEBase &myObj) { return myObj.name == objectName; });
820 0 : if (thisObj != state.dataGroundHeatExchanger->slinkyGLHE.end()) {
821 0 : return &(*thisObj);
822 : }
823 0 : }
824 :
825 : // If we didn't find it, fatal
826 0 : ShowFatalError(state, format("Ground Heat Exchanger Factory: Error getting inputs for GHX named: {}", objectName));
827 : // Shut up the compiler
828 0 : return nullptr;
829 : }
830 :
831 : //******************************************************************************
832 :
833 5035536 : std::vector<Real64> GLHEVert::distances(MyCartesian const &point_i, MyCartesian const &point_j)
834 : {
835 5035536 : std::vector<Real64> sumVals;
836 :
837 : // Calculate the distance between points
838 5035536 : sumVals.push_back(pow_2(point_i.x - point_j.x));
839 5035536 : sumVals.push_back(pow_2(point_i.y - point_j.y));
840 5035536 : sumVals.push_back(pow_2(point_i.z - point_j.z));
841 :
842 5035536 : Real64 sumTot = 0.0;
843 5035536 : std::vector<Real64> retVals;
844 5035536 : std::for_each(sumVals.begin(), sumVals.end(), [&](Real64 n) { sumTot += n; });
845 5035536 : retVals.push_back(std::sqrt(sumTot));
846 :
847 : // Calculate distance to mirror point
848 5035536 : sumVals.pop_back();
849 5035536 : sumVals.push_back(pow_2(point_i.z - (-point_j.z)));
850 :
851 5035536 : sumTot = 0.0;
852 5035536 : std::for_each(sumVals.begin(), sumVals.end(), [&](Real64 n) { sumTot += n; });
853 5035536 : retVals.push_back(std::sqrt(sumTot));
854 :
855 10071072 : return retVals;
856 5035536 : }
857 :
858 : //******************************************************************************
859 :
860 5035536 : Real64 GLHEVert::calcResponse(std::vector<Real64> const &dists, Real64 const currTime)
861 : {
862 5035536 : Real64 pointToPointResponse = erfc(dists[0] / (2 * sqrt(this->soil.diffusivity * currTime))) / dists[0];
863 5035536 : Real64 pointToReflectedResponse = erfc(dists[1] / (2 * sqrt(this->soil.diffusivity * currTime))) / dists[1];
864 :
865 5035536 : return pointToPointResponse - pointToReflectedResponse;
866 : }
867 :
868 : //******************************************************************************
869 :
870 8976 : Real64 GLHEVert::integral(MyCartesian const &point_i, std::shared_ptr<GLHEVertSingle> const &bh_j, Real64 const currTime)
871 : {
872 :
873 : // This code could be optimized in a number of ways.
874 : // The first, most simple way would be to precompute the distances from point i to point j, then store them for reuse.
875 : // The second, more intensive method would be to break the calcResponse calls out into four different parts.
876 : // The first point, last point, odd points, and even points. Then multiply the odd/even points by their respective coefficient for the
877 : // Simpson's method. After that, all points are summed together and divided by 3.
878 :
879 8976 : Real64 sum_f = 0.0;
880 8976 : int i = 0;
881 8976 : int const lastIndex_j = static_cast<int>(bh_j->pointLocations_j.size() - 1u);
882 5044512 : for (auto const &point_j : bh_j->pointLocations_j) {
883 5035536 : std::vector<Real64> dists = distances(point_i, point_j);
884 5035536 : Real64 const f = calcResponse(dists, currTime);
885 :
886 : // Integrate using Simpson's
887 5035536 : if (i == 0 || i == lastIndex_j) {
888 17952 : sum_f += f;
889 5017584 : } else if (isEven(i)) {
890 2504304 : sum_f += 2 * f;
891 : } else {
892 2513280 : sum_f += 4 * f;
893 : }
894 :
895 5035536 : ++i;
896 5044512 : }
897 :
898 8976 : return (bh_j->dl_j / 3.0) * sum_f;
899 : }
900 :
901 : //******************************************************************************
902 :
903 176 : Real64 GLHEVert::doubleIntegral(std::shared_ptr<GLHEVertSingle> const &bh_i, std::shared_ptr<GLHEVertSingle> const &bh_j, Real64 const currTime)
904 : {
905 :
906 : // Similar optimizations as discussed above could happen here
907 :
908 176 : if (bh_i == bh_j) {
909 :
910 44 : Real64 sum_f = 0;
911 44 : int i = 0;
912 44 : int const lastIndex = static_cast<int>(bh_i->pointLocations_ii.size() - 1u);
913 2288 : for (auto &thisPoint : bh_i->pointLocations_ii) {
914 :
915 2244 : Real64 f = integral(thisPoint, bh_j, currTime);
916 :
917 : // Integrate using Simpson's
918 2244 : if (i == 0 || i == lastIndex) {
919 88 : sum_f += f;
920 2156 : } else if (isEven(i)) {
921 1056 : sum_f += 2 * f;
922 : } else {
923 1100 : sum_f += 4 * f;
924 : }
925 :
926 2244 : ++i;
927 44 : }
928 :
929 44 : return (bh_i->dl_ii / 3.0) * sum_f;
930 :
931 : } else {
932 :
933 132 : Real64 sum_f = 0;
934 132 : int i = 0;
935 132 : int const lastIndex = static_cast<int>(bh_i->pointLocations_i.size() - 1u);
936 6864 : for (auto const &thisPoint : bh_i->pointLocations_i) {
937 :
938 6732 : Real64 f = integral(thisPoint, bh_j, currTime);
939 :
940 : // Integrate using Simpson's
941 6732 : if (i == 0 || i == lastIndex) {
942 264 : sum_f += f;
943 6468 : } else if (isEven(i)) {
944 3168 : sum_f += 2 * f;
945 : } else {
946 3300 : sum_f += 4 * f;
947 : }
948 :
949 6732 : ++i;
950 132 : }
951 :
952 132 : return (bh_i->dl_i / 3.0) * sum_f;
953 : }
954 : }
955 :
956 : //******************************************************************************
957 :
958 2 : void GLHEVert::calcLongTimestepGFunctions(EnergyPlusData &state)
959 : {
960 2 : switch (this->gFuncCalcMethod) {
961 1 : case GFuncCalcMethod::UniformHeatFlux:
962 1 : this->calcUniformHeatFluxGFunctions(state);
963 1 : break;
964 1 : case GFuncCalcMethod::UniformBoreholeWallTemp:
965 1 : this->calcUniformBHWallTempGFunctions(state);
966 1 : break;
967 0 : default:
968 0 : assert(false);
969 : }
970 2 : }
971 :
972 : //******************************************************************************
973 :
974 1 : void GLHEVert::calcUniformBHWallTempGFunctions(EnergyPlusData &state)
975 : {
976 : // construct boreholes vector
977 1 : std::vector<gt::boreholes::Borehole> boreholes;
978 5 : for (auto &bh : this->myRespFactors->myBorholes) {
979 4 : boreholes.emplace_back(bh->props->bhLength, bh->props->bhTopDepth, bh->props->bhDiameter / 2.0, bh->xLoc, bh->yLoc);
980 1 : }
981 :
982 : // Obtain number of segments by adaptive discretization
983 1 : gt::segments::adaptive adptDisc;
984 1 : int nSegments = adptDisc.discretize(this->bhLength, this->totalTubeLength);
985 :
986 2 : this->myRespFactors->GFNC = gt::gfunction::uniform_borehole_wall_temperature(
987 2 : boreholes, this->myRespFactors->time, this->soil.diffusivity, nSegments, true, state.dataGlobal->numThread);
988 1 : }
989 :
990 : //******************************************************************************
991 :
992 2 : void GLHEVert::calcGFunctions(EnergyPlusData &state)
993 : {
994 :
995 : // No other choice than to calculate the g-functions here
996 2 : this->setupTimeVectors();
997 2 : this->calcShortTimestepGFunctions(state);
998 2 : this->calcLongTimestepGFunctions(state);
999 2 : this->combineShortAndLongTimestepGFunctions();
1000 :
1001 : // save data for later
1002 2 : if (state.files.outputControl.glhe && !state.dataSysVars->DisableGLHECaching) {
1003 0 : myCacheData["Response Factors"]["time"] = std::vector<Real64>(this->myRespFactors->time);
1004 0 : myCacheData["Response Factors"]["LNTTS"] = std::vector<Real64>(this->myRespFactors->LNTTS);
1005 0 : myCacheData["Response Factors"]["GFNC"] = std::vector<Real64>(this->myRespFactors->GFNC.begin(), this->myRespFactors->GFNC.end());
1006 0 : writeGLHECacheToFile(state);
1007 : }
1008 2 : }
1009 :
1010 : //******************************************************************************
1011 :
1012 2 : void GLHEVert::setupTimeVectors()
1013 : {
1014 :
1015 2 : constexpr int numDaysInYear = 365;
1016 2 : constexpr Real64 lnttsStepSize = 0.5;
1017 :
1018 : // Minimum simulation time for which finite line source method is applicable
1019 2 : constexpr Real64 lntts_min_for_long_timestep = -8.5;
1020 :
1021 : // Time scale constant
1022 2 : Real64 const t_s = pow_2(this->bhLength) / (9 * this->soil.diffusivity);
1023 :
1024 : // Temporary vector for holding the LNTTS vals
1025 2 : std::vector<Real64> tempLNTTS;
1026 :
1027 2 : tempLNTTS.push_back(lntts_min_for_long_timestep);
1028 :
1029 : // Determine how many g-function pairs to generate based on user defined maximum simulation time
1030 : while (true) {
1031 22 : Real64 maxPossibleSimTime = exp(tempLNTTS.back()) * t_s;
1032 22 : if (maxPossibleSimTime < this->myRespFactors->maxSimYears * numDaysInYear * Constant::rHoursInDay * Constant::rSecsInHour) {
1033 20 : tempLNTTS.push_back(tempLNTTS.back() + lnttsStepSize);
1034 : } else {
1035 2 : break;
1036 : }
1037 20 : }
1038 :
1039 2 : this->myRespFactors->LNTTS = tempLNTTS;
1040 2 : this->myRespFactors->time = tempLNTTS;
1041 2 : std::transform(this->myRespFactors->time.begin(), this->myRespFactors->time.end(), this->myRespFactors->time.begin(), [&t_s](auto const &c) {
1042 22 : return exp(c) * t_s;
1043 : });
1044 2 : this->myRespFactors->GFNC = std::vector<Real64>(tempLNTTS.size(), 0.0);
1045 2 : }
1046 :
1047 : //******************************************************************************
1048 :
1049 1 : void GLHEVert::calcUniformHeatFluxGFunctions(EnergyPlusData &state)
1050 : {
1051 1 : DisplayString(state, "Initializing GroundHeatExchanger:System: " + this->name);
1052 :
1053 : // Calculate the g-functions
1054 12 : for (size_t lntts_index = 0; lntts_index < this->myRespFactors->LNTTS.size(); ++lntts_index) {
1055 55 : for (auto const &bh_i : this->myRespFactors->myBorholes) {
1056 44 : Real64 sum_T_ji = 0;
1057 220 : for (auto const &bh_j : this->myRespFactors->myBorholes) {
1058 176 : sum_T_ji += doubleIntegral(bh_i, bh_j, this->myRespFactors->time[lntts_index]);
1059 44 : }
1060 44 : this->myRespFactors->GFNC[lntts_index] += sum_T_ji;
1061 11 : }
1062 11 : this->myRespFactors->GFNC[lntts_index] /= (2 * this->totalTubeLength);
1063 :
1064 11 : std::stringstream ss;
1065 11 : ss << std::fixed << std::setprecision(1) << float(lntts_index) / this->myRespFactors->LNTTS.size() * 100;
1066 :
1067 11 : DisplayString(state, "...progress: " + ss.str() + "%");
1068 11 : }
1069 1 : }
1070 :
1071 : //******************************************************************************
1072 :
1073 2 : void GLHEVert::calcShortTimestepGFunctions(EnergyPlusData &state)
1074 : {
1075 : // SUBROUTINE PARAMETER DEFINITIONS:
1076 2 : std::string_view const RoutineName = "calcShortTimestepGFunctions";
1077 :
1078 : enum class CellType
1079 : {
1080 : Invalid = -1,
1081 : FLUID,
1082 : CONVECTION,
1083 : PIPE,
1084 : GROUT,
1085 : SOIL,
1086 : Num
1087 : };
1088 :
1089 : struct Cell
1090 : {
1091 :
1092 : ~Cell() = default;
1093 :
1094 : CellType type;
1095 : Real64 radius_center;
1096 : Real64 radius_outer;
1097 : Real64 radius_inner;
1098 : Real64 thickness;
1099 : Real64 vol;
1100 : Real64 conductivity;
1101 : Real64 rhoCp;
1102 : Real64 temperature;
1103 : Real64 temperature_prev_ts;
1104 :
1105 1070 : Cell()
1106 1070 : : type(), radius_center(0.0), radius_outer(0.0), radius_inner(0.0), thickness(0.0), vol(0.0), conductivity(0.0), rhoCp(0.0),
1107 1070 : temperature(0.0), temperature_prev_ts(0.0)
1108 : {
1109 1070 : }
1110 : };
1111 :
1112 : // vector to hold 1-D cells
1113 2 : std::vector<Cell> Cells;
1114 :
1115 : // setup pipe, convection, and fluid layer geometries
1116 2 : constexpr int num_pipe_cells = 4;
1117 2 : constexpr int num_conv_cells = 1;
1118 2 : constexpr int num_fluid_cells = 3;
1119 2 : Real64 const pipe_thickness = this->pipe.thickness;
1120 2 : Real64 const pcf_cell_thickness = pipe_thickness / num_pipe_cells;
1121 2 : Real64 const radius_pipe_out = std::sqrt(2) * this->pipe.outRadius;
1122 2 : Real64 const radius_pipe_in = radius_pipe_out - pipe_thickness;
1123 2 : Real64 const radius_conv = radius_pipe_in - num_conv_cells * pcf_cell_thickness;
1124 2 : Real64 const radius_fluid = radius_conv - (num_fluid_cells - 0.5) * pcf_cell_thickness; // accounts for half thickness of boundary cell
1125 :
1126 : // setup grout layer geometry
1127 2 : constexpr int num_grout_cells = 27;
1128 2 : Real64 const radius_grout = this->bhRadius;
1129 2 : Real64 const grout_cell_thickness = (radius_grout - radius_pipe_out) / num_grout_cells;
1130 :
1131 : // setup soil layer geometry
1132 2 : constexpr int num_soil_cells = 500;
1133 2 : constexpr Real64 radius_soil = 10.0;
1134 2 : Real64 const soil_cell_thickness = (radius_soil - radius_grout) / num_soil_cells;
1135 :
1136 : // use design flow rate
1137 2 : this->massFlowRate = this->designMassFlow;
1138 :
1139 : // calculate equivalent thermal resistance between borehole wall and fluid
1140 2 : Real64 bhResistance = calcBHAverageResistance(state);
1141 2 : Real64 bhConvectionResistance = calcPipeConvectionResistance(state);
1142 2 : Real64 bh_equivalent_resistance_tube_grout = bhResistance - bhConvectionResistance / 2.0;
1143 2 : Real64 bh_equivalent_resistance_convection = bhResistance - bh_equivalent_resistance_tube_grout;
1144 :
1145 2 : Real64 initial_temperature = this->inletTemp;
1146 2 : Real64 cpFluid_init = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getSpecificHeat(state, initial_temperature, RoutineName);
1147 2 : Real64 fluidDensity_init = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getDensity(state, initial_temperature, RoutineName);
1148 :
1149 : // initialize the fluid cells
1150 8 : for (int i = 0; i < num_fluid_cells; ++i) {
1151 6 : Cell thisCell;
1152 6 : thisCell.type = CellType::FLUID;
1153 6 : thisCell.thickness = pcf_cell_thickness;
1154 6 : thisCell.radius_center = radius_fluid + i * thisCell.thickness;
1155 :
1156 : // boundary cell is only half thickness
1157 6 : if (i == 0) {
1158 2 : thisCell.radius_inner = thisCell.radius_center;
1159 : } else {
1160 4 : thisCell.radius_inner = thisCell.radius_center - thisCell.thickness / 2.0;
1161 : }
1162 :
1163 6 : thisCell.radius_outer = thisCell.radius_center + thisCell.thickness / 2.0;
1164 6 : thisCell.conductivity = 200;
1165 6 : thisCell.rhoCp = 2.0 * cpFluid_init * fluidDensity_init * pow_2(this->pipe.innerRadius) / (pow_2(radius_conv) - pow_2(radius_fluid));
1166 6 : Cells.push_back(thisCell);
1167 : }
1168 :
1169 : // initialize the convection cells
1170 4 : for (int i = 0; i < num_conv_cells; ++i) {
1171 2 : Cell thisCell;
1172 2 : thisCell.thickness = pcf_cell_thickness;
1173 2 : thisCell.radius_inner = radius_conv + i * thisCell.thickness;
1174 2 : thisCell.radius_center = thisCell.radius_inner + thisCell.thickness / 2.0;
1175 2 : thisCell.radius_outer = thisCell.radius_inner + thisCell.thickness;
1176 2 : thisCell.conductivity = log(radius_pipe_in / radius_conv) / (2 * Constant::Pi * bh_equivalent_resistance_convection);
1177 2 : thisCell.rhoCp = 1;
1178 2 : Cells.push_back(thisCell);
1179 : }
1180 :
1181 : // initialize pipe cells
1182 10 : for (int i = 0; i < num_pipe_cells; ++i) {
1183 8 : Cell thisCell;
1184 8 : thisCell.type = CellType::PIPE;
1185 8 : thisCell.thickness = pcf_cell_thickness;
1186 8 : thisCell.radius_inner = radius_pipe_in + i * thisCell.thickness;
1187 8 : thisCell.radius_center = thisCell.radius_inner + thisCell.thickness / 2.0;
1188 8 : thisCell.radius_outer = thisCell.radius_inner + thisCell.thickness;
1189 8 : thisCell.conductivity = log(radius_grout / radius_pipe_in) / (2 * Constant::Pi * bh_equivalent_resistance_tube_grout);
1190 8 : thisCell.rhoCp = this->pipe.rhoCp;
1191 8 : Cells.push_back(thisCell);
1192 : }
1193 :
1194 : // initialize grout cells
1195 56 : for (int i = 0; i < num_grout_cells; ++i) {
1196 54 : Cell thisCell;
1197 54 : thisCell.type = CellType::GROUT;
1198 54 : thisCell.thickness = grout_cell_thickness;
1199 54 : thisCell.radius_inner = radius_pipe_out + i * thisCell.thickness;
1200 54 : thisCell.radius_center = thisCell.radius_inner + thisCell.thickness / 2.0;
1201 54 : thisCell.radius_outer = thisCell.radius_inner + thisCell.thickness;
1202 54 : thisCell.conductivity = log(radius_grout / radius_pipe_in) / (2 * Constant::Pi * bh_equivalent_resistance_tube_grout);
1203 54 : thisCell.rhoCp = grout.rhoCp;
1204 54 : Cells.push_back(thisCell);
1205 : }
1206 :
1207 : // initialize soil cells
1208 1002 : for (int i = 0; i < num_soil_cells; ++i) {
1209 1000 : Cell thisCell;
1210 1000 : thisCell.type = CellType::SOIL;
1211 1000 : thisCell.thickness = soil_cell_thickness;
1212 1000 : thisCell.radius_inner = radius_grout + i * thisCell.thickness;
1213 1000 : thisCell.radius_center = thisCell.radius_inner + thisCell.thickness / 2.0;
1214 1000 : thisCell.radius_outer = thisCell.radius_inner + thisCell.thickness;
1215 1000 : thisCell.conductivity = this->soil.k;
1216 1000 : thisCell.rhoCp = this->soil.rhoCp;
1217 1000 : Cells.push_back(thisCell);
1218 : }
1219 :
1220 : // other non-geometric specific setup
1221 1072 : for (auto &thisCell : Cells) {
1222 1070 : thisCell.vol = Constant::Pi * (pow_2(thisCell.radius_outer) - pow_2(thisCell.radius_inner));
1223 1070 : thisCell.temperature = initial_temperature;
1224 2 : }
1225 :
1226 : // set upper limit of time for the short time-step g-function calcs so there is some overlap
1227 2 : Real64 constexpr lntts_max_for_short_timestep = -9.0;
1228 2 : Real64 const t_s = pow_2(this->bhLength) / (9.0 * this->soil.diffusivity);
1229 :
1230 2 : Real64 constexpr time_step = 500.0;
1231 2 : Real64 const time_max_for_short_timestep = exp(lntts_max_for_short_timestep) * t_s;
1232 2 : Real64 total_time = 0.0;
1233 :
1234 2 : Real64 constexpr heat_flux = 40.4;
1235 :
1236 : // time step loop
1237 534 : while (total_time < time_max_for_short_timestep) {
1238 :
1239 285152 : for (auto &thisCell : Cells) {
1240 284620 : thisCell.temperature_prev_ts = thisCell.temperature;
1241 532 : }
1242 :
1243 532 : std::vector<Real64> a;
1244 532 : std::vector<Real64> b;
1245 532 : std::vector<Real64> c;
1246 532 : std::vector<Real64> d;
1247 :
1248 : // setup tdma matrices
1249 532 : int num_cells = Cells.size();
1250 285152 : for (int cell_index = 0; cell_index < num_cells; ++cell_index) {
1251 284620 : if (cell_index == 0) {
1252 : // heat flux BC
1253 :
1254 532 : auto const &thisCell = Cells[cell_index];
1255 532 : auto const &eastCell = Cells[cell_index + 1];
1256 :
1257 532 : Real64 FE1 = log(thisCell.radius_outer / thisCell.radius_center) / (2 * Constant::Pi * thisCell.conductivity);
1258 532 : Real64 FE2 = log(eastCell.radius_center / eastCell.radius_inner) / (2 * Constant::Pi * eastCell.conductivity);
1259 532 : Real64 AE = 1 / (FE1 + FE2);
1260 :
1261 532 : Real64 AD = thisCell.rhoCp * thisCell.vol / time_step;
1262 :
1263 532 : a.push_back(0);
1264 532 : b.push_back(-AE / AD - 1);
1265 532 : c.push_back(AE / AD);
1266 532 : d.push_back(-thisCell.temperature_prev_ts - heat_flux / AD);
1267 :
1268 284088 : } else if (cell_index == num_cells - 1) {
1269 : // const ground temp bc
1270 :
1271 532 : auto &thisCell = Cells[cell_index];
1272 :
1273 532 : a.push_back(0);
1274 532 : b.push_back(1);
1275 532 : c.push_back(0);
1276 532 : d.push_back(thisCell.temperature_prev_ts);
1277 :
1278 : } else {
1279 : // all other cells
1280 :
1281 283556 : auto const &westCell = Cells[cell_index - 1];
1282 283556 : auto const &eastCell = Cells[cell_index + 1];
1283 283556 : auto const &thisCell = Cells[cell_index];
1284 :
1285 283556 : Real64 FE1 = log(thisCell.radius_outer / thisCell.radius_center) / (2 * Constant::Pi * thisCell.conductivity);
1286 283556 : Real64 FE2 = log(eastCell.radius_center / eastCell.radius_inner) / (2 * Constant::Pi * eastCell.conductivity);
1287 283556 : Real64 AE = 1 / (FE1 + FE2);
1288 :
1289 283556 : Real64 FW1 = log(westCell.radius_outer / westCell.radius_center) / (2 * Constant::Pi * westCell.conductivity);
1290 283556 : Real64 FW2 = log(thisCell.radius_center / thisCell.radius_inner) / (2 * Constant::Pi * thisCell.conductivity);
1291 283556 : Real64 AW = -1 / (FW1 + FW2);
1292 :
1293 283556 : Real64 AD = thisCell.rhoCp * thisCell.vol / time_step;
1294 :
1295 283556 : a.push_back(-AW / AD);
1296 283556 : b.push_back(AW / AD - AE / AD - 1);
1297 283556 : c.push_back(AE / AD);
1298 283556 : d.push_back(-thisCell.temperature_prev_ts);
1299 : }
1300 : } // end tdma setup
1301 :
1302 : // solve for new temperatures
1303 532 : std::vector<Real64> new_temps = TDMA(a, b, c, d);
1304 :
1305 285152 : for (int cell_index = 0; cell_index < num_cells; ++cell_index) {
1306 284620 : Cells[cell_index].temperature = new_temps[cell_index];
1307 : }
1308 :
1309 : // calculate bh wall temp
1310 532 : Real64 T_bhWall = 0.0;
1311 18620 : for (int cell_index = 0; cell_index < num_cells; ++cell_index) {
1312 18620 : auto const &leftCell = Cells[cell_index];
1313 18620 : auto const &rightCell = Cells[cell_index + 1];
1314 :
1315 18620 : if (leftCell.type == CellType::GROUT && rightCell.type == CellType::SOIL) {
1316 :
1317 532 : Real64 left_conductance = 2 * Constant::Pi * leftCell.conductivity / log(leftCell.radius_outer / leftCell.radius_inner);
1318 532 : Real64 right_conductance = 2 * Constant::Pi * rightCell.conductivity / log(rightCell.radius_center / leftCell.radius_inner);
1319 :
1320 532 : T_bhWall =
1321 532 : (left_conductance * leftCell.temperature + right_conductance * rightCell.temperature) / (left_conductance + right_conductance);
1322 532 : break;
1323 : }
1324 : }
1325 :
1326 532 : total_time += time_step;
1327 :
1328 532 : GFNC_shortTimestep.push_back(2 * Constant::Pi * this->soil.k * ((Cells[0].temperature - initial_temperature) / heat_flux - bhResistance));
1329 532 : LNTTS_shortTimestep.push_back(log(total_time / t_s));
1330 :
1331 532 : } // end timestep loop
1332 2 : }
1333 :
1334 : //******************************************************************************
1335 :
1336 532 : std::vector<Real64> TDMA(std::vector<Real64> const &a, std::vector<Real64> const &b, std::vector<Real64> &c, std::vector<Real64> &d)
1337 : {
1338 : // from: https://en.wikibooks.org/wiki/Algorithm_Implementation/Linear_Algebra/Tridiagonal_matrix_algorithm#C.2B.2B
1339 :
1340 532 : int n = static_cast<int>(d.size() - 1u);
1341 :
1342 532 : c[0] /= b[0];
1343 532 : d[0] /= b[0];
1344 :
1345 284088 : for (int i = 1; i < n; ++i) {
1346 283556 : c[i] /= b[i] - a[i] * c[i - 1];
1347 283556 : d[i] = (d[i] - a[i] * d[i - 1]) / (b[i] - a[i] * c[i - 1]);
1348 : }
1349 :
1350 532 : d[n] = (d[n] - a[n] * d[n - 1]) / (b[n] - a[n] * c[n - 1]);
1351 :
1352 284620 : for (int i = n; i-- > 0;) {
1353 284088 : d[i] -= c[i] * d[i + 1];
1354 : }
1355 :
1356 532 : return d;
1357 : }
1358 :
1359 : //******************************************************************************
1360 :
1361 2 : void GLHEVert::combineShortAndLongTimestepGFunctions()
1362 : {
1363 2 : std::vector<Real64> GFNC_combined;
1364 2 : std::vector<Real64> LNTTS_combined;
1365 :
1366 2 : Real64 const t_s = pow_2(this->bhLength) / (9.0 * this->soil.diffusivity);
1367 :
1368 : // Nothing to do. Just put the short time step g-functions on the combined vector
1369 2 : int num_shortTimestepGFunctions = GFNC_shortTimestep.size();
1370 534 : for (int index_shortTS = 0; index_shortTS < num_shortTimestepGFunctions; ++index_shortTS) {
1371 532 : GFNC_combined.push_back(GFNC_shortTimestep[index_shortTS]);
1372 532 : LNTTS_combined.push_back(LNTTS_shortTimestep[index_shortTS]);
1373 : }
1374 :
1375 : // Add the rest of the long time-step g-functions to the combined curve
1376 24 : for (int index_longTS = 0; index_longTS < this->myRespFactors->GFNC.size(); ++index_longTS) {
1377 22 : GFNC_combined.push_back(this->myRespFactors->GFNC[index_longTS]);
1378 22 : LNTTS_combined.push_back(this->myRespFactors->LNTTS[index_longTS]);
1379 : }
1380 :
1381 2 : this->myRespFactors->time = LNTTS_combined;
1382 2 : std::transform(this->myRespFactors->time.begin(), this->myRespFactors->time.end(), this->myRespFactors->time.begin(), [&t_s](auto const &c) {
1383 554 : return exp(c) * t_s;
1384 : });
1385 :
1386 2 : this->myRespFactors->LNTTS = LNTTS_combined;
1387 2 : this->myRespFactors->GFNC = GFNC_combined;
1388 2 : }
1389 :
1390 0 : void GLHEBase::makeThisGLHECacheAndCompareWithFileCache(EnergyPlusData &state)
1391 : {
1392 0 : if (state.files.outputControl.glhe && !state.dataSysVars->DisableGLHECaching) {
1393 0 : makeThisGLHECacheStruct();
1394 0 : readCacheFileAndCompareWithThisGLHECache(state);
1395 : }
1396 0 : }
1397 :
1398 : //******************************************************************************
1399 :
1400 0 : void GLHEVert::makeThisGLHECacheStruct()
1401 : {
1402 : // For convenience
1403 0 : auto &d = myCacheData["Phys Data"];
1404 :
1405 0 : d["Flow Rate"] = this->designFlow;
1406 0 : d["Soil k"] = this->soil.k;
1407 0 : d["Soil rhoCp"] = this->soil.rhoCp;
1408 0 : d["BH Top Depth"] = this->myRespFactors->props->bhTopDepth;
1409 0 : d["BH Length"] = this->myRespFactors->props->bhLength;
1410 0 : d["BH Diameter"] = this->myRespFactors->props->bhDiameter;
1411 0 : d["Grout k"] = this->myRespFactors->props->grout.k;
1412 0 : d["Grout rhoCp"] = this->myRespFactors->props->grout.rhoCp;
1413 0 : d["Pipe k"] = this->myRespFactors->props->pipe.k;
1414 0 : d["Pipe rhoCP"] = this->myRespFactors->props->pipe.rhoCp;
1415 0 : d["Pipe Diameter"] = this->myRespFactors->props->pipe.outDia;
1416 0 : d["Pipe Thickness"] = this->myRespFactors->props->pipe.thickness;
1417 0 : d["U-tube Dist"] = this->myRespFactors->props->bhUTubeDist;
1418 0 : d["Max Simulation Years"] = this->myRespFactors->maxSimYears;
1419 0 : d["g-Function Calc Method"] = GroundHeatExchangers::GFuncCalcMethodsStrs[int(this->gFuncCalcMethod)];
1420 :
1421 0 : auto &d_bh_data = d["BH Data"];
1422 0 : int i = 0;
1423 0 : for (auto const &thisBH : this->myRespFactors->myBorholes) {
1424 0 : ++i;
1425 0 : auto &d_bh = d_bh_data[fmt::format("BH {}", i)];
1426 0 : d_bh["X-Location"] = thisBH->xLoc;
1427 0 : d_bh["Y-Location"] = thisBH->yLoc;
1428 0 : }
1429 0 : }
1430 :
1431 : //******************************************************************************
1432 :
1433 0 : void GLHEVert::readCacheFileAndCompareWithThisGLHECache(EnergyPlusData &state)
1434 : {
1435 :
1436 0 : if (!(state.files.outputControl.glhe && FileSystem::fileExists(state.dataStrGlobals->outputGLHEFilePath))) {
1437 : // if the file doesn't exist, there are no data to read
1438 0 : return;
1439 : }
1440 : // file exists -- read data and load if possible
1441 :
1442 0 : nlohmann::json const cached_json = FileSystem::readJSON(state.dataStrGlobals->outputGLHEFilePath);
1443 :
1444 0 : for (auto const &existing_data : cached_json) {
1445 0 : if (myCacheData["Phys Data"] == existing_data["Phys Data"]) {
1446 0 : myCacheData["Response Factors"] = existing_data["Response Factors"];
1447 0 : gFunctionsExist = true;
1448 0 : break;
1449 : }
1450 0 : }
1451 :
1452 0 : if (gFunctionsExist) {
1453 : // Populate the time array
1454 0 : this->myRespFactors->time = std::vector<Real64>(myCacheData["Response Factors"]["time"].get<std::vector<Real64>>());
1455 :
1456 : // Populate the lntts array
1457 0 : this->myRespFactors->LNTTS = std::vector<Real64>(myCacheData["Response Factors"]["LNTTS"].get<std::vector<Real64>>());
1458 :
1459 : // Populate the g-function array
1460 0 : this->myRespFactors->GFNC = std::vector<Real64>(myCacheData["Response Factors"]["GFNC"].get<std::vector<Real64>>());
1461 : }
1462 0 : }
1463 :
1464 : //******************************************************************************
1465 :
1466 0 : void GLHEVert::writeGLHECacheToFile(EnergyPlusData &state) const
1467 : {
1468 :
1469 0 : nlohmann::json cached_json;
1470 0 : if (FileSystem::fileExists(state.dataStrGlobals->outputGLHEFilePath)) {
1471 : // file exists -- add data
1472 : // open file
1473 0 : cached_json = FileSystem::readJSON(state.dataStrGlobals->outputGLHEFilePath);
1474 :
1475 : // add current data
1476 0 : cached_json.emplace(fmt::format("GHLE {}", cached_json.size() + 1), myCacheData);
1477 : } else {
1478 : // file doesn't exist -- add data
1479 : // add current data
1480 0 : cached_json.emplace("GHLE 1", myCacheData);
1481 : }
1482 0 : FileSystem::writeFile<FileSystem::FileTypes::GLHE>(state.dataStrGlobals->outputGLHEFilePath, cached_json, 2);
1483 0 : }
1484 :
1485 : //******************************************************************************
1486 :
1487 2 : void GLHESlinky::calcGFunctions(EnergyPlusData &state)
1488 : {
1489 : // SUBROUTINE INFORMATION:
1490 : // AUTHOR: Matt Mitchell
1491 : // DATE WRITTEN: February, 2015
1492 :
1493 : // PURPOSE OF THIS SUBROUTINE:
1494 : // calculates g-functions for the slinky ground heat exchanger model
1495 :
1496 : // SUBROUTINE LOCAL VARIABLE DECLARATIONS:
1497 2 : constexpr Real64 tLg_min = -2.0;
1498 2 : constexpr Real64 tLg_grid = 0.25;
1499 2 : constexpr Real64 ts = 3600.0;
1500 2 : constexpr Real64 convertYearsToSeconds = 356.0 * 24.0 * 60.0 * 60.0;
1501 : Real64 fraction;
1502 2 : Array2D<Real64> valStored({0, this->numTrenches}, {0, this->numCoils}, -1.0);
1503 : int I0;
1504 : int J0;
1505 :
1506 2 : DisplayString(state, "Initializing GroundHeatExchanger:Slinky: " + this->name);
1507 :
1508 2 : this->X0.allocate(this->numCoils);
1509 2 : this->Y0.allocate(this->numTrenches);
1510 :
1511 : // Calculate the number of g-functions required
1512 2 : Real64 tLg_max = std::log10(this->maxSimYears * convertYearsToSeconds / ts);
1513 2 : int NPairs = static_cast<int>((tLg_max - tLg_min) / (tLg_grid) + 1);
1514 :
1515 : // Allocate and setup g-function arrays
1516 2 : this->myRespFactors->GFNC = std::vector<Real64>(NPairs, 0.0);
1517 2 : this->myRespFactors->LNTTS = std::vector<Real64>(NPairs, 0.0);
1518 2 : this->QnMonthlyAgg.allocate(static_cast<int>(this->maxSimYears * 12));
1519 2 : this->QnHr.allocate(730 + this->AGG + this->SubAGG);
1520 2 : this->QnSubHr.allocate(static_cast<int>((this->SubAGG + 1) * maxTSinHr + 1));
1521 2 : this->LastHourN.allocate(this->SubAGG + 1);
1522 :
1523 : // Calculate the number of loops (per trench) and number of trenches to be involved
1524 : // Due to the symmetry of a slinky GHX field, we need only calculate about
1525 : // on quarter of the rings' tube wall temperature perturbation to get the
1526 : // mean wall temperature perturbation of the entire slinky GHX field.
1527 2 : int numLC = std::ceil(this->numCoils / 2.0);
1528 2 : int numRC = std::ceil(this->numTrenches / 2.0);
1529 :
1530 : // Calculate coordinates (X0, Y0, Z0) of a ring's center
1531 202 : for (int coil = 1; coil <= this->numCoils; ++coil) {
1532 200 : this->X0(coil) = coilPitch * (coil - 1);
1533 : }
1534 6 : for (int trench = 1; trench <= this->numTrenches; ++trench) {
1535 4 : this->Y0(trench) = (trench - 1) * this->trenchSpacing;
1536 : }
1537 2 : this->Z0 = this->coilDepth;
1538 :
1539 : // If number of trenches is greater than 1, one quarter of the rings are involved.
1540 : // If number of trenches is 1, one half of the rings are involved.
1541 2 : if (this->numTrenches > 1) {
1542 2 : fraction = 0.25;
1543 : } else {
1544 0 : fraction = 0.5;
1545 : }
1546 :
1547 : // Calculate the corresponding time of each temperature response factor
1548 58 : for (int NT = 1; NT <= NPairs; ++NT) {
1549 56 : Real64 tLg = tLg_min + tLg_grid * (NT - 1);
1550 56 : Real64 t = std::pow(10, tLg) * ts;
1551 :
1552 : // Set the average temperature response of the whole field to zero
1553 56 : Real64 gFunc = 0;
1554 :
1555 56 : valStored = -1.0;
1556 :
1557 112 : for (int m1 = 1; m1 <= numRC; ++m1) {
1558 2856 : for (int n1 = 1; n1 <= numLC; ++n1) {
1559 8400 : for (int m = 1; m <= this->numTrenches; ++m) {
1560 565600 : for (int n = 1; n <= this->numCoils; ++n) {
1561 :
1562 : // Zero out val after each iteration
1563 560000 : Real64 doubleIntegralVal = 0.0;
1564 560000 : Real64 midFieldVal = 0.0;
1565 :
1566 : // Calculate the distance between ring centers
1567 560000 : Real64 disRing = distToCenter(m, n, m1, n1);
1568 :
1569 : // Save mm1 and nn1
1570 560000 : int mm1 = std::abs(m - m1);
1571 560000 : int nn1 = std::abs(n - n1);
1572 :
1573 : // If we're calculating a ring's temperature response to itself as a ring source,
1574 : // then we need some extra effort in calculating the double integral
1575 560000 : if (m1 == m && n1 == n) {
1576 2800 : I0 = 33;
1577 2800 : J0 = 1089;
1578 : } else {
1579 557200 : I0 = 33;
1580 557200 : J0 = 561;
1581 : }
1582 :
1583 : Real64 gFuncin;
1584 :
1585 : // if the ring(n1, m1) is the near-field ring of the ring(n,m)
1586 560000 : if (disRing <= 2.5 + this->coilDiameter) {
1587 : // if no calculated value has been stored
1588 64848 : if (valStored(mm1, nn1) < 0) {
1589 728 : doubleIntegralVal = doubleIntegral(m, n, m1, n1, t, I0, J0);
1590 728 : valStored(mm1, nn1) = doubleIntegralVal;
1591 : // else: if a stored value is found for the combination of (m, n, m1, n1)
1592 : } else {
1593 64120 : doubleIntegralVal = valStored(mm1, nn1);
1594 : }
1595 :
1596 : // due to symmetry, the temperature response of ring(n1, m1) should be 0.25, 0.5, or 1 times its calculated value
1597 64848 : if (!isEven(this->numTrenches) && !isEven(this->numCoils) && m1 == numRC && n1 == numLC && this->numTrenches > 1.5) {
1598 0 : gFuncin = 0.25 * doubleIntegralVal;
1599 64848 : } else if (!isEven(this->numTrenches) && m1 == numRC && this->numTrenches > 1.5) {
1600 0 : gFuncin = 0.5 * doubleIntegralVal;
1601 64848 : } else if (!isEven(this->numCoils) && n1 == numLC) {
1602 0 : gFuncin = 0.5 * doubleIntegralVal;
1603 : } else {
1604 64848 : gFuncin = doubleIntegralVal;
1605 : }
1606 :
1607 : // if the ring(n1, m1) is in the far-field or the ring(n,m)
1608 495152 : } else if (disRing > (10 + this->coilDiameter)) {
1609 302848 : gFuncin = 0;
1610 :
1611 : // else the ring(n1, m1) is in the middle-field of the ring(n,m)
1612 : } else {
1613 : // if no calculated value have been stored
1614 192304 : if (valStored(mm1, nn1) < 0.0) {
1615 2296 : midFieldVal = midFieldResponseFunction(m, n, m1, n1, t);
1616 2296 : valStored(mm1, nn1) = midFieldVal;
1617 : // if a stored value is found for the combination of (m, n, m1, n1), then
1618 : } else {
1619 190008 : midFieldVal = valStored(mm1, nn1);
1620 : }
1621 :
1622 : // due to symmetry, the temperature response of ring(n1, m1) should be 0.25, 0.5, or 1 times its calculated value
1623 192304 : if (!isEven(this->numTrenches) && !isEven(this->numCoils) && m1 == numRC && n1 == numLC && this->numTrenches > 1.5) {
1624 0 : gFuncin = 0.25 * midFieldVal;
1625 192304 : } else if (!isEven(this->numTrenches) && m1 == numRC && this->numTrenches > 1.5) {
1626 0 : gFuncin = 0.5 * midFieldVal;
1627 192304 : } else if (!isEven(this->numCoils) && n1 == numLC) {
1628 0 : gFuncin = 0.5 * midFieldVal;
1629 : } else {
1630 192304 : gFuncin = midFieldVal;
1631 : }
1632 : }
1633 :
1634 560000 : gFunc += gFuncin;
1635 :
1636 : } // n
1637 : } // m
1638 : } // n1
1639 : } // m1
1640 :
1641 56 : this->myRespFactors->GFNC[NT - 1] = (gFunc * (this->coilDiameter / 2.0)) / (4 * Constant::Pi * fraction * this->numTrenches * this->numCoils);
1642 56 : this->myRespFactors->LNTTS[NT - 1] = tLg;
1643 :
1644 : } // NT time
1645 2 : }
1646 :
1647 : //******************************************************************************
1648 :
1649 0 : void GLHESlinky::makeThisGLHECacheStruct()
1650 : {
1651 0 : }
1652 :
1653 : //******************************************************************************
1654 :
1655 0 : void GLHESlinky::readCacheFileAndCompareWithThisGLHECache([[maybe_unused]] EnergyPlusData &state)
1656 : {
1657 0 : }
1658 :
1659 : //******************************************************************************
1660 :
1661 : Real64
1662 14453208 : GLHESlinky::nearFieldResponseFunction(int const m, int const n, int const m1, int const n1, Real64 const eta, Real64 const theta, Real64 const t)
1663 : {
1664 : // SUBROUTINE INFORMATION:
1665 : // AUTHOR: Matt Mitchell
1666 : // DATE WRITTEN: February, 2015
1667 :
1668 : // PURPOSE OF THIS SUBROUTINE:
1669 : // Calculates the temperature response of from one near-field point to another
1670 :
1671 : // SUBROUTINE LOCAL VARIABLE DECLARATIONS:
1672 14453208 : Real64 distance1 = distance(m, n, m1, n1, eta, theta);
1673 14453208 : Real64 sqrtAlphaT = std::sqrt(this->soil.diffusivity * t);
1674 :
1675 14453208 : if (!verticalConfig) {
1676 :
1677 7226604 : Real64 sqrtDistDepth = std::sqrt(pow_2(distance1) + 4 * pow_2(this->coilDepth));
1678 7226604 : Real64 errFunc1 = std::erfc(0.5 * distance1 / sqrtAlphaT);
1679 7226604 : Real64 errFunc2 = std::erfc(0.5 * sqrtDistDepth / sqrtAlphaT);
1680 :
1681 7226604 : return errFunc1 / distance1 - errFunc2 / sqrtDistDepth;
1682 :
1683 : } else {
1684 :
1685 7226604 : Real64 distance2 = distanceToFictRing(m, n, m1, n1, eta, theta);
1686 7226604 : Real64 errFunc1 = std::erfc(0.5 * distance1 / sqrtAlphaT);
1687 7226604 : Real64 errFunc2 = std::erfc(0.5 * distance2 / sqrtAlphaT);
1688 :
1689 7226604 : return errFunc1 / distance1 - errFunc2 / distance2;
1690 : }
1691 : }
1692 :
1693 : //******************************************************************************
1694 :
1695 2296 : Real64 GLHESlinky::midFieldResponseFunction(int const m, int const n, int const m1, int const n1, Real64 const t)
1696 : {
1697 : // SUBROUTINE INFORMATION:
1698 : // AUTHOR: Matt Mitchell
1699 : // DATE WRITTEN: February, 2015
1700 :
1701 : // PURPOSE OF THIS SUBROUTINE:
1702 : // Calculates the temperature response of from one mid-field point to another
1703 :
1704 : // SUBROUTINE LOCAL VARIABLE DECLARATIONS:
1705 2296 : Real64 sqrtAlphaT = std::sqrt(this->soil.diffusivity * t);
1706 :
1707 2296 : Real64 distance = distToCenter(m, n, m1, n1);
1708 2296 : Real64 sqrtDistDepth = std::sqrt(pow_2(distance) + 4 * pow_2(this->coilDepth));
1709 :
1710 2296 : Real64 errFunc1 = std::erfc(0.5 * distance / sqrtAlphaT);
1711 2296 : Real64 errFunc2 = std::erfc(0.5 * sqrtDistDepth / sqrtAlphaT);
1712 :
1713 2296 : return 4 * pow_2(Constant::Pi) * (errFunc1 / distance - errFunc2 / sqrtDistDepth);
1714 : }
1715 :
1716 : //******************************************************************************
1717 :
1718 14453208 : Real64 GLHESlinky::distance(int const m, int const n, int const m1, int const n1, Real64 const eta, Real64 const theta)
1719 : {
1720 : // SUBROUTINE INFORMATION:
1721 : // AUTHOR: Matt Mitchell
1722 : // DATE WRITTEN: February, 2015
1723 :
1724 : // PURPOSE OF THIS SUBROUTINE:
1725 : // Calculates the distance between any two points on any two loops
1726 :
1727 14453208 : Real64 const cos_theta = std::cos(theta);
1728 14453208 : Real64 const sin_theta = std::sin(theta);
1729 14453208 : Real64 const cos_eta = std::cos(eta);
1730 14453208 : Real64 const sin_eta = std::sin(eta);
1731 :
1732 14453208 : Real64 x = this->X0(n) + cos_theta * (this->coilDiameter / 2.0);
1733 14453208 : Real64 y = this->Y0(m) + sin_theta * (this->coilDiameter / 2.0);
1734 :
1735 14453208 : Real64 xIn = this->X0(n1) + cos_eta * (this->coilDiameter / 2.0 - this->pipe.outRadius);
1736 14453208 : Real64 yIn = this->Y0(m1) + sin_eta * (this->coilDiameter / 2.0 - this->pipe.outRadius);
1737 :
1738 14453208 : Real64 xOut = this->X0(n1) + cos_eta * (this->coilDiameter / 2.0 + this->pipe.outRadius);
1739 14453208 : Real64 yOut = this->Y0(m1) + sin_eta * (this->coilDiameter / 2.0 + this->pipe.outRadius);
1740 :
1741 14453208 : if (!verticalConfig) {
1742 :
1743 7226604 : return 0.5 * std::sqrt(pow_2(x - xIn) + pow_2(y - yIn)) + 0.5 * std::sqrt(pow_2(x - xOut) + pow_2(y - yOut));
1744 :
1745 : } else {
1746 :
1747 7226604 : Real64 z = this->Z0 + sin_theta * (this->coilDiameter / 2.0);
1748 :
1749 7226604 : Real64 zIn = this->Z0 + sin_eta * (this->coilDiameter / 2.0 - this->pipe.outRadius);
1750 7226604 : Real64 zOut = this->Z0 + sin_eta * (this->coilDiameter / 2.0 + this->pipe.outRadius);
1751 :
1752 7226604 : return 0.5 * std::sqrt(pow_2(x - xIn) + pow_2(this->Y0(m1) - this->Y0(m)) + pow_2(z - zIn)) +
1753 7226604 : 0.5 * std::sqrt(pow_2(x - xOut) + pow_2(this->Y0(m1) - this->Y0(m)) + pow_2(z - zOut));
1754 : }
1755 : }
1756 :
1757 : //******************************************************************************
1758 :
1759 7226604 : Real64 GLHESlinky::distanceToFictRing(int const m, int const n, int const m1, int const n1, Real64 const eta, Real64 const theta)
1760 : {
1761 : // SUBROUTINE INFORMATION:
1762 : // AUTHOR: Matt Mitchell
1763 : // DATE WRITTEN: February, 2015
1764 :
1765 : // PURPOSE OF THIS SUBROUTINE:
1766 : // Calculates the distance between any two points between real and fictitious rings
1767 :
1768 7226604 : Real64 const sin_theta = std::sin(theta);
1769 7226604 : Real64 const cos_theta = std::cos(theta);
1770 7226604 : Real64 const sin_eta = std::sin(eta);
1771 7226604 : Real64 const cos_eta = std::cos(eta);
1772 :
1773 7226604 : Real64 x = this->X0(n) + cos_theta * (this->coilDiameter / 2.0);
1774 : // Real64 y = Y0( m ) + sin_theta * (coilDiameter / 2.0);
1775 7226604 : Real64 z = this->Z0 + sin_theta * (this->coilDiameter / 2.0) + 2 * this->coilDepth;
1776 :
1777 7226604 : Real64 xIn = this->X0(n1) + cos_eta * (this->coilDiameter / 2.0 - this->pipe.outRadius);
1778 : // Real64 yIn = Y0( m1 ) + sin_eta * (coilDiameter / 2.0 - pipe.outRadius);
1779 7226604 : Real64 zIn = this->Z0 + sin_eta * (this->coilDiameter / 2.0 - this->pipe.outRadius);
1780 :
1781 7226604 : Real64 xOut = this->X0(n1) + cos_eta * (this->coilDiameter / 2.0 + this->pipe.outRadius);
1782 : // Real64 yOut = Y0( m1 ) + sin_eta * (coilDiameter / 2.0 + outRadius);
1783 7226604 : Real64 zOut = this->Z0 + sin_eta * (this->coilDiameter / 2.0 + this->pipe.outRadius);
1784 :
1785 7226604 : return 0.5 * std::sqrt(pow_2(x - xIn) + pow_2(this->Y0(m1) - this->Y0(m)) + pow_2(z - zIn)) +
1786 7226604 : 0.5 * std::sqrt(pow_2(x - xOut) + pow_2(this->Y0(m1) - this->Y0(m)) + pow_2(z - zOut));
1787 : }
1788 :
1789 : //******************************************************************************
1790 :
1791 562296 : Real64 GLHESlinky::distToCenter(int const m, int const n, int const m1, int const n1)
1792 : {
1793 : // SUBROUTINE INFORMATION:
1794 : // AUTHOR: Matt Mitchell
1795 : // DATE WRITTEN: February, 2015
1796 :
1797 : // PURPOSE OF THIS SUBROUTINE:
1798 : // Calculates the center-to-center distance between rings
1799 :
1800 562296 : return std::sqrt(pow_2(this->X0(n) - this->X0(n1)) + pow_2(this->Y0(m) - this->Y0(m1)));
1801 : }
1802 :
1803 : //******************************************************************************
1804 :
1805 20225392 : inline bool GLHEBase::isEven(int const val)
1806 : {
1807 : // SUBROUTINE INFORMATION:
1808 : // AUTHOR: Matt Mitchell
1809 : // DATE WRITTEN: February, 2015
1810 :
1811 : // PURPOSE OF THIS SUBROUTINE:
1812 : // Determines if an integer is even
1813 :
1814 20225392 : if (val % 2 == 0) {
1815 10481472 : return true;
1816 : } else {
1817 9743920 : return false;
1818 : }
1819 : }
1820 :
1821 : //******************************************************************************
1822 :
1823 24024 : Real64 GLHESlinky::integral(int const m, int const n, int const m1, int const n1, Real64 const t, Real64 const eta, int const J0)
1824 : {
1825 : // SUBROUTINE INFORMATION:
1826 : // AUTHOR: Matt Mitchell
1827 : // DATE WRITTEN: February, 2015
1828 :
1829 : // PURPOSE OF THIS SUBROUTINE:
1830 : // Integrates the temperature response at one point based on
1831 : // input from other points
1832 :
1833 : // METHODOLOGY EMPLOYED:
1834 : // Simpson's 1/3 rule of integration
1835 :
1836 : // SUBROUTINE LOCAL VARIABLE DECLARATIONS:
1837 24024 : Real64 theta = 0.0;
1838 24024 : constexpr Real64 theta1 = 0.0;
1839 24024 : constexpr Real64 theta2 = 2 * Constant::Pi;
1840 24024 : std::vector<Real64> f;
1841 :
1842 24024 : Real64 h = (theta2 - theta1) / (J0 - 1);
1843 :
1844 : // Calculate the function at various equally spaced x values
1845 14477232 : for (int j = 0; j < J0; ++j) {
1846 14453208 : theta = theta1 + j * h;
1847 14453208 : f.push_back(nearFieldResponseFunction(m, n, m1, n1, eta, theta, t));
1848 : }
1849 :
1850 14429184 : for (int j = 1; j < J0 - 1; ++j) {
1851 14405160 : if (!isEven(j)) {
1852 7214592 : f[j] = 4 * f[j];
1853 : } else {
1854 7190568 : f[j] = 2 * f[j];
1855 : }
1856 : }
1857 :
1858 48048 : return (h / 3) * std::reduce(f.begin(), f.end());
1859 24024 : }
1860 :
1861 : //******************************************************************************
1862 :
1863 728 : Real64 GLHESlinky::doubleIntegral(int const m, int const n, int const m1, int const n1, Real64 const t, int const I0, int const J0)
1864 : {
1865 : // SUBROUTINE INFORMATION:
1866 : // AUTHOR: Matt Mitchell
1867 : // DATE WRITTEN: February, 2015
1868 :
1869 : // PURPOSE OF THIS SUBROUTINE:
1870 : // Integrates the temperature response at one point based on
1871 : // input from other points
1872 :
1873 : // METHODOLOGY EMPLOYED:
1874 : // Simpson's 1/3 rule of integration
1875 :
1876 : // SUBROUTINE LOCAL VARIABLE DECLARATIONS:
1877 728 : constexpr Real64 eta1 = 0.0;
1878 728 : constexpr Real64 eta2 = 2 * Constant::Pi;
1879 :
1880 728 : std::vector<Real64> g;
1881 :
1882 728 : Real64 h = (eta2 - eta1) / (I0 - 1);
1883 :
1884 : // Calculates the value of the function at various equally spaced values
1885 24752 : for (int i = 0; i < I0; ++i) {
1886 24024 : Real64 eta = eta1 + i * h;
1887 24024 : g.push_back(integral(m, n, m1, n1, t, eta, J0));
1888 : }
1889 :
1890 23296 : for (int i = 1; i < g.size() - 1; ++i) {
1891 22568 : if (!isEven(i)) {
1892 11648 : g[i] = 4 * g[i];
1893 : } else {
1894 10920 : g[i] = 2 * g[i];
1895 : }
1896 : }
1897 :
1898 1456 : return (h / 3) * std::reduce(g.begin(), g.end());
1899 728 : }
1900 :
1901 : //******************************************************************************
1902 :
1903 14136 : void GLHEVert::getAnnualTimeConstant()
1904 : {
1905 : // SUBROUTINE INFORMATION:
1906 : // AUTHOR: Matt Mitchell
1907 : // DATE WRITTEN: February, 2015
1908 :
1909 : // PURPOSE OF THIS SUBROUTINE:
1910 : // calculate annual time constant for ground conduction
1911 :
1912 14136 : constexpr Real64 hrInYear = 8760;
1913 :
1914 14136 : this->timeSS = (pow_2(this->bhLength) / (9.0 * this->soil.diffusivity)) / Constant::rSecsInHour / hrInYear; // Excuse me?
1915 14136 : this->timeSSFactor = this->timeSS * 8760.0;
1916 14136 : }
1917 :
1918 : //******************************************************************************
1919 :
1920 0 : void GLHESlinky::getAnnualTimeConstant()
1921 : {
1922 : // SUBROUTINE INFORMATION:
1923 : // AUTHOR: Matt Mitchell
1924 : // DATE WRITTEN: February, 2015
1925 :
1926 : // PURPOSE OF THIS SUBROUTINE:
1927 : // calculate annual time constant for ground conduction
1928 :
1929 0 : this->timeSSFactor = 1.0;
1930 0 : }
1931 :
1932 : //******************************************************************************
1933 :
1934 14136 : void GLHEBase::calcGroundHeatExchanger(EnergyPlusData &state)
1935 : {
1936 : // SUBROUTINE INFORMATION:
1937 : // AUTHOR: Dan Fisher
1938 : // DATE WRITTEN: August, 2000
1939 : // MODIFIED Arun Murugappan
1940 :
1941 : // PURPOSE OF THIS SUBROUTINE:
1942 : // This is the main routine to simulate the operation of vertical
1943 : // closed-loop ground heat exchangers (GLHE).
1944 :
1945 : // METHODOLOGY EMPLOYED:
1946 : // The borehole and fluid temperatures are calculated from the response to
1947 : // the current heat transfer rate and the response to the history of past
1948 : // applied heat pulses. The response to each pulse is calculated from a non-
1949 : // dimensionalized response function, or G-function, that is specific to the
1950 : // given borehole field arrangement, depth and spacing. The data defining
1951 : // this function is read from input.
1952 : // The heat pulse histories need to be recorded over an extended period (months).
1953 : // To aid computational efficiency past pulses are continuously aggregated into
1954 : // equivalent heat pulses of longer duration, as each pulse becomes less recent.
1955 :
1956 : // REFERENCES:
1957 : // Eskilson, P. 'Thermal Analysis of Heat Extraction Boreholes' Ph.D. Thesis:
1958 : // Dept. of Mathematical Physics, University of Lund, Sweden, June 1987.
1959 : // Yavuzturk, C., J.D. Spitler. 1999. 'A Short Time Step Response Factor Model
1960 : // for Vertical Ground Loop Heat Exchangers.' ASHRAE Transactions. 105(2): 475-485.
1961 :
1962 : // SUBROUTINE ARGUMENT DEFINITIONS
1963 14136 : std::string_view const RoutineName = "CalcGroundHeatExchanger";
1964 :
1965 : // LOCAL PARAMETERS
1966 : Real64 fluidAveTemp;
1967 : Real64 tmpQnSubHourly; // current Qn sub-hourly value
1968 14136 : Real64 sumTotal(0.0); // sum of all the Qn (load) blocks
1969 :
1970 : // Calculate G-Functions
1971 14136 : if (this->firstTime) {
1972 2 : if (!gFunctionsExist) {
1973 0 : makeThisGLHECacheAndCompareWithFileCache(state);
1974 0 : if (!gFunctionsExist) {
1975 0 : calcGFunctions(state);
1976 0 : gFunctionsExist = true;
1977 : }
1978 : }
1979 2 : this->firstTime = false;
1980 : }
1981 :
1982 14136 : this->inletTemp = state.dataLoopNodes->Node(this->inletNodeNum).Temp;
1983 :
1984 14136 : Real64 cpFluid = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getSpecificHeat(state, this->inletTemp, RoutineName);
1985 :
1986 14136 : Real64 kGroundFactor = 2.0 * Constant::Pi * this->soil.k;
1987 :
1988 : // Get time constants
1989 14136 : getAnnualTimeConstant();
1990 :
1991 14136 : if (triggerDesignDayReset && state.dataGlobal->WarmupFlag) {
1992 1 : updateCurSimTime = true;
1993 : }
1994 14136 : if (state.dataGlobal->DayOfSim == 1 && updateCurSimTime) {
1995 5 : state.dataGroundHeatExchanger->currentSimTime = 0.0;
1996 5 : state.dataGroundHeatExchanger->prevTimeSteps = 0.0;
1997 5 : this->QnHr = 0.0;
1998 5 : this->QnMonthlyAgg = 0.0;
1999 5 : this->QnSubHr = 0.0;
2000 5 : this->LastHourN = 1;
2001 5 : state.dataGroundHeatExchanger->N = 1;
2002 5 : updateCurSimTime = false;
2003 5 : triggerDesignDayReset = false;
2004 : }
2005 :
2006 14136 : state.dataGroundHeatExchanger->currentSimTime = (state.dataGlobal->DayOfSim - 1) * 24 + state.dataGlobal->HourOfDay - 1 +
2007 14136 : (state.dataGlobal->TimeStep - 1) * state.dataGlobal->TimeStepZone +
2008 14136 : state.dataHVACGlobal->SysTimeElapsed; //+ TimeStepsys
2009 14136 : state.dataGroundHeatExchanger->locHourOfDay = static_cast<int>(mod(state.dataGroundHeatExchanger->currentSimTime, Constant::iHoursInDay) + 1);
2010 14136 : state.dataGroundHeatExchanger->locDayOfSim = static_cast<int>(state.dataGroundHeatExchanger->currentSimTime / 24 + 1);
2011 :
2012 14136 : if (state.dataGlobal->DayOfSim > 1) {
2013 9720 : updateCurSimTime = true;
2014 : }
2015 :
2016 14136 : if (!state.dataGlobal->WarmupFlag) {
2017 2136 : triggerDesignDayReset = true;
2018 : }
2019 :
2020 14136 : if (state.dataGroundHeatExchanger->currentSimTime <= 0.0) {
2021 64 : state.dataGroundHeatExchanger->prevTimeSteps = 0.0; // This resets history when rounding 24:00 hours during warmup avoids hard crash later
2022 64 : calcAggregateLoad(state); // Just allocates and initializes prevHour array
2023 64 : return;
2024 : }
2025 :
2026 : // Store currentSimTime in prevTimeSteps only if a time step occurs
2027 14072 : if (state.dataGroundHeatExchanger->prevTimeSteps(1) != state.dataGroundHeatExchanger->currentSimTime) {
2028 1637 : state.dataGroundHeatExchanger->prevTimeSteps =
2029 3274 : eoshift(state.dataGroundHeatExchanger->prevTimeSteps, -1, state.dataGroundHeatExchanger->currentSimTime);
2030 1637 : ++state.dataGroundHeatExchanger->N;
2031 : }
2032 :
2033 14072 : if (state.dataGroundHeatExchanger->N != PrevN) {
2034 1637 : PrevN = state.dataGroundHeatExchanger->N;
2035 1637 : this->QnSubHr = eoshift(this->QnSubHr, -1, this->lastQnSubHr);
2036 : }
2037 :
2038 14072 : calcAggregateLoad(state);
2039 :
2040 : // Update the heat exchanger resistance each time
2041 14072 : this->HXResistance = calcHXResistance(state);
2042 :
2043 14072 : if (state.dataGroundHeatExchanger->N == 1) {
2044 0 : if (this->massFlowRate <= 0.0) {
2045 0 : tmpQnSubHourly = 0.0;
2046 0 : fluidAveTemp = this->tempGround;
2047 0 : this->ToutNew = this->inletTemp;
2048 : } else {
2049 0 : Real64 gFuncVal = getGFunc(state.dataGroundHeatExchanger->currentSimTime / (this->timeSSFactor));
2050 :
2051 0 : Real64 C_1 = (this->totalTubeLength) / (2.0 * this->massFlowRate * cpFluid);
2052 0 : tmpQnSubHourly = (this->tempGround - this->inletTemp) / (gFuncVal / (kGroundFactor) + this->HXResistance + C_1);
2053 0 : fluidAveTemp = this->tempGround - tmpQnSubHourly * this->HXResistance;
2054 0 : this->ToutNew = this->tempGround - tmpQnSubHourly * (gFuncVal / (kGroundFactor) + this->HXResistance - C_1);
2055 : }
2056 : } else {
2057 : // no monthly super position
2058 14072 : if (state.dataGroundHeatExchanger->currentSimTime < (hrsPerMonth + this->AGG + this->SubAGG)) {
2059 :
2060 : // Calculate the Sub Hourly Superposition
2061 :
2062 : // same as above for sub-hourly( with no aggregation]
2063 14072 : Real64 sumQnSubHourly = 0.0;
2064 : int IndexN;
2065 14072 : if (int(state.dataGroundHeatExchanger->currentSimTime) < this->SubAGG) {
2066 2832 : IndexN = int(state.dataGroundHeatExchanger->currentSimTime) + 1;
2067 : } else {
2068 11240 : IndexN = this->SubAGG + 1;
2069 : }
2070 :
2071 14072 : int subHourlyLimit = state.dataGroundHeatExchanger->N - this->LastHourN(IndexN); // Check this when running simulation
2072 938240 : for (int I = 1; I <= subHourlyLimit; ++I) {
2073 938224 : if (I == subHourlyLimit) {
2074 14056 : if (int(state.dataGroundHeatExchanger->currentSimTime) >= this->SubAGG) {
2075 : Real64 gFuncVal =
2076 11240 : getGFunc((state.dataGroundHeatExchanger->currentSimTime - state.dataGroundHeatExchanger->prevTimeSteps(I + 1)) /
2077 11240 : (this->timeSSFactor));
2078 11240 : Real64 RQSubHr = gFuncVal / (kGroundFactor);
2079 11240 : sumQnSubHourly += (this->QnSubHr(I) - this->QnHr(IndexN)) * RQSubHr;
2080 : } else {
2081 : Real64 gFuncVal =
2082 2816 : getGFunc((state.dataGroundHeatExchanger->currentSimTime - state.dataGroundHeatExchanger->prevTimeSteps(I + 1)) /
2083 2816 : (this->timeSSFactor));
2084 2816 : Real64 RQSubHr = gFuncVal / (kGroundFactor);
2085 2816 : sumQnSubHourly += this->QnSubHr(I) * RQSubHr;
2086 : }
2087 14056 : break;
2088 : }
2089 : // prevTimeSteps(I+1) This is "I+1" because prevTimeSteps(1) = CurrentTimestep
2090 924168 : Real64 gFuncVal = getGFunc((state.dataGroundHeatExchanger->currentSimTime - state.dataGroundHeatExchanger->prevTimeSteps(I + 1)) /
2091 924168 : (this->timeSSFactor));
2092 924168 : Real64 RQSubHr = gFuncVal / (kGroundFactor);
2093 924168 : sumQnSubHourly += (this->QnSubHr(I) - this->QnSubHr(I + 1)) * RQSubHr;
2094 : }
2095 :
2096 : // Calculate the Hourly Superposition
2097 : // same as above for hourly
2098 14072 : Real64 sumQnHourly = 0.0;
2099 :
2100 14072 : int hourlyLimit = int(state.dataGroundHeatExchanger->currentSimTime);
2101 672872 : for (int I = this->SubAGG + 1; I <= hourlyLimit; ++I) {
2102 669896 : if (I == hourlyLimit) {
2103 11096 : Real64 gFuncVal = getGFunc(state.dataGroundHeatExchanger->currentSimTime / (this->timeSSFactor));
2104 11096 : Real64 RQHour = gFuncVal / (kGroundFactor);
2105 11096 : sumQnHourly += this->QnHr(I) * RQHour;
2106 11096 : break;
2107 : }
2108 658800 : Real64 gFuncVal = getGFunc((state.dataGroundHeatExchanger->currentSimTime - int(state.dataGroundHeatExchanger->currentSimTime) + I) /
2109 658800 : (this->timeSSFactor));
2110 658800 : Real64 RQHour = gFuncVal / (kGroundFactor);
2111 658800 : sumQnHourly += (this->QnHr(I) - this->QnHr(I + 1)) * RQHour;
2112 : }
2113 :
2114 : // Find the total Sum of the Temperature difference due to all load blocks
2115 14072 : sumTotal = sumQnSubHourly + sumQnHourly;
2116 :
2117 : // Calculate the sub-hourly temperature due the Last Time steps Load
2118 : Real64 gFuncVal =
2119 14072 : getGFunc((state.dataGroundHeatExchanger->currentSimTime - state.dataGroundHeatExchanger->prevTimeSteps(2)) / (this->timeSSFactor));
2120 14072 : Real64 RQSubHr = gFuncVal / kGroundFactor;
2121 :
2122 14072 : if (this->massFlowRate <= 0.0) {
2123 2768 : tmpQnSubHourly = 0.0;
2124 2768 : fluidAveTemp = this->tempGround - sumTotal; // Q(N)*RB = 0
2125 2768 : this->ToutNew = this->inletTemp;
2126 : } else {
2127 : // Dr.Sitler's Explicit set of equations to calculate the New Outlet Temperature of the U-Tube
2128 11304 : Real64 C0 = RQSubHr;
2129 11304 : Real64 C1 = this->tempGround - (sumTotal - this->QnSubHr(1) * RQSubHr);
2130 11304 : Real64 C2 = this->totalTubeLength / (2.0 * this->massFlowRate * cpFluid);
2131 11304 : Real64 C3 = this->massFlowRate * cpFluid / (this->totalTubeLength);
2132 11304 : tmpQnSubHourly = (C1 - this->inletTemp) / (this->HXResistance + C0 - C2 + (1 / C3));
2133 11304 : fluidAveTemp = C1 - (C0 + this->HXResistance) * tmpQnSubHourly;
2134 11304 : this->ToutNew = C1 + (C2 - C0 - this->HXResistance) * tmpQnSubHourly;
2135 : }
2136 :
2137 : } else { // Monthly Aggregation and super position
2138 :
2139 : // the number of months of simulation elapsed
2140 0 : int numOfMonths = static_cast<int>((state.dataGroundHeatExchanger->currentSimTime + 1) / hrsPerMonth);
2141 :
2142 : // The Month up to which the monthly blocks are superposed
2143 : int currentMonth;
2144 :
2145 0 : if (state.dataGroundHeatExchanger->currentSimTime < ((numOfMonths)*hrsPerMonth) + this->AGG + this->SubAGG) {
2146 0 : currentMonth = numOfMonths - 1;
2147 : } else {
2148 0 : currentMonth = numOfMonths;
2149 : }
2150 :
2151 : // Monthly superposition
2152 : // tmp variable which holds the sum of the Temperature difference due to Aggregated heat extraction/rejection step
2153 0 : Real64 sumQnMonthly = 0.0;
2154 :
2155 0 : for (int I = 1; I <= currentMonth; ++I) {
2156 0 : if (I == 1) {
2157 0 : Real64 gFuncVal = getGFunc(state.dataGroundHeatExchanger->currentSimTime / (this->timeSSFactor));
2158 0 : Real64 RQMonth = gFuncVal / (kGroundFactor);
2159 0 : sumQnMonthly += this->QnMonthlyAgg(I) * RQMonth;
2160 0 : continue;
2161 0 : }
2162 0 : Real64 gFuncVal = getGFunc((state.dataGroundHeatExchanger->currentSimTime - (I - 1) * hrsPerMonth) / (this->timeSSFactor));
2163 0 : Real64 RQMonth = gFuncVal / (kGroundFactor);
2164 0 : sumQnMonthly += (this->QnMonthlyAgg(I) - this->QnMonthlyAgg(I - 1)) * RQMonth;
2165 : }
2166 :
2167 : // Hourly Superposition
2168 0 : Real64 sumQnHourly = 0.0;
2169 0 : int hourlyLimit = int(state.dataGroundHeatExchanger->currentSimTime - currentMonth * hrsPerMonth);
2170 0 : for (int I = 1 + this->SubAGG; I <= hourlyLimit; ++I) {
2171 0 : if (I == hourlyLimit) {
2172 : Real64 gFuncVal =
2173 0 : getGFunc((state.dataGroundHeatExchanger->currentSimTime - int(state.dataGroundHeatExchanger->currentSimTime) + I) /
2174 0 : (this->timeSSFactor));
2175 0 : Real64 RQHour = gFuncVal / (kGroundFactor);
2176 0 : sumQnHourly += (this->QnHr(I) - this->QnMonthlyAgg(currentMonth)) * RQHour;
2177 0 : break;
2178 : }
2179 0 : Real64 gFuncVal = getGFunc((state.dataGroundHeatExchanger->currentSimTime - int(state.dataGroundHeatExchanger->currentSimTime) + I) /
2180 0 : (this->timeSSFactor));
2181 0 : Real64 RQHour = gFuncVal / (kGroundFactor);
2182 0 : sumQnHourly += (this->QnHr(I) - this->QnHr(I + 1)) * RQHour;
2183 : }
2184 :
2185 : // sub-hourly Superposition
2186 0 : int subHourlyLimit = state.dataGroundHeatExchanger->N - this->LastHourN(this->SubAGG + 1);
2187 0 : Real64 sumQnSubHourly = 0.0;
2188 0 : for (int I = 1; I <= subHourlyLimit; ++I) {
2189 0 : if (I == subHourlyLimit) {
2190 0 : Real64 gFuncVal = getGFunc((state.dataGroundHeatExchanger->currentSimTime - state.dataGroundHeatExchanger->prevTimeSteps(I + 1)) /
2191 0 : (this->timeSSFactor));
2192 0 : Real64 RQSubHr = gFuncVal / (kGroundFactor);
2193 0 : sumQnSubHourly += (this->QnSubHr(I) - this->QnHr(this->SubAGG + 1)) * RQSubHr;
2194 0 : break;
2195 : }
2196 0 : Real64 gFuncVal = getGFunc((state.dataGroundHeatExchanger->currentSimTime - state.dataGroundHeatExchanger->prevTimeSteps(I + 1)) /
2197 0 : (this->timeSSFactor));
2198 0 : Real64 RQSubHr = gFuncVal / (kGroundFactor);
2199 0 : sumQnSubHourly += (this->QnSubHr(I) - this->QnSubHr(I + 1)) * RQSubHr;
2200 : }
2201 :
2202 0 : sumTotal = sumQnMonthly + sumQnHourly + sumQnSubHourly;
2203 :
2204 : // Calculate the sub-hourly temperature due the Last Time steps Load
2205 : Real64 gFuncVal =
2206 0 : getGFunc((state.dataGroundHeatExchanger->currentSimTime - state.dataGroundHeatExchanger->prevTimeSteps(2)) / (this->timeSSFactor));
2207 0 : Real64 RQSubHr = gFuncVal / (kGroundFactor);
2208 :
2209 0 : if (this->massFlowRate <= 0.0) {
2210 0 : tmpQnSubHourly = 0.0;
2211 0 : fluidAveTemp = this->tempGround - sumTotal; // Q(N)*RB = 0
2212 0 : this->ToutNew = this->inletTemp;
2213 : } else {
2214 : // Explicit set of equations to calculate the New Outlet Temperature of the U-Tube
2215 0 : Real64 C0 = RQSubHr;
2216 0 : Real64 C1 = this->tempGround - (sumTotal - this->QnSubHr(1) * RQSubHr);
2217 0 : Real64 C2 = this->totalTubeLength / (2 * this->massFlowRate * cpFluid);
2218 0 : Real64 C3 = this->massFlowRate * cpFluid / (this->totalTubeLength);
2219 0 : tmpQnSubHourly = (C1 - this->inletTemp) / (this->HXResistance + C0 - C2 + (1 / C3));
2220 0 : fluidAveTemp = C1 - (C0 + this->HXResistance) * tmpQnSubHourly;
2221 0 : this->ToutNew = C1 + (C2 - C0 - this->HXResistance) * tmpQnSubHourly;
2222 : }
2223 : } // end of AGG OR NO AGG
2224 : } // end of N = 1 branch
2225 14072 : this->bhTemp = this->tempGround - sumTotal;
2226 :
2227 : // Load the QnSubHourly Array with a new value at end of every timestep
2228 14072 : this->lastQnSubHr = tmpQnSubHourly;
2229 14072 : this->outletTemp = this->ToutNew;
2230 14072 : this->QGLHE = tmpQnSubHourly * this->totalTubeLength;
2231 14072 : this->aveFluidTemp = fluidAveTemp;
2232 : }
2233 :
2234 : //******************************************************************************
2235 :
2236 14136 : void GLHEBase::updateGHX(EnergyPlusData &state)
2237 : {
2238 : // SUBROUTINE INFORMATION:
2239 : // AUTHOR: Matt Mitchell
2240 : // DATE WRITTEN: February, 2015
2241 :
2242 : // PURPOSE OF THIS SUBROUTINE:
2243 : // Updates the outlet node and check for out of bounds temperatures
2244 :
2245 : // SUBROUTINE ARGUMENT DEFINITIONS
2246 14136 : std::string_view const RoutineName = "UpdateGroundHeatExchanger";
2247 14136 : constexpr Real64 deltaTempLimit = 100.0; // temp limit for warnings
2248 :
2249 14136 : PlantUtilities::SafeCopyPlantNode(state, this->inletNodeNum, this->outletNodeNum);
2250 :
2251 14136 : state.dataLoopNodes->Node(this->outletNodeNum).Temp = this->outletTemp;
2252 14136 : state.dataLoopNodes->Node(this->outletNodeNum).Enthalpy =
2253 14136 : this->outletTemp * state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getSpecificHeat(state, this->outletTemp, RoutineName);
2254 :
2255 14136 : Real64 GLHEdeltaTemp = std::abs(this->outletTemp - this->inletTemp);
2256 :
2257 14136 : if (GLHEdeltaTemp > deltaTempLimit && this->numErrorCalls < state.dataGroundHeatExchanger->numVerticalGLHEs && !state.dataGlobal->WarmupFlag) {
2258 0 : Real64 fluidDensity = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getDensity(state, this->inletTemp, RoutineName);
2259 0 : this->designMassFlow = this->designFlow * fluidDensity;
2260 0 : ShowWarningError(state, "Check GLHE design inputs & g-functions for consistency");
2261 0 : ShowContinueError(state, format("For GroundHeatExchanger: {}GLHE delta Temp > 100C.", this->name));
2262 0 : ShowContinueError(state, "This can be encountered in cases where the GLHE mass flow rate is either significantly");
2263 0 : ShowContinueError(state, " lower than the design value, or cases where the mass flow rate rapidly changes.");
2264 0 : ShowContinueError(state, format("GLHE Current Flow Rate={:.3T}; GLHE Design Flow Rate={:.3T}", this->massFlowRate, this->designMassFlow));
2265 0 : ++this->numErrorCalls;
2266 : }
2267 14136 : }
2268 :
2269 : //******************************************************************************
2270 :
2271 14136 : void GLHEBase::calcAggregateLoad(EnergyPlusData &state)
2272 : {
2273 :
2274 : // SUBROUTINE INFORMATION:
2275 : // AUTHOR: Arun Murugappan
2276 : // DATE WRITTEN: August, 2000
2277 :
2278 : // PURPOSE OF THIS SUBROUTINE:
2279 : // Manages the heat transfer history.
2280 :
2281 : // METHODOLOGY EMPLOYED:
2282 : // The heat pulse histories need to be recorded over an extended period (months).
2283 : // To aid computational efficiency past pulses are continuously aggregated into
2284 : // equivalent heat pulses of longer duration, as each pulse becomes less recent.
2285 : // Past sub-hourly loads are re-aggregated into equivalent hourly and monthly loads.
2286 :
2287 : // REFERENCES:
2288 : // Eskilson, P. 'Thermal Analysis of Heat Extraction Boreholes' Ph.D. Thesis:
2289 : // Dept. of Mathematical Physics, University of Lund, Sweden, June 1987.
2290 : // Yavuzturk, C., J.D. Spitler. 1999. 'A Short Time Step Response Factor Model
2291 : // for Vertical Ground Loop Heat Exchangers. ASHRAE Transactions. 105(2): 475-485.
2292 :
2293 14136 : if (state.dataGroundHeatExchanger->currentSimTime <= 0.0) {
2294 64 : return;
2295 : }
2296 :
2297 : // FOR EVERY HOUR UPDATE THE HOURLY QN this->QnHr(J)
2298 : // THIS IS DONE BY AGGREGATING THE sub-hourly QN FROM THE PREVIOUS HOUR TO UNTIL THE CURRENT HOUR
2299 : // AND STORING IT IN verticalGLHE(GLHENum)%QnHr(J)
2300 :
2301 : // sub-hourly Qn IS NOT AGGREGATED . IT IS THE BASIC LOAD
2302 14072 : if (this->prevHour != state.dataGroundHeatExchanger->locHourOfDay) {
2303 380 : Real64 SumQnHr = 0.0;
2304 : int J;
2305 2005 : for (J = 1; J <= (state.dataGroundHeatExchanger->N - this->LastHourN(1)); ++J) {
2306 1625 : SumQnHr +=
2307 1625 : this->QnSubHr(J) * std::abs(state.dataGroundHeatExchanger->prevTimeSteps(J) - state.dataGroundHeatExchanger->prevTimeSteps(J + 1));
2308 : }
2309 380 : if (state.dataGroundHeatExchanger->prevTimeSteps(1) != state.dataGroundHeatExchanger->prevTimeSteps(J)) {
2310 380 : SumQnHr /= std::abs(state.dataGroundHeatExchanger->prevTimeSteps(1) - state.dataGroundHeatExchanger->prevTimeSteps(J));
2311 : } else {
2312 0 : SumQnHr /= 0.05; // estimated small timestep
2313 : }
2314 380 : this->QnHr = eoshift(this->QnHr, -1, SumQnHr);
2315 380 : this->LastHourN = eoshift(this->LastHourN, -1, state.dataGroundHeatExchanger->N);
2316 : }
2317 :
2318 : // CHECK IF A MONTH PASSES...
2319 14072 : if (mod(((state.dataGroundHeatExchanger->locDayOfSim - 1) * Constant::iHoursInDay + (state.dataGroundHeatExchanger->locHourOfDay)),
2320 14072 : hrsPerMonth) == 0 &&
2321 0 : this->prevHour != state.dataGroundHeatExchanger->locHourOfDay) {
2322 0 : Real64 MonthNum = static_cast<int>(
2323 0 : (state.dataGroundHeatExchanger->locDayOfSim * Constant::iHoursInDay + state.dataGroundHeatExchanger->locHourOfDay) / hrsPerMonth);
2324 0 : Real64 SumQnMonth = 0.0;
2325 0 : for (int J = 1; J <= int(hrsPerMonth); ++J) {
2326 0 : SumQnMonth += this->QnHr(J);
2327 : }
2328 0 : SumQnMonth /= hrsPerMonth;
2329 0 : this->QnMonthlyAgg(MonthNum) = SumQnMonth;
2330 : }
2331 14072 : this->prevHour = state.dataGroundHeatExchanger->locHourOfDay;
2332 : }
2333 :
2334 : //******************************************************************************
2335 :
2336 26 : void GetGroundHeatExchangerInput(EnergyPlusData &state)
2337 : {
2338 : // SUBROUTINE INFORMATION:
2339 : // AUTHOR: Dan Fisher
2340 : // DATE WRITTEN: August, 2000
2341 : // MODIFIED Arun Murugappan
2342 :
2343 : // GET NUMBER OF ALL EQUIPMENT TYPES
2344 52 : state.dataGroundHeatExchanger->numVerticalGLHEs =
2345 26 : state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, "GroundHeatExchanger:System");
2346 52 : state.dataGroundHeatExchanger->numSlinkyGLHEs =
2347 26 : state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, "GroundHeatExchanger:Slinky");
2348 52 : state.dataGroundHeatExchanger->numVertArray =
2349 26 : state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, "GroundHeatExchanger:Vertical:Array");
2350 52 : state.dataGroundHeatExchanger->numVertProps =
2351 26 : state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, "GroundHeatExchanger:Vertical:Properties");
2352 52 : state.dataGroundHeatExchanger->numResponseFactors =
2353 26 : state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, "GroundHeatExchanger:ResponseFactors");
2354 52 : state.dataGroundHeatExchanger->numSingleBorehole =
2355 26 : state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, "GroundHeatExchanger:Vertical:Single");
2356 :
2357 26 : if (state.dataGroundHeatExchanger->numVerticalGLHEs <= 0 && state.dataGroundHeatExchanger->numSlinkyGLHEs <= 0) {
2358 6 : ShowSevereError(state, "Error processing inputs for GLHE objects");
2359 6 : ShowContinueError(state, "Simulation indicated these objects were found, but input processor doesn't find any");
2360 6 : ShowContinueError(state, "Check inputs for GroundHeatExchanger:System and GroundHeatExchanger:Slinky");
2361 9 : ShowContinueError(state, "Also check plant/branch inputs for references to invalid/deleted objects");
2362 : }
2363 :
2364 26 : if (state.dataGroundHeatExchanger->numVertProps > 0) {
2365 :
2366 25 : std::string const currObj = "GroundHeatExchanger:Vertical:Properties";
2367 :
2368 25 : auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(currObj);
2369 25 : if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
2370 : ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", currObj)); // LCOV_EXCL_LINE
2371 : }
2372 :
2373 25 : auto &instancesValue = instances.value();
2374 50 : for (auto it = instancesValue.begin(); it != instancesValue.end(); ++it) {
2375 25 : auto const &instance = it.value();
2376 25 : std::string const &objName = it.key();
2377 25 : std::string const &objNameUC = Util::makeUPPER(objName);
2378 25 : state.dataInputProcessing->inputProcessor->markObjectAsUsed(currObj, objName);
2379 25 : std::shared_ptr<GLHEVertProps> thisObj(new GLHEVertProps(state, objNameUC, instance));
2380 25 : state.dataGroundHeatExchanger->vertPropsVector.push_back(thisObj);
2381 50 : }
2382 25 : }
2383 :
2384 26 : if (state.dataGroundHeatExchanger->numResponseFactors > 0) {
2385 :
2386 6 : std::string const currObj = "GroundHeatExchanger:ResponseFactors";
2387 :
2388 6 : auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(currObj);
2389 6 : if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
2390 : ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", currObj)); // LCOV_EXCL_LINE
2391 : }
2392 :
2393 6 : auto &instancesValue = instances.value();
2394 12 : for (auto it = instancesValue.begin(); it != instancesValue.end(); ++it) {
2395 6 : auto const &instance = it.value();
2396 6 : std::string const &objName = it.key();
2397 6 : std::string const &objNameUC = Util::makeUPPER(objName);
2398 6 : state.dataInputProcessing->inputProcessor->markObjectAsUsed(currObj, objName);
2399 6 : std::shared_ptr<GLHEResponseFactors> thisObj(new GLHEResponseFactors(state, objNameUC, instance));
2400 6 : state.dataGroundHeatExchanger->responseFactorsVector.push_back(thisObj);
2401 12 : }
2402 6 : }
2403 :
2404 26 : if (state.dataGroundHeatExchanger->numVertArray > 0) {
2405 :
2406 16 : std::string const currObj = "GroundHeatExchanger:Vertical:Array";
2407 :
2408 16 : auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(currObj);
2409 16 : if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
2410 : ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", currObj)); // LCOV_EXCL_LINE
2411 : }
2412 :
2413 16 : auto &instancesValue = instances.value();
2414 32 : for (auto it = instancesValue.begin(); it != instancesValue.end(); ++it) {
2415 16 : auto const &instance = it.value();
2416 16 : std::string const &objName = it.key();
2417 16 : std::string const &objNameUC = Util::makeUPPER(objName);
2418 16 : state.dataInputProcessing->inputProcessor->markObjectAsUsed(currObj, objName);
2419 16 : std::shared_ptr<GLHEVertArray> thisObj(new GLHEVertArray(state, objNameUC, instance));
2420 16 : state.dataGroundHeatExchanger->vertArraysVector.push_back(thisObj);
2421 32 : }
2422 16 : }
2423 :
2424 26 : if (state.dataGroundHeatExchanger->numSingleBorehole > 0) {
2425 :
2426 4 : std::string const currObj = "GroundHeatExchanger:Vertical:Single";
2427 :
2428 4 : auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(currObj);
2429 4 : if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
2430 : ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", currObj)); // LCOV_EXCL_LINE
2431 : }
2432 :
2433 4 : auto &instancesValue = instances.value();
2434 18 : for (auto it = instancesValue.begin(); it != instancesValue.end(); ++it) {
2435 14 : auto const &instance = it.value();
2436 14 : std::string const &objName = it.key();
2437 14 : std::string const &objNameUC = Util::makeUPPER(objName);
2438 14 : state.dataInputProcessing->inputProcessor->markObjectAsUsed(currObj, objName);
2439 14 : std::shared_ptr<GLHEVertSingle> thisObj(new GLHEVertSingle(state, objNameUC, instance));
2440 14 : state.dataGroundHeatExchanger->singleBoreholesVector.push_back(thisObj);
2441 18 : }
2442 4 : }
2443 :
2444 26 : if (state.dataGroundHeatExchanger->numVerticalGLHEs > 0) {
2445 :
2446 22 : std::string const currObj = "GroundHeatExchanger:System";
2447 :
2448 22 : auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(currObj);
2449 22 : if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
2450 : ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", currObj)); // LCOV_EXCL_LINE
2451 : }
2452 :
2453 22 : auto &instancesValue = instances.value();
2454 44 : for (auto it = instancesValue.begin(); it != instancesValue.end(); ++it) {
2455 22 : auto const &instance = it.value();
2456 22 : std::string const &objName = it.key();
2457 22 : std::string const &objNameUC = Util::makeUPPER(objName);
2458 22 : state.dataInputProcessing->inputProcessor->markObjectAsUsed(currObj, objName);
2459 22 : state.dataGroundHeatExchanger->verticalGLHE.emplace_back(state, objNameUC, instance);
2460 44 : }
2461 22 : }
2462 :
2463 : // SLINKY GLHE
2464 26 : if (state.dataGroundHeatExchanger->numSlinkyGLHEs > 0) {
2465 :
2466 1 : std::string const currObj = "GroundHeatExchanger:Slinky";
2467 :
2468 1 : auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(currObj);
2469 1 : if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
2470 : ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", currObj)); // LCOV_EXCL_LINE
2471 : }
2472 :
2473 1 : auto &instancesValue = instances.value();
2474 2 : for (auto it = instancesValue.begin(); it != instancesValue.end(); ++it) {
2475 1 : auto const &instance = it.value();
2476 1 : std::string const &objName = it.key();
2477 1 : std::string const &objNameUC = Util::makeUPPER(objName);
2478 1 : state.dataInputProcessing->inputProcessor->markObjectAsUsed(currObj, objName);
2479 1 : state.dataGroundHeatExchanger->slinkyGLHE.emplace_back(state, objNameUC, instance);
2480 2 : }
2481 1 : }
2482 26 : }
2483 :
2484 : //******************************************************************************
2485 :
2486 3 : void GLHEBase::setupOutput(EnergyPlusData &state)
2487 : {
2488 6 : SetupOutputVariable(state,
2489 : "Ground Heat Exchanger Average Borehole Temperature",
2490 : Constant::Units::C,
2491 3 : this->bhTemp,
2492 : OutputProcessor::TimeStepType::System,
2493 : OutputProcessor::StoreType::Average,
2494 3 : this->name);
2495 6 : SetupOutputVariable(state,
2496 : "Ground Heat Exchanger Heat Transfer Rate",
2497 : Constant::Units::W,
2498 3 : this->QGLHE,
2499 : OutputProcessor::TimeStepType::System,
2500 : OutputProcessor::StoreType::Average,
2501 3 : this->name);
2502 6 : SetupOutputVariable(state,
2503 : "Ground Heat Exchanger Inlet Temperature",
2504 : Constant::Units::C,
2505 3 : this->inletTemp,
2506 : OutputProcessor::TimeStepType::System,
2507 : OutputProcessor::StoreType::Average,
2508 3 : this->name);
2509 6 : SetupOutputVariable(state,
2510 : "Ground Heat Exchanger Outlet Temperature",
2511 : Constant::Units::C,
2512 3 : this->outletTemp,
2513 : OutputProcessor::TimeStepType::System,
2514 : OutputProcessor::StoreType::Average,
2515 3 : this->name);
2516 6 : SetupOutputVariable(state,
2517 : "Ground Heat Exchanger Mass Flow Rate",
2518 : Constant::Units::kg_s,
2519 3 : this->massFlowRate,
2520 : OutputProcessor::TimeStepType::System,
2521 : OutputProcessor::StoreType::Average,
2522 3 : this->name);
2523 6 : SetupOutputVariable(state,
2524 : "Ground Heat Exchanger Average Fluid Temperature",
2525 : Constant::Units::C,
2526 3 : this->aveFluidTemp,
2527 : OutputProcessor::TimeStepType::System,
2528 : OutputProcessor::StoreType::Average,
2529 3 : this->name);
2530 6 : SetupOutputVariable(state,
2531 : "Ground Heat Exchanger Farfield Ground Temperature",
2532 : Constant::Units::C,
2533 3 : this->tempGround,
2534 : OutputProcessor::TimeStepType::System,
2535 : OutputProcessor::StoreType::Average,
2536 3 : this->name);
2537 3 : }
2538 :
2539 : //******************************************************************************
2540 :
2541 11309 : Real64 GLHEVert::calcBHAverageResistance(EnergyPlusData &state)
2542 : {
2543 : // Calculates the average thermal resistance of the borehole using the first-order multipole method.
2544 : // Javed, S. & Spitler, J.D. 2016. 'Accuracy of Borehole Thermal Resistance Calculation Methods
2545 : // for Grouted Single U-tube Ground Heat Exchangers.' Applied Energy.187:790-806.
2546 : // Equation 13
2547 :
2548 11309 : Real64 const beta = 2 * Constant::Pi * this->grout.k * calcPipeResistance(state);
2549 :
2550 11309 : Real64 const final_term_1 = log(this->theta_2 / (2 * this->theta_1 * pow(1 - pow_4(this->theta_1), this->sigma)));
2551 11309 : Real64 const num_final_term_2 = pow_2(this->theta_3) * pow_2(1 - (4 * this->sigma * pow_4(this->theta_1)) / (1 - pow_4(this->theta_1)));
2552 11309 : Real64 const den_final_term_2_pt_1 = (1 + beta) / (1 - beta);
2553 11309 : Real64 const den_final_term_2_pt_2 = pow_2(this->theta_3) * (1 + (16 * this->sigma * pow_4(this->theta_1)) / pow_2(1 - pow_4(this->theta_1)));
2554 11309 : Real64 const den_final_term_2 = den_final_term_2_pt_1 + den_final_term_2_pt_2;
2555 11309 : Real64 const final_term_2 = num_final_term_2 / den_final_term_2;
2556 :
2557 11309 : return (1 / (4 * Constant::Pi * this->grout.k)) * (beta + final_term_1 - final_term_2);
2558 : }
2559 :
2560 : //******************************************************************************
2561 :
2562 11307 : Real64 GLHEVert::calcBHTotalInternalResistance(EnergyPlusData &state)
2563 : {
2564 : // Calculates the total internal thermal resistance of the borehole using the first-order multipole method.
2565 : // Javed, S. & Spitler, J.D. 2016. 'Accuracy of Borehole Thermal Resistance Calculation Methods
2566 : // for Grouted Single U-tube Ground Heat Exchangers.' Applied Energy. 187:790-806.
2567 : // Equation 26
2568 :
2569 11307 : Real64 beta = 2 * Constant::Pi * this->grout.k * calcPipeResistance(state);
2570 :
2571 11307 : Real64 final_term_1 = log(pow(1 + pow_2(this->theta_1), this->sigma) / (this->theta_3 * pow(1 - pow_2(this->theta_1), this->sigma)));
2572 11307 : Real64 num_term_2 = pow_2(this->theta_3) * pow_2(1 - pow_4(this->theta_1) + 4 * this->sigma * pow_2(this->theta_1));
2573 11307 : Real64 den_term_2_pt_1 = (1 + beta) / (1 - beta) * pow_2(1 - pow_4(this->theta_1));
2574 11307 : Real64 den_term_2_pt_2 = pow_2(this->theta_3) * pow_2(1 - pow_4(this->theta_1));
2575 11307 : Real64 den_term_2_pt_3 = 8 * this->sigma * pow_2(this->theta_1) * pow_2(this->theta_3) * (1 + pow_4(this->theta_1));
2576 11307 : Real64 den_term_2 = den_term_2_pt_1 - den_term_2_pt_2 + den_term_2_pt_3;
2577 11307 : Real64 final_term_2 = num_term_2 / den_term_2;
2578 :
2579 11307 : return (1 / (Constant::Pi * this->grout.k)) * (beta + final_term_1 - final_term_2);
2580 : }
2581 :
2582 : //******************************************************************************
2583 :
2584 3 : Real64 GLHEVert::calcBHGroutResistance(EnergyPlusData &state)
2585 : {
2586 : // Calculates grout resistance. Use for validation.
2587 : // Javed, S. & Spitler, J.D. 2016. 'Accuracy of Borehole Thermal Resistance Calculation Methods
2588 : // for Grouted Single U-tube Ground Heat Exchangers.' Applied Energy. 187:790-806.
2589 : // Equation 3
2590 :
2591 3 : return calcBHAverageResistance(state) - calcPipeResistance(state) / 2.0;
2592 : }
2593 :
2594 : //******************************************************************************
2595 :
2596 14072 : Real64 GLHEVert::calcHXResistance(EnergyPlusData &state)
2597 : {
2598 : // Calculates the effective thermal resistance of the borehole assuming a uniform heat flux.
2599 : // Javed, S. & Spitler, J.D. Calculation of Borehole Thermal Resistance. In 'Advances in
2600 : // Ground-Source Heat Pump Systems,' pp. 84. Rees, S.J. ed. Cambridge, MA. Elsevier Ltd. 2016.
2601 : // Eq: 3-67
2602 :
2603 14072 : if (this->massFlowRate <= 0.0) {
2604 2768 : return 0;
2605 : } else {
2606 11304 : std::string_view const RoutineName = "calcBHResistance";
2607 :
2608 11304 : Real64 const cpFluid = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getSpecificHeat(state, this->inletTemp, RoutineName);
2609 11304 : return calcBHAverageResistance(state) +
2610 11304 : 1 / (3 * calcBHTotalInternalResistance(state)) * pow_2(this->bhLength / (this->massFlowRate * cpFluid));
2611 : }
2612 : }
2613 :
2614 : //******************************************************************************
2615 :
2616 22621 : Real64 GLHEVert::calcPipeConductionResistance()
2617 : {
2618 : // Calculates the thermal resistance of a pipe, in [K/(W/m)].
2619 : // Javed, S. & Spitler, J.D. 2016. 'Accuracy of Borehole Thermal Resistance Calculation Methods
2620 : // for Grouted Single U-tube Ground Heat Exchangers.' Applied Energy. 187:790-806.
2621 :
2622 22621 : return log(this->pipe.outDia / this->pipe.innerDia) / (2 * Constant::Pi * this->pipe.k);
2623 : }
2624 :
2625 : //******************************************************************************
2626 :
2627 22625 : Real64 GLHEVert::calcPipeConvectionResistance(EnergyPlusData &state)
2628 : {
2629 : // Calculates the convection resistance using Gnielinski and Petukov, in [K/(W/m)]
2630 : // Gneilinski, V. 1976. 'New equations for heat and mass transfer in turbulent pipe and channel flow.'
2631 : // International Chemical Engineering 16(1976), pp. 359-368.
2632 :
2633 : // SUBROUTINE PARAMETER DEFINITIONS:
2634 22625 : constexpr const char *RoutineName("calcPipeConvectionResistance");
2635 :
2636 : // Get fluid props
2637 22625 : this->inletTemp = state.dataLoopNodes->Node(this->inletNodeNum).Temp;
2638 :
2639 22625 : Real64 const cpFluid = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getSpecificHeat(state, this->inletTemp, RoutineName);
2640 22625 : Real64 const kFluid = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getConductivity(state, this->inletTemp, RoutineName);
2641 22625 : Real64 const fluidViscosity = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getViscosity(state, this->inletTemp, RoutineName);
2642 :
2643 : // Smoothing fit limits
2644 22625 : constexpr Real64 lower_limit = 2000;
2645 22625 : constexpr Real64 upper_limit = 4000;
2646 :
2647 22625 : Real64 const bhMassFlowRate = this->massFlowRate / this->myRespFactors->numBoreholes;
2648 22625 : Real64 const reynoldsNum = 4 * bhMassFlowRate / (fluidViscosity * Constant::Pi * this->pipe.innerDia);
2649 :
2650 22625 : Real64 nusseltNum = 0.0;
2651 22625 : if (reynoldsNum < lower_limit) {
2652 22609 : nusseltNum = 4.01; // laminar mean(4.36, 3.66)
2653 16 : } else if (reynoldsNum < upper_limit) {
2654 1 : Real64 constexpr nu_low = 4.01; // laminar
2655 1 : Real64 const f = frictionFactor(reynoldsNum); // turbulent
2656 1 : Real64 const prandtlNum = (cpFluid * fluidViscosity) / (kFluid);
2657 1 : Real64 const nu_high = (f / 8) * (reynoldsNum - 1000) * prandtlNum / (1 + 12.7 * std::sqrt(f / 8) * (pow(prandtlNum, 2.0 / 3.0) - 1));
2658 1 : Real64 const sf = 1 / (1 + std::exp(-(reynoldsNum - 3000) / 150.0)); // smoothing function
2659 1 : nusseltNum = (1 - sf) * nu_low + sf * nu_high;
2660 : } else {
2661 15 : Real64 const f = frictionFactor(reynoldsNum);
2662 15 : Real64 const prandtlNum = (cpFluid * fluidViscosity) / (kFluid);
2663 15 : nusseltNum = (f / 8) * (reynoldsNum - 1000) * prandtlNum / (1 + 12.7 * std::sqrt(f / 8) * (pow(prandtlNum, 2.0 / 3.0) - 1));
2664 : }
2665 :
2666 22625 : Real64 h = nusseltNum * kFluid / this->pipe.innerDia;
2667 :
2668 22625 : return 1 / (h * Constant::Pi * this->pipe.innerDia);
2669 : }
2670 :
2671 : //******************************************************************************
2672 :
2673 25 : Real64 GLHEVert::frictionFactor(Real64 const reynoldsNum)
2674 : {
2675 : // Calculates the friction factor in smooth tubes
2676 : // Petukov, B.S. 1970. 'Heat transfer and friction in turbulent pipe flow with variable physical properties.'
2677 : // In Advances in Heat Transfer, ed. T.F. Irvine and J.P. Hartnett, Vol. 6. New York Academic Press.
2678 :
2679 : // limits picked to be within about 1% of actual values
2680 25 : constexpr Real64 lower_limit = 1500;
2681 25 : constexpr Real64 upper_limit = 5000;
2682 :
2683 25 : if (reynoldsNum < lower_limit) {
2684 3 : return 64.0 / reynoldsNum; // pure laminar flow
2685 22 : } else if (reynoldsNum < upper_limit) {
2686 : // pure laminar flow
2687 4 : Real64 const f_low = 64.0 / reynoldsNum;
2688 : // pure turbulent flow
2689 4 : Real64 const f_high = pow(0.79 * log(reynoldsNum) - 1.64, -2.0);
2690 4 : Real64 const sf = 1 / (1 + exp(-(reynoldsNum - 3000.0) / 450.0)); // smoothing function
2691 4 : return (1 - sf) * f_low + sf * f_high;
2692 : } else {
2693 18 : return pow(0.79 * log(reynoldsNum) - 1.64, -2.0); // pure turbulent flow
2694 : }
2695 : }
2696 :
2697 : //******************************************************************************
2698 :
2699 22620 : Real64 GLHEVert::calcPipeResistance(EnergyPlusData &state)
2700 : {
2701 : // Calculates the combined conduction and convection pipe resistance
2702 : // Javed, S. & Spitler, J.D. 2016. 'Accuracy of Borehole Thermal Resistance Calculation Methods
2703 : // for Grouted Single U-tube Ground Heat Exchangers.' J. Energy Engineering. Draft in progress.
2704 : // Equation 3
2705 :
2706 22620 : return calcPipeConductionResistance() + calcPipeConvectionResistance(state);
2707 : }
2708 :
2709 : //******************************************************************************
2710 :
2711 4 : Real64 GLHESlinky::calcHXResistance(EnergyPlusData &state)
2712 : {
2713 :
2714 : // SUBROUTINE INFORMATION:
2715 : // AUTHOR Matt Mitchell
2716 : // DATE WRITTEN February, 2015
2717 :
2718 : // PURPOSE OF THIS SUBROUTINE:
2719 : // Calculates the resistance of the slinky HX from the fluid to the outer tube wall.
2720 :
2721 : // SUBROUTINE PARAMETER DEFINITIONS:
2722 4 : std::string_view const RoutineName = "CalcSlinkyGroundHeatExchanger";
2723 :
2724 : // SUBROUTINE LOCAL VARIABLE DECLARATIONS:
2725 : Real64 nusseltNum;
2726 : Real64 Rconv;
2727 4 : constexpr Real64 A = 3150;
2728 4 : constexpr Real64 B = 350;
2729 4 : constexpr Real64 laminarNusseltNo = 4.364;
2730 :
2731 4 : Real64 cpFluid = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getSpecificHeat(state, this->inletTemp, RoutineName);
2732 4 : Real64 kFluid = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getConductivity(state, this->inletTemp, RoutineName);
2733 4 : Real64 fluidDensity = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getDensity(state, this->inletTemp, RoutineName);
2734 4 : Real64 fluidViscosity = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getViscosity(state, this->inletTemp, RoutineName);
2735 :
2736 : // calculate mass flow rate
2737 4 : Real64 singleSlinkyMassFlowRate = this->massFlowRate / this->numTrenches;
2738 :
2739 4 : Real64 pipeInnerRad = this->pipe.outRadius - this->pipe.thickness;
2740 4 : Real64 pipeInnerDia = 2.0 * pipeInnerRad;
2741 :
2742 4 : if (singleSlinkyMassFlowRate == 0.0) {
2743 1 : Rconv = 0.0;
2744 : } else {
2745 : // Re=Rho*V*D/Mu
2746 : Real64 reynoldsNum =
2747 3 : fluidDensity * pipeInnerDia * (singleSlinkyMassFlowRate / fluidDensity / (Constant::Pi * pow_2(pipeInnerRad))) / fluidViscosity;
2748 3 : Real64 prandtlNum = (cpFluid * fluidViscosity) / (kFluid);
2749 : // Convection Resistance
2750 3 : if (reynoldsNum <= 2300) {
2751 1 : nusseltNum = laminarNusseltNo;
2752 2 : } else if (reynoldsNum > 2300 && reynoldsNum <= 4000) {
2753 1 : Real64 sf = 0.5 + 0.5 * std::tanh((reynoldsNum - A) / B);
2754 1 : Real64 turbulentNusseltNo = 0.023 * std::pow(reynoldsNum, 0.8) * std::pow(prandtlNum, 0.35);
2755 1 : nusseltNum = laminarNusseltNo * (1 - sf) + turbulentNusseltNo * sf;
2756 1 : } else {
2757 1 : nusseltNum = 0.023 * std::pow(reynoldsNum, 0.8) * std::pow(prandtlNum, 0.35);
2758 : }
2759 3 : Real64 hci = nusseltNum * kFluid / pipeInnerDia;
2760 3 : Rconv = 1.0 / (2.0 * Constant::Pi * pipeInnerDia * hci);
2761 : }
2762 :
2763 : // Conduction Resistance
2764 4 : Real64 Rcond = std::log(this->pipe.outRadius / pipeInnerRad) / (2.0 * Constant::Pi * this->pipe.k) / 2.0; // pipe in parallel so /2
2765 :
2766 4 : return Rcond + Rconv;
2767 : }
2768 :
2769 : //******************************************************************************
2770 :
2771 1622335 : Real64 GLHEBase::interpGFunc(Real64 const x_val) const
2772 : {
2773 : // Purpose: interpolate between g-function values, with linear extrapolation above and below range
2774 :
2775 1622335 : auto const &x = this->myRespFactors->LNTTS;
2776 1622335 : auto const &y = this->myRespFactors->GFNC;
2777 1622335 : auto const &upper_it = std::upper_bound(x.begin(), x.end(), x_val);
2778 :
2779 1622335 : int l_idx = 0;
2780 1622335 : int u_idx = 0;
2781 :
2782 1622335 : if (upper_it == x.begin()) {
2783 : // Linear extrapolation beyond the lower bound
2784 13899 : l_idx = 0;
2785 13899 : u_idx = 1;
2786 1608436 : } else if (upper_it == x.end()) {
2787 : // Linear extrapolation beyond the upper bound
2788 13 : u_idx = x.size() - 1;
2789 13 : l_idx = u_idx - 1;
2790 : } else {
2791 : // In the middle of the range
2792 3216846 : u_idx = std::distance(x.begin(), upper_it);
2793 1608423 : l_idx = u_idx - 1;
2794 : }
2795 :
2796 1622335 : Real64 const x_low = x[l_idx];
2797 1622335 : Real64 const x_high = x[u_idx];
2798 1622335 : Real64 const y_low = y[l_idx];
2799 1622335 : Real64 const y_high = y[u_idx];
2800 :
2801 1622335 : return (x_val - x_low) / (x_high - x_low) * (y_high - y_low) + y_low;
2802 1622335 : }
2803 :
2804 : //******************************************************************************
2805 :
2806 1 : Real64 GLHESlinky::getGFunc(Real64 const time)
2807 : {
2808 : // SUBROUTINE INFORMATION:
2809 : // AUTHOR: Matt Mitchell
2810 : // DATE WRITTEN: February, 2015
2811 :
2812 : // PURPOSE OF THIS SUBROUTINE:
2813 : // Gets the g-function for slinky GHXs Note: Base 10 here.
2814 :
2815 1 : Real64 LNTTS = std::log10(time);
2816 1 : return interpGFunc(LNTTS);
2817 : }
2818 :
2819 : //******************************************************************************
2820 :
2821 1622194 : Real64 GLHEVert::getGFunc(Real64 const time)
2822 : {
2823 : // SUBROUTINE INFORMATION:
2824 : // AUTHOR: Matt Mitchell
2825 : // DATE WRITTEN: February, 2015
2826 :
2827 : // PURPOSE OF THIS SUBROUTINE:
2828 : // Gets the g-function for vertical GHXs Note: Base e here.
2829 :
2830 1622194 : Real64 LNTTS = std::log(time);
2831 1622194 : Real64 gFuncVal = interpGFunc(LNTTS);
2832 1622194 : Real64 RATIO = this->bhRadius / this->bhLength;
2833 :
2834 1622194 : if (RATIO != this->myRespFactors->gRefRatio) {
2835 1 : gFuncVal -= std::log(this->bhRadius / (this->bhLength * this->myRespFactors->gRefRatio));
2836 : }
2837 :
2838 1622194 : return gFuncVal;
2839 : }
2840 :
2841 : //******************************************************************************
2842 :
2843 14263 : void GLHEVert::initGLHESimVars(EnergyPlusData &state)
2844 : {
2845 : // SUBROUTINE INFORMATION:
2846 : // AUTHOR: Dan Fisher
2847 : // DATE WRITTEN: August, 2000
2848 : // MODIFIED Arun Murugappan
2849 :
2850 : // SUBROUTINE LOCAL VARIABLE DECLARATIONS:
2851 14263 : Real64 currTime = ((state.dataGlobal->DayOfSim - 1) * 24 + (state.dataGlobal->HourOfDay - 1) +
2852 14263 : (state.dataGlobal->TimeStep - 1) * state.dataGlobal->TimeStepZone + state.dataHVACGlobal->SysTimeElapsed) *
2853 14263 : Constant::rSecsInHour;
2854 :
2855 14263 : if (this->myEnvrnFlag && state.dataGlobal->BeginEnvrnFlag) {
2856 7 : this->initEnvironment(state, currTime);
2857 : }
2858 :
2859 : // Calculate the average ground temperature over the depth of the borehole
2860 14263 : Real64 minDepth = this->myRespFactors->props->bhTopDepth;
2861 14263 : Real64 maxDepth = this->myRespFactors->props->bhLength + minDepth;
2862 14263 : Real64 oneQuarterDepth = minDepth + (maxDepth - minDepth) * 0.25;
2863 14263 : Real64 halfDepth = minDepth + (maxDepth - minDepth) * 0.5;
2864 14263 : Real64 threeQuarterDepth = minDepth + (maxDepth - minDepth) * 0.75;
2865 :
2866 14263 : this->tempGround = 0;
2867 :
2868 14263 : this->tempGround += this->groundTempModel->getGroundTempAtTimeInSeconds(state, minDepth, currTime);
2869 14263 : this->tempGround += this->groundTempModel->getGroundTempAtTimeInSeconds(state, maxDepth, currTime);
2870 14263 : this->tempGround += this->groundTempModel->getGroundTempAtTimeInSeconds(state, oneQuarterDepth, currTime);
2871 14263 : this->tempGround += this->groundTempModel->getGroundTempAtTimeInSeconds(state, halfDepth, currTime);
2872 14263 : this->tempGround += this->groundTempModel->getGroundTempAtTimeInSeconds(state, threeQuarterDepth, currTime);
2873 :
2874 14263 : this->tempGround /= 5;
2875 :
2876 14263 : this->massFlowRate = PlantUtilities::RegulateCondenserCompFlowReqOp(state, this->plantLoc, this->designMassFlow);
2877 :
2878 14263 : PlantUtilities::SetComponentFlowRate(state, this->massFlowRate, this->inletNodeNum, this->outletNodeNum, this->plantLoc);
2879 :
2880 : // Reset local environment init flag
2881 14263 : if (!state.dataGlobal->BeginEnvrnFlag) {
2882 14104 : this->myEnvrnFlag = true;
2883 : }
2884 14263 : }
2885 :
2886 : //******************************************************************************
2887 :
2888 7 : void GLHEVert::initEnvironment(EnergyPlusData &state, [[maybe_unused]] Real64 const CurTime)
2889 : {
2890 7 : std::string_view const RoutineName = "initEnvironment";
2891 7 : this->myEnvrnFlag = false;
2892 :
2893 7 : Real64 fluidDensity = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getDensity(state, 20.0, RoutineName);
2894 7 : this->designMassFlow = this->designFlow * fluidDensity;
2895 7 : PlantUtilities::InitComponentNodes(state, 0.0, this->designMassFlow, this->inletNodeNum, this->outletNodeNum);
2896 :
2897 7 : this->lastQnSubHr = 0.0;
2898 7 : state.dataLoopNodes->Node(this->inletNodeNum).Temp = this->tempGround;
2899 7 : state.dataLoopNodes->Node(this->outletNodeNum).Temp = this->tempGround;
2900 :
2901 : // zero out all history arrays
2902 7 : this->QnHr = 0.0;
2903 7 : this->QnMonthlyAgg = 0.0;
2904 7 : this->QnSubHr = 0.0;
2905 7 : this->LastHourN = 0;
2906 7 : state.dataGroundHeatExchanger->prevTimeSteps = 0.0;
2907 7 : state.dataGroundHeatExchanger->currentSimTime = 0.0;
2908 7 : this->QGLHE = 0.0;
2909 7 : this->prevHour = 1;
2910 7 : }
2911 :
2912 : //******************************************************************************
2913 :
2914 13 : void GLHEVert::oneTimeInit_new(EnergyPlusData &state)
2915 : {
2916 : // Locate the hx on the plant loops for later usage
2917 13 : bool errFlag = false;
2918 26 : PlantUtilities::ScanPlantLoopsForObject(
2919 13 : state, this->name, DataPlant::PlantEquipmentType::GrndHtExchgSystem, this->plantLoc, errFlag, _, _, _, _, _);
2920 13 : if (errFlag) {
2921 0 : ShowFatalError(state, "initGLHESimVars: Program terminated due to previous condition(s).");
2922 : }
2923 13 : }
2924 0 : void GLHEVert::oneTimeInit([[maybe_unused]] EnergyPlusData &state)
2925 : {
2926 0 : }
2927 :
2928 : //******************************************************************************
2929 :
2930 0 : void GLHESlinky::initGLHESimVars(EnergyPlusData &state)
2931 : {
2932 : // SUBROUTINE INFORMATION:
2933 : // AUTHOR: Dan Fisher
2934 : // DATE WRITTEN: August, 2000
2935 : // MODIFIED Arun Murugappan
2936 :
2937 0 : Real64 CurTime = ((state.dataGlobal->DayOfSim - 1) * Constant::rHoursInDay + (state.dataGlobal->HourOfDay - 1) +
2938 0 : (state.dataGlobal->TimeStep - 1) * state.dataGlobal->TimeStepZone + state.dataHVACGlobal->SysTimeElapsed) *
2939 0 : Constant::rSecsInHour;
2940 :
2941 : // Init more variables
2942 0 : if (this->myEnvrnFlag && state.dataGlobal->BeginEnvrnFlag) {
2943 0 : this->initEnvironment(state, CurTime);
2944 : }
2945 :
2946 0 : this->tempGround = this->groundTempModel->getGroundTempAtTimeInSeconds(state, this->coilDepth, CurTime);
2947 :
2948 0 : this->massFlowRate = PlantUtilities::RegulateCondenserCompFlowReqOp(state, this->plantLoc, this->designMassFlow);
2949 :
2950 0 : PlantUtilities::SetComponentFlowRate(state, this->massFlowRate, this->inletNodeNum, this->outletNodeNum, this->plantLoc);
2951 :
2952 : // Reset local environment init flag
2953 0 : if (!state.dataGlobal->BeginEnvrnFlag) {
2954 0 : this->myEnvrnFlag = true;
2955 : }
2956 0 : }
2957 :
2958 : //******************************************************************************
2959 :
2960 0 : void GLHESlinky::initEnvironment(EnergyPlusData &state, Real64 const CurTime)
2961 : {
2962 :
2963 0 : std::string_view const RoutineName = "initEnvironment";
2964 0 : this->myEnvrnFlag = false;
2965 :
2966 0 : Real64 fluidDensity = state.dataPlnt->PlantLoop(this->plantLoc.loopNum).glycol->getDensity(state, 20.0, RoutineName);
2967 0 : this->designMassFlow = this->designFlow * fluidDensity;
2968 0 : PlantUtilities::InitComponentNodes(state, 0.0, this->designMassFlow, this->inletNodeNum, this->outletNodeNum);
2969 :
2970 0 : this->lastQnSubHr = 0.0;
2971 0 : state.dataLoopNodes->Node(this->inletNodeNum).Temp = this->groundTempModel->getGroundTempAtTimeInSeconds(state, this->coilDepth, CurTime);
2972 0 : state.dataLoopNodes->Node(this->outletNodeNum).Temp = this->groundTempModel->getGroundTempAtTimeInSeconds(state, this->coilDepth, CurTime);
2973 :
2974 : // zero out all history arrays
2975 0 : this->QnHr = 0.0;
2976 0 : this->QnMonthlyAgg = 0.0;
2977 0 : this->QnSubHr = 0.0;
2978 0 : this->LastHourN = 0;
2979 0 : state.dataGroundHeatExchanger->prevTimeSteps = 0.0;
2980 0 : state.dataGroundHeatExchanger->currentSimTime = 0.0;
2981 0 : this->QGLHE = 0.0;
2982 0 : this->prevHour = 1;
2983 0 : }
2984 :
2985 : //******************************************************************************
2986 :
2987 0 : void GLHESlinky::oneTimeInit_new(EnergyPlusData &state)
2988 : {
2989 : // Locate the hx on the plant loops for later usage
2990 0 : bool errFlag = false;
2991 0 : PlantUtilities::ScanPlantLoopsForObject(
2992 0 : state, this->name, DataPlant::PlantEquipmentType::GrndHtExchgSlinky, this->plantLoc, errFlag, _, _, _, _, _);
2993 0 : if (errFlag) {
2994 0 : ShowFatalError(state, "initGLHESimVars: Program terminated due to previous condition(s).");
2995 : }
2996 0 : }
2997 0 : void GLHESlinky::oneTimeInit([[maybe_unused]] EnergyPlusData &state)
2998 : {
2999 0 : }
3000 :
3001 : //******************************************************************************
3002 :
3003 : } // namespace EnergyPlus::GroundHeatExchangers
|