geoloop 0.0.1__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. geoloop/axisym/AxisymetricEL.py +751 -0
  2. geoloop/axisym/__init__.py +3 -0
  3. geoloop/bin/Flowdatamain.py +89 -0
  4. geoloop/bin/Lithologymain.py +84 -0
  5. geoloop/bin/Loadprofilemain.py +100 -0
  6. geoloop/bin/Plotmain.py +250 -0
  7. geoloop/bin/Runbatch.py +81 -0
  8. geoloop/bin/Runmain.py +86 -0
  9. geoloop/bin/SingleRunSim.py +928 -0
  10. geoloop/bin/__init__.py +3 -0
  11. geoloop/cli/__init__.py +0 -0
  12. geoloop/cli/batch.py +106 -0
  13. geoloop/cli/main.py +105 -0
  14. geoloop/configuration.py +946 -0
  15. geoloop/constants.py +112 -0
  16. geoloop/geoloopcore/CoaxialPipe.py +503 -0
  17. geoloop/geoloopcore/CustomPipe.py +727 -0
  18. geoloop/geoloopcore/__init__.py +3 -0
  19. geoloop/geoloopcore/b2g.py +739 -0
  20. geoloop/geoloopcore/b2g_ana.py +516 -0
  21. geoloop/geoloopcore/boreholedesign.py +683 -0
  22. geoloop/geoloopcore/getloaddata.py +112 -0
  23. geoloop/geoloopcore/pyg_ana.py +280 -0
  24. geoloop/geoloopcore/pygfield_ana.py +519 -0
  25. geoloop/geoloopcore/simulationparameters.py +130 -0
  26. geoloop/geoloopcore/soilproperties.py +152 -0
  27. geoloop/geoloopcore/strat_interpolator.py +194 -0
  28. geoloop/lithology/__init__.py +3 -0
  29. geoloop/lithology/plot_lithology.py +277 -0
  30. geoloop/lithology/process_lithology.py +695 -0
  31. geoloop/loadflowdata/__init__.py +3 -0
  32. geoloop/loadflowdata/flow_data.py +161 -0
  33. geoloop/loadflowdata/loadprofile.py +325 -0
  34. geoloop/plotting/__init__.py +3 -0
  35. geoloop/plotting/create_plots.py +1142 -0
  36. geoloop/plotting/load_data.py +432 -0
  37. geoloop/utils/RunManager.py +164 -0
  38. geoloop/utils/__init__.py +0 -0
  39. geoloop/utils/helpers.py +841 -0
  40. geoloop-1.0.0.dist-info/METADATA +120 -0
  41. geoloop-1.0.0.dist-info/RECORD +46 -0
  42. geoloop-1.0.0.dist-info/entry_points.txt +2 -0
  43. geoloop-0.0.1.dist-info/licenses/LICENSE → geoloop-1.0.0.dist-info/licenses/LICENSE.md +2 -1
  44. geoloop-0.0.1.dist-info/METADATA +0 -10
  45. geoloop-0.0.1.dist-info/RECORD +0 -6
  46. {geoloop-0.0.1.dist-info → geoloop-1.0.0.dist-info}/WHEEL +0 -0
  47. {geoloop-0.0.1.dist-info → geoloop-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,516 @@
1
+ import numpy as np
2
+ import pygfunction as gt
3
+ from scipy.constants import pi
4
+
5
+ from geoloop.geoloopcore.CustomPipe import CustomPipe
6
+ from geoloop.geoloopcore.simulationparameters import SimulationParameters
7
+ from geoloop.geoloopcore.soilproperties import SoilProperties
8
+
9
+
10
+ class B2G_ana:
11
+ """
12
+ Class for analytical / semi-analytical borehole-to-ground simulation.
13
+
14
+ This class uses pygfunction's g-function and a load aggregation scheme
15
+ (Claesson-Javed) to compute borehole wall temperatures and then obtains
16
+ fluid/pipe temperatures from `CustomPipe` methods.
17
+
18
+ Attributes
19
+ ----------
20
+ custom_pipe : CustomPipe
21
+ Depth-dependent borehole configuration and properties.
22
+ soil_props : SoilProperties
23
+ Depth-dependent soil parameters (conductivity and temperature).
24
+ sim_params : SimulationParameters
25
+ Simulation parameters (time, flow, power, temperature).
26
+
27
+ Notes
28
+ -----
29
+ The simulation can be run for input power or input temperature and calls the appropriate function.
30
+ `runsimulation_power` is called for power simulations
31
+ `runsimulation_temperature` is called for temperature simulations
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ custom_pipe: CustomPipe,
37
+ soil_properties: SoilProperties,
38
+ simulation_parameters: SimulationParameters,
39
+ ) -> None:
40
+ """
41
+ Initialize the analytical BHE model wrapper.
42
+
43
+ Parameters
44
+ ----------
45
+ custom_pipe : CustomPipe
46
+ Pipe/borehole configuration object.
47
+ soil_properties : SoilProperties
48
+ Soil properties provider.
49
+ simulation_parameters : SimulationParameters
50
+ Operational and simulation parameters.
51
+ """
52
+ self.custom_pipe = custom_pipe
53
+ self.soil_props = soil_properties
54
+ self.sim_params = simulation_parameters
55
+
56
+ def runsimulation(self):
57
+ """
58
+ Run the simulation of the borehole to ground heat exchanger.
59
+
60
+ Returns
61
+ -------
62
+ tuple
63
+ (hours, Q_b, flowrate, qsign, T_fi, T_fo, T_bave,
64
+ z, zseg, T_b, T_ftimes, qbzseg, h_fpipes)
65
+ """
66
+ if self.sim_params.run_type == SimulationParameters.POWER:
67
+ return self.runsimulation_power()
68
+ else:
69
+ return self.runsimulation_temperature()
70
+
71
+ def runsimulation_temperature(self) -> tuple:
72
+ """
73
+ Run the simulation of the borehole to ground heat exchanger for an input inlet temperature.
74
+
75
+ The method:
76
+ - computes the segment-wise g-function scaling,
77
+ - uses load-aggregation to obtain borehole wall temperature response,
78
+ - calls CustomPipe methods to compute fluid temperatures for each time step.
79
+
80
+ Returns
81
+ -------
82
+ tuple
83
+ (hours, Q_b, flowrate, qsign, T_fi, T_fo, T_bave,
84
+ z, zseg, T_b, T_ftimes, qbzseg, h_fpipes)
85
+ """
86
+ sim_params = self.sim_params
87
+ custom_pipe = self.custom_pipe
88
+ soil_props = self.soil_props
89
+ nsegments = sim_params.nsegments
90
+
91
+ # temporal arrays and derived geometry
92
+ H = custom_pipe.b.H
93
+ D = custom_pipe.b.D
94
+ Nt = sim_params.Nt
95
+ time = sim_params.time
96
+ m_flow = sim_params.m_flow[0]
97
+ scaleflow = sim_params.m_flow / m_flow
98
+
99
+ dz = H / nsegments
100
+ zmin = D + 0.5 * dz
101
+ zmax = D + H - 0.5 * dz
102
+ zseg = np.linspace(zmin, zmax, nsegments)
103
+ zz = np.linspace(D, D + H, nsegments + 1)
104
+
105
+ k_s = soil_props.get_k_s(zz[0:-1], zz[1:], sim_params.isample)
106
+ z = np.linspace(D, D + H, nsegments + 1)
107
+
108
+ # Build load aggregation + g-function once
109
+ LoadAgg = []
110
+ for k in range(nsegments):
111
+ la = gt.load_aggregation.ClaessonJaved(sim_params.dt, sim_params.tmax)
112
+ LoadAgg.append(la)
113
+
114
+ # The field contains only one borehole
115
+ boreField = [gt.boreholes.Borehole(custom_pipe.b.H, custom_pipe.b.D, custom_pipe.b.r_b, x=0., y=0.)]
116
+
117
+ # Get time values needed for g-function evaluation
118
+ time_req = LoadAgg[0].get_times_for_simulation()
119
+
120
+ # Calculate g-function
121
+ # g-Function calculation options
122
+ options = {"nSegments": 8, "disp": False}
123
+ np.seterr(under="ignore")
124
+
125
+ alpha = soil_props.alfa
126
+ gFunc = gt.gfunction.gFunction(
127
+ boreField, alpha, time=time_req, options=options, method="similarities"
128
+ )
129
+
130
+ # Initialize load aggregation scheme
131
+ for k in range(nsegments):
132
+ LoadAgg[k].initialize(gFunc.gFunc / (2 * pi * k_s[k]))
133
+
134
+ # Protect agains zero loads
135
+ Qabsmin = H * 0.1 # assume at least 0.1 W /m to avoid division by zero
136
+
137
+ # Compute deltaT_b by temporal superposition for each segment/time
138
+ deltaT_b = np.zeros(Nt)
139
+ deltaT_bk = np.zeros((Nt, nsegments))
140
+ power = np.zeros(Nt)
141
+ Q_b = sim_params.Q
142
+
143
+ # First pass: compute deltaT_b using aggregated loads
144
+ for i, (t, Q_b_i) in enumerate(zip(time, Q_b)):
145
+ # avoid the Q_B_i to be zero, it is at least the 10% of H in watts
146
+ if abs(Q_b_i) < Qabsmin:
147
+ Q_b_i = Qabsmin * np.sign(Q_b_i)
148
+
149
+ # Increment time step by (1)
150
+ for k in range(nsegments):
151
+ LoadAgg[k].next_time_step(t)
152
+ # Apply current load
153
+ LoadAgg[k].set_current_load(Q_b_i / H)
154
+
155
+ # Evaluate the average borehole wall temeprature
156
+ deltaT_b[i] = 0
157
+ for k in range(nsegments):
158
+ deltaT_bk[i, k] = LoadAgg[k].temporal_superposition()
159
+
160
+ deltaT_b[i] = np.average(deltaT_bk[i, :])
161
+ power[i] = Q_b_i
162
+
163
+ # Initialize output containers
164
+ T_b_top = np.zeros(Nt)
165
+ T_bave = np.zeros(Nt)
166
+ T_fi = np.zeros(Nt)
167
+ T_fo = np.zeros(Nt)
168
+
169
+ Q_b = np.zeros(Nt)
170
+ deltaT_b_ref = np.zeros(Nt)
171
+ flowrate = np.zeros(Nt)
172
+ Rs = np.zeros(Nt)
173
+ qsign = np.zeros(Nt)
174
+
175
+ # Initial guess for Rs (borehole thermal resistance) per time step
176
+ imax = -1
177
+ for i, t in enumerate(time):
178
+ if (i > imax) and (i < Nt):
179
+ p = power[i]
180
+ Rs[i] = deltaT_b[i] / (p / H)
181
+
182
+ minR = 0.01
183
+ Rs = np.where(np.logical_and(Rs < minR, Rs > -0.5), minR, Rs)
184
+
185
+ # iterate to refine Rsz using qbzseg from pipe model (few iterations)
186
+ iter = 0
187
+ niter = 3
188
+ Rsz = np.zeros((len(Rs), nsegments))
189
+ qbzseg = np.zeros((len(Rs), nsegments))
190
+ T_b = np.zeros((len(Rs), nsegments))
191
+ h_fpipes = np.zeros((len(Rs), custom_pipe.nPipes))
192
+ T_ftimes = np.zeros((len(Rs), nsegments + 1, custom_pipe.nPipes))
193
+
194
+ while iter < niter:
195
+ if iter == 0:
196
+ for i, rsi in enumerate(Rs):
197
+ Rsz[i] = rsi * np.ones(nsegments)
198
+
199
+ else:
200
+ # Recompute g-function/loadaggs (keeps same options)
201
+ alpha = soil_props.alfa
202
+ gFunc = gt.gfunction.gFunction(
203
+ boreField,
204
+ alpha,
205
+ time=time_req,
206
+ options=options,
207
+ method="similarities",
208
+ )
209
+
210
+ # Reset load aggregation
211
+ for k in range(nsegments):
212
+ LoadAgg[k] = gt.load_aggregation.ClaessonJaved(
213
+ sim_params.dt, sim_params.tmax
214
+ )
215
+ # Initialize load aggregation scheme
216
+ LoadAgg[k].initialize(gFunc.gFunc / (2 * pi * k_s[k]))
217
+
218
+ Rsz = np.zeros((len(Rs), nsegments))
219
+ for k in range(nsegments):
220
+ # this only works if time is same as reduced time array
221
+ for i, (t, Q_b_i) in enumerate(zip(time, qbzseg[:, k])):
222
+ # Increment time step by (1)
223
+ LoadAgg[k].next_time_step(t)
224
+
225
+ if abs(Q_b_i * H) < Qabsmin:
226
+ Q_b_i = Qabsmin * np.sign(Q_b_i) / H
227
+
228
+ # Apply current load
229
+ LoadAgg[k].set_current_load(Q_b_i)
230
+
231
+ # Evaluate borehole wall temeprature
232
+ deltaT_bk[i][k] = LoadAgg[k].temporal_superposition()
233
+ Rsz[i, k] = deltaT_bk[i][k] / qbzseg[i, k]
234
+
235
+ # For each time-step compute fluid solution using CustomPipe
236
+ iter += 1
237
+ imax = -1
238
+ for i, t in enumerate(time):
239
+ if (i > imax) and (i < Nt):
240
+ p = power[i]
241
+ custom_pipe.update_scaleflow(scaleflow[i])
242
+ h_f = custom_pipe.h_f
243
+ signpower = np.sign(p)
244
+ T_f_in = sim_params.Tin[i]
245
+
246
+ T_f_out, p, Reff, T_f, Tb, qbz = (
247
+ custom_pipe.get_temperature_depthvar(
248
+ T_f_in,
249
+ signpower,
250
+ Rsz[i],
251
+ soil_props=soil_props,
252
+ nsegments=nsegments,
253
+ )
254
+ )
255
+
256
+ Q_b[i] = p
257
+ T_b_top[i] = Tb[0]
258
+ deltaT_b_ref[i] = deltaT_b[i]
259
+ T_bave[i] = np.average(Tb)
260
+ T_fi[i] = T_f_in
261
+ h_fpipes[i] = h_f
262
+ T_fo[i] = T_f_out
263
+ flowrate[i] = scaleflow[i] * m_flow
264
+ T_ftimes[i] = T_f # stored index time, depth, pipe
265
+ T_b[i] = Tb
266
+ qbzseg[i] = qbz
267
+ qsign[i] = np.sign(max(qbz) * min(qbz))
268
+ imax = i
269
+
270
+ hours = time / 3600.0
271
+
272
+ return (
273
+ hours,
274
+ Q_b,
275
+ flowrate,
276
+ qsign,
277
+ T_fi,
278
+ T_fo,
279
+ T_bave,
280
+ z,
281
+ zseg,
282
+ T_b,
283
+ T_ftimes,
284
+ qbzseg,
285
+ h_fpipes,
286
+ )
287
+
288
+ def runsimulation_power(self) -> tuple:
289
+ """
290
+ Run the simulation of the borehole to ground heat exchanger for an input heat demand.
291
+
292
+ Returns
293
+ -------
294
+ tuple
295
+ (hours, Q_b, flowrate, qsign, T_fi, T_fo, T_bave,
296
+ z, zseg, T_b, T_ftimes, qbzseg, h_fpipes)
297
+ """
298
+ sim_params = self.sim_params
299
+ custom_pipe = self.custom_pipe
300
+ soil_props = self.soil_props
301
+ nsegments = sim_params.nsegments
302
+
303
+ # geomertry and derived arrays
304
+ H = custom_pipe.b.H
305
+ D = custom_pipe.b.D
306
+ Nt = sim_params.Nt
307
+ time = sim_params.time
308
+ m_flow = sim_params.m_flow[0]
309
+ scaleflow = sim_params.m_flow / m_flow
310
+
311
+ dz = H / nsegments
312
+ zmin = D + 0.5 * dz
313
+ zmax = D + H - 0.5 * dz
314
+ zseg = np.linspace(zmin, zmax, nsegments)
315
+ zz = np.linspace(D, D + H, nsegments + 1)
316
+
317
+ k_s = soil_props.get_k_s(zz[0:-1], zz[1:], sim_params.isample)
318
+ z = np.linspace(D, D + H, nsegments + 1)
319
+
320
+ # prepare load aggregation + gfunc
321
+ # The field contains only one borehole
322
+ old = False
323
+ if old:
324
+ boreField = [
325
+ gt.boreholes.Borehole(
326
+ custom_pipe.b.H, custom_pipe.b.D, custom_pipe.b.r_b, x=0.0, y=0.0
327
+ )
328
+ ]
329
+ # The field contains only one borehole, but needs one extra at very large distance to be correct, ie. gfunc plateaus at 6.7)
330
+ else:
331
+ boreField = [
332
+ gt.boreholes.Borehole(
333
+ custom_pipe.b.H,
334
+ custom_pipe.b.D,
335
+ custom_pipe.b.r_b,
336
+ x=0.0,
337
+ y=0.0,
338
+ tilt=np.radians(0.1),
339
+ ),
340
+ gt.boreholes.Borehole(
341
+ custom_pipe.b.H,
342
+ custom_pipe.b.D,
343
+ custom_pipe.b.r_b,
344
+ x=1000.0,
345
+ y=0.0,
346
+ tilt=np.radians(0.1),
347
+ ),
348
+ ]
349
+
350
+ LoadAgg = []
351
+ for k in range(nsegments):
352
+ la = gt.load_aggregation.ClaessonJaved(sim_params.dt, sim_params.tmax)
353
+ LoadAgg.append(la)
354
+
355
+ # Get time values needed for g-function evaluation
356
+ time_req = LoadAgg[0].get_times_for_simulation()
357
+
358
+ # Calculate g-function
359
+ # g-Function calculation options
360
+ options = {"nSegments": 8, "disp": False}
361
+ np.seterr(under="ignore")
362
+
363
+ alpha = soil_props.alfa
364
+ gFunc = gt.gfunction.gFunction(
365
+ boreField, alpha, time=time_req, options=options, method="similarities"
366
+ )
367
+
368
+ # initialize load aggregation scheme
369
+ for k in range(nsegments):
370
+ LoadAgg[k].initialize(gFunc.gFunc / (2 * pi * k_s[k]))
371
+
372
+ Qabsmin = H * 0.1 # assume at least 0.1 W /m to avoid division by zero
373
+
374
+ deltaT_b = np.zeros(Nt)
375
+ deltaT_bk = np.zeros((Nt, nsegments))
376
+ power = np.zeros(Nt)
377
+ Q_b = sim_params.Q
378
+
379
+ for i, (t, Q_b_i) in enumerate(zip(time, Q_b)):
380
+ # avoid the Q_B_i to be zero, it is at least the 10% of H in watts
381
+ if abs(Q_b_i) < Qabsmin:
382
+ Q_b_i = Qabsmin * np.sign(Q_b_i)
383
+
384
+ # Increment time step by (1)
385
+ for k in range(nsegments):
386
+ LoadAgg[k].next_time_step(t)
387
+ # Apply current load
388
+ LoadAgg[k].set_current_load(Q_b_i / H)
389
+
390
+ # Evaluate the average borehole wall temeprature
391
+ deltaT_b[i] = 0
392
+ for k in range(nsegments):
393
+ deltaT_bk[i, k] = LoadAgg[k].temporal_superposition()
394
+
395
+ deltaT_b[i] = np.average(deltaT_bk[i, :])
396
+ power[i] = Q_b_i
397
+
398
+ T_b_top = np.zeros(Nt)
399
+ T_bave = np.zeros(Nt)
400
+ T_fi = np.zeros(Nt)
401
+ T_fo = np.zeros(Nt)
402
+
403
+ Q_b = np.zeros(Nt)
404
+ deltaT_b_ref = np.zeros(Nt)
405
+ flowrate = np.zeros(Nt)
406
+ Rs = np.zeros(Nt)
407
+ qsign = np.zeros(Nt)
408
+
409
+ # compute initial Rs
410
+ imax = -1
411
+ for i, t in enumerate(time):
412
+ if (i > imax) and (i < Nt):
413
+ power_i = power[i]
414
+ p = np.maximum(power_i, 100)
415
+ Rs[i] = deltaT_b[i] / (p / H)
416
+
417
+ minR = 0.01
418
+ Rs = np.where(np.logical_and(Rs < minR, Rs > -0.5), minR, Rs)
419
+
420
+ # iterative refinement
421
+ iter = 0
422
+ niter = 3
423
+ Rsz = np.zeros((len(Rs), nsegments))
424
+ qbzseg = np.zeros((len(Rs), nsegments))
425
+ T_b = np.zeros((len(Rs), nsegments))
426
+ h_fpipes = np.zeros((len(Rs), custom_pipe.nPipes))
427
+ T_ftimes = np.zeros((len(Rs), nsegments + 1, custom_pipe.nPipes))
428
+
429
+ while iter < niter:
430
+ if iter == 0:
431
+ for i, rsi in enumerate(Rs):
432
+ Rsz[i] = rsi * np.ones(nsegments)
433
+
434
+ else:
435
+ # reinitilize g-function
436
+ alpha = soil_props.alfa
437
+ gFunc = gt.gfunction.gFunction(
438
+ boreField,
439
+ alpha,
440
+ time=time_req,
441
+ options=options,
442
+ method="similarities",
443
+ )
444
+
445
+ # Reinitialize load aggregation scheme
446
+ for k in range(nsegments):
447
+ LoadAgg[k] = gt.load_aggregation.ClaessonJaved(
448
+ sim_params.dt, sim_params.tmax
449
+ )
450
+ LoadAgg[k].initialize(gFunc.gFunc / (2 * pi * k_s[k]))
451
+
452
+ Rsz = np.zeros((len(Rs), nsegments))
453
+ for k in range(nsegments):
454
+ # this only works if time is same as reduced time array
455
+ for i, (t, Q_b_i) in enumerate(zip(time, qbzseg[:, k])):
456
+ # Increment time step by (1)
457
+ LoadAgg[k].next_time_step(t)
458
+
459
+ if abs(Q_b_i * H) < Qabsmin:
460
+ Q_b_i = Qabsmin * np.sign(Q_b_i) / H
461
+
462
+ # Apply current load
463
+ LoadAgg[k].set_current_load(Q_b_i)
464
+
465
+ # Evaluate borehole wall temeprature
466
+ deltaT_bk[i][k] = LoadAgg[k].temporal_superposition()
467
+ Rsz[i, k] = deltaT_bk[i][k] / qbzseg[i, k]
468
+ iter += 1
469
+ imax = -1
470
+
471
+ # compute fluid temperatures for this iteration
472
+ for i, t in enumerate(time):
473
+ if (i > imax) and (i < Nt):
474
+ # print(il)
475
+ power_i = power[i]
476
+ custom_pipe.update_scaleflow(scaleflow[i])
477
+ h_f = custom_pipe.h_f
478
+
479
+ (T_f_out, T_f_in, Reff, T_f, Tb, qbz) = (
480
+ custom_pipe.get_temperature_depthvar_power(
481
+ power_i, Rsz[i], soil_props, nsegments=nsegments
482
+ )
483
+ )
484
+
485
+ Q_b[i] = power[i]
486
+ T_b_top[i] = Tb[0]
487
+ deltaT_b_ref[i] = deltaT_b[i]
488
+ T_bave[i] = np.average(Tb)
489
+ T_fi[i] = T_f_in
490
+ h_fpipes[i] = h_f
491
+ T_fo[i] = T_f_out
492
+ flowrate[i] = scaleflow[i] * m_flow
493
+ T_ftimes[i] = T_f # stored index time, depth, pipe
494
+ T_b[i] = Tb
495
+ # qbz is calculated at the depth division in segments (zseg)
496
+ qbzseg[i] = qbz
497
+ qsign[i] = np.sign(max(qbz) * min(qbz))
498
+ imax = i
499
+
500
+ hours = time / 3600.0
501
+
502
+ return (
503
+ hours,
504
+ Q_b,
505
+ flowrate,
506
+ qsign,
507
+ T_fi,
508
+ T_fo,
509
+ T_bave,
510
+ z,
511
+ zseg,
512
+ T_b,
513
+ T_ftimes,
514
+ qbzseg,
515
+ h_fpipes,
516
+ )