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