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,1142 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ import matplotlib.animation as animation
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ import pandas as pd
8
+ import pygfunction as gt
9
+ import seaborn as sns
10
+ import xarray as xr
11
+ from PIL import Image
12
+
13
+ from geoloop.bin.SingleRunSim import SingleRun
14
+ from geoloop.constants import format_dict, units_dict
15
+ from geoloop.geoloopcore.strat_interpolator import StratInterpolator
16
+ from geoloop.utils.helpers import get_param_names
17
+
18
+
19
+ class PlotResults:
20
+ """
21
+ A class with functions for generating plots of simulation results. Different plots can be created for different
22
+ types of simulations and models (e.g. single simulations, stochastic simulation, FINVOL model, ANALYTICAL model).
23
+
24
+ """
25
+
26
+ @staticmethod
27
+ def create_scatterplots(
28
+ results_df: pd.DataFrame,
29
+ params_df: pd.DataFrame,
30
+ y_variable: str,
31
+ out_path: Path,
32
+ ) -> None:
33
+ """
34
+ Only compatible with results and inputs of stochastic (MC) simulations.
35
+ Generates and saves scatter plots for all parameters in the simulations results, with `y_variable` on the y-axis.
36
+ The parameters in the list of 'y_variable' are plotted against all other parameters in the results and input
37
+ dataframes.
38
+
39
+ Parameters
40
+ ----------
41
+ results_df : pd.DataFrame
42
+ DataFrame containing simulation result parameters as columns.
43
+ params_df : pd.DataFrame
44
+ DataFrame containing simulation input parameters as columns.
45
+ y_variable : str
46
+ The variable parameter to be plotted on the y-axis.
47
+ out_path : Path
48
+ Path to the directory where the plots will be saved.
49
+
50
+ Raises
51
+ ------
52
+ ValueError
53
+ If `y_variable` is not found in either `results_df` or `param_df`.
54
+
55
+ Returns
56
+ -------
57
+ None
58
+ """
59
+ if y_variable in results_df.columns:
60
+ y_data = results_df
61
+ elif y_variable in params_df.columns:
62
+ y_data = params_df
63
+ else:
64
+ raise ValueError(
65
+ f"{y_variable} not found in either results_df or param_df."
66
+ )
67
+
68
+ # Get the list of all variables excluding the y_variable
69
+ result_variables = [
70
+ "Q_b",
71
+ "flowrate",
72
+ "T_fi",
73
+ "T_fo",
74
+ "T_bave",
75
+ "dploop",
76
+ "qloop",
77
+ "T_b",
78
+ "qzb",
79
+ "T_f",
80
+ "Re_in",
81
+ "Re_out",
82
+ "k_s_res",
83
+ "k_g_res",
84
+ ]
85
+ variable_param_names, locked_param_names = get_param_names()
86
+
87
+ x_variables = result_variables + variable_param_names
88
+
89
+ # Loop through variables and create scatterplots
90
+ for x_variable in x_variables:
91
+ if x_variable != y_variable and x_variable in results_df.columns:
92
+ # only plot the variables that are truly variable (stdev != 0)
93
+ if results_df[x_variable].std().round(7) != 0:
94
+ title = f"{format_dict[x_variable]} ({units_dict[x_variable]}) after {results_df.loc[0, 'hours']} hours production vs. {format_dict[y_variable]} ({units_dict[y_variable]})"
95
+ g = sns.JointGrid(
96
+ results_df, x=x_variable, y=y_data[y_variable], hue="file_name"
97
+ )
98
+ g.plot_joint(
99
+ sns.scatterplot, alpha=0.7, edgecolor=".2", linewidth=0.5
100
+ )
101
+ g.plot_marginals(
102
+ sns.boxplot,
103
+ linewidth=0.5,
104
+ linecolor=".2",
105
+ boxprops=dict(alpha=0.9),
106
+ )
107
+ sns.set_style("whitegrid")
108
+ g.ax_joint.legend(bbox_to_anchor=(0.63, -0.15))
109
+
110
+ # Reverse y-axis if y_variable is "H"
111
+ if y_variable == "H":
112
+ g.ax_joint.invert_yaxis()
113
+
114
+ # Plot vertical line with turbulent Re value if x_variable is either 'Re_in' or 'Re_out'
115
+ if x_variable in ["Re_in", "Re_out"]:
116
+ g.ax_joint.axvline(
117
+ 4000, color="red", linestyle="--", label="Re turbulent"
118
+ )
119
+ g.ax_joint.legend() # Ensure the legend is updated
120
+ g.set_axis_labels(
121
+ xlabel=f"{format_dict[x_variable]} ({units_dict[x_variable]})",
122
+ ylabel=f"{format_dict[y_variable]} ({units_dict[y_variable]})",
123
+ )
124
+ g.fig.suptitle(title)
125
+ g.fig.tight_layout()
126
+ save_path = out_path.with_name(
127
+ out_path.name + f"_{y_variable}vs{x_variable}_scat.png"
128
+ )
129
+ plt.savefig(save_path)
130
+ plt.close()
131
+
132
+ elif (
133
+ x_variable != y_variable
134
+ and x_variable
135
+ in params_df.select_dtypes(include=["float64", "int64"]).columns
136
+ ):
137
+ if params_df[x_variable].std().round(7) != 0:
138
+ title = f"{format_dict[x_variable]} ({units_dict[x_variable]}) after {params_df.loc[0, 'nyear']} year production vs. {format_dict[y_variable]} ({units_dict[y_variable]})"
139
+ g = sns.JointGrid(
140
+ params_df, x=x_variable, y=y_data[y_variable], hue="file_name"
141
+ )
142
+ g.plot_joint(
143
+ sns.scatterplot, alpha=0.7, edgecolor=".2", linewidth=0.5
144
+ )
145
+ g.plot_marginals(
146
+ sns.boxplot,
147
+ linewidth=0.5,
148
+ linecolor=".2",
149
+ boxprops=dict(alpha=0.9),
150
+ )
151
+ sns.set_style("whitegrid")
152
+ g.ax_joint.legend(bbox_to_anchor=(0.63, -0.15))
153
+
154
+ # Reverse y-axis if y_variable is "H"
155
+ if y_variable == "H":
156
+ g.ax_joint.invert_yaxis()
157
+
158
+ g.set_axis_labels(
159
+ xlabel=f"{format_dict[x_variable]} ({units_dict[x_variable]})",
160
+ ylabel=f"{format_dict[y_variable]} ({units_dict[y_variable]})",
161
+ )
162
+ g.fig.suptitle(title)
163
+ g.fig.tight_layout()
164
+ save_path = out_path.with_name(
165
+ out_path.name + f"_{y_variable}vs{x_variable}_scat.png"
166
+ )
167
+ plt.savefig(save_path)
168
+ plt.close()
169
+
170
+ @staticmethod
171
+ def create_timeseriesplot(
172
+ results_dfs: pd.DataFrame | list[pd.DataFrame],
173
+ out_path: Path,
174
+ plot_parameters: list,
175
+ ):
176
+ """
177
+ Only compatible with (multiple) single simulations.
178
+ Generates and saves a timeseries plot for various simulation results over time. Results of multiple
179
+ single simulations can be plotted together.
180
+
181
+ Plot shows the generated power, flowrate, fluid inlet en outlet temperatures, depth-average borehole wall temperature,
182
+ pumping pressure and required pumping power over time.
183
+
184
+ Parameters
185
+ ----------
186
+ results_dfs : Union[pd.DataFrame, List[pd.DataFrame]]
187
+ DataFrame(s) containing simulation results.
188
+ out_path : Path
189
+ Directory where plots are saved.
190
+ plot_parameters : List[str]
191
+ List of variables to plot.
192
+
193
+ Raises
194
+ ------
195
+ ValueError
196
+ If any required parameter is missing from a DataFrame in `results_dfs`.
197
+
198
+ Returns
199
+ -------
200
+ None
201
+ """
202
+ out_path_prefix = out_path.with_name(out_path.name + "_timeplot")
203
+
204
+ # Ensure results_dfs is a list of dataframes
205
+ if isinstance(results_dfs, pd.DataFrame):
206
+ results_dfs = [results_dfs]
207
+
208
+ figsize = (11, 4)
209
+ plt.rcParams.update({"font.size": 12})
210
+
211
+ # Define labels for each parameter
212
+ labels = {}
213
+ for idx, df in enumerate(results_dfs):
214
+ file_name = df["file_name"].iloc[0]
215
+ labels[idx] = {
216
+ "T_fi": f"{file_name}, $T_{{in}}$ [°C]",
217
+ "T_fo": f"{file_name}, $T_{{out}}$ [°C]",
218
+ "T_bave": f"{file_name}, $T_{{b,ave}}$ [°C]",
219
+ "Delta_T": f"{file_name}, Δ Temperature (Tout - Tin)",
220
+ "Q_b": f"{file_name}, Heat Load [W]",
221
+ "dploop": f"{file_name}, Pump Pressure [bar]",
222
+ "qloop": f"{file_name}, Pump Power [W]",
223
+ "flowrate": f"{file_name}, Flowrate [kg/s]",
224
+ "COP": f"{file_name}, Coefficient of Performance",
225
+ }
226
+
227
+ # Create a global color palette for consistency across dataframes
228
+ line_colors = sns.color_palette(
229
+ "colorblind", n_colors=len(results_dfs) * len(plot_parameters) * 3
230
+ )
231
+ color_iter = iter(line_colors)
232
+
233
+ # Check if any dataframe needs COP or Delta_T calculated
234
+ for df in results_dfs:
235
+ if (
236
+ "COP" in plot_parameters
237
+ and "Q_b" in df.columns
238
+ and "qloop" in df.columns
239
+ ):
240
+ df['COP'] = abs(df['Q_b']) / df['qloop']
241
+ if (
242
+ "Delta_T" in plot_parameters
243
+ and "T_fi" in df.columns
244
+ and "T_fo" in df.columns
245
+ ):
246
+ df["Delta_T"] = df["T_fo"] - df["T_fi"]
247
+
248
+ plots = [
249
+ ("Q_b", "Heat Load [W]", "Q_b", None, None),
250
+ ("flowrate", "Flowrate [kg/s]", "flowrate", None, None),
251
+ ("COP", "Coefficient of Performance", "COP", None, None)
252
+ ]
253
+
254
+ plotted_params = []
255
+ extra_artists = []
256
+ # Plot dploop and qloop, even if only one is present
257
+ if "dploop" in plot_parameters or "qloop" in plot_parameters:
258
+ fig, ax = plt.subplots(figsize=figsize)
259
+ ax.set_xlabel(r"$t$ [hours]")
260
+ primary_axis_used = False
261
+
262
+ if "dploop" in plot_parameters:
263
+ ax.set_ylabel("Pump Pressure [bar]")
264
+ for idx, df in enumerate(results_dfs):
265
+ if "dploop" in df.columns:
266
+ sns.lineplot(
267
+ x="time",
268
+ y="dploop",
269
+ data=df,
270
+ ax=ax,
271
+ label=labels[idx]["dploop"],
272
+ color=next(color_iter),
273
+ )
274
+ primary_axis_used = True
275
+ plotted_params.append("dploop")
276
+
277
+ ax.legend(loc=(-0.07, -0.35))
278
+
279
+ for idx, df in enumerate(results_dfs):
280
+ if "qloop" in plot_parameters and "qloop" in df.columns:
281
+
282
+ ax_twin = ax.twinx()
283
+ ax_twin.set_ylabel("Pump Power [W]")
284
+
285
+ color = next(color_iter)
286
+
287
+ # Plot the line
288
+ sns.lineplot(
289
+ x="time",
290
+ y="qloop",
291
+ data=df,
292
+ ax=ax_twin,
293
+ label=labels[idx]["qloop"],
294
+ linestyle="dotted",
295
+ color=color,
296
+ )
297
+
298
+ # Match axis color to line color
299
+ ax_twin.spines["right"].set_color(color)
300
+ ax_twin.yaxis.label.set_color(color)
301
+ ax_twin.tick_params(axis="y", colors=color)
302
+
303
+ plotted_params.append("qploop")
304
+ ax_twin.legend(loc=(0.53, -0.15 - (len(results_dfs) * 0.1)))
305
+ extra_artists.append(ax_twin.legend_)
306
+ if primary_axis_used:
307
+ ax.legend(loc=(-0.07, -0.15 - (len(results_dfs) * 0.1)))
308
+ extra_artists.append(ax.legend_)
309
+
310
+ ax.grid()
311
+ if not primary_axis_used:
312
+ ax.set_yticklabels([])
313
+ ax.set_ylabel("")
314
+ ax.grid(False, axis="y")
315
+ ax_twin.grid(axis="both")
316
+
317
+ file_name = out_path.with_name(
318
+ out_path.name + f"_timeplot_{'_'.join(sorted(set(plotted_params)))}.png"
319
+ )
320
+
321
+ fig.tight_layout()
322
+ plt.savefig(
323
+ file_name,
324
+ dpi=300,
325
+ bbox_extra_artists=(extra_artists),
326
+ bbox_inches="tight",
327
+ )
328
+ plt.close()
329
+
330
+ # Plot T_fi, T_fo, T_bave together, and Delta_T on the secondary axis if applicable
331
+ plotted_params = []
332
+ extra_artists = []
333
+ if (
334
+ any(param in plot_parameters for param in ["T_fi", "T_fo", "T_bave"])
335
+ or "Delta_T" in plot_parameters
336
+ ):
337
+ fig, ax = plt.subplots(figsize=figsize)
338
+ ax.set_xlabel(r"$t$ [hours]")
339
+ primary_axis_used = False
340
+
341
+ if any(param in plot_parameters for param in ["T_fi", "T_fo", "T_bave"]):
342
+ ax.set_ylabel("Temperature [°C]")
343
+ for idx, df in enumerate(results_dfs):
344
+ if "T_fi" in plot_parameters and "T_fi" in df.columns:
345
+ sns.lineplot(
346
+ x="time",
347
+ y="T_fi",
348
+ data=df,
349
+ ax=ax,
350
+ label=labels[idx]["T_fi"],
351
+ color=next(color_iter),
352
+ linestyle="-",
353
+ )
354
+ primary_axis_used = True
355
+ plotted_params.append("T_fi")
356
+ if "T_fo" in plot_parameters and "T_fo" in df.columns:
357
+ sns.lineplot(
358
+ x="time",
359
+ y="T_fo",
360
+ data=df,
361
+ ax=ax,
362
+ label=labels[idx]["T_fo"],
363
+ color=next(color_iter),
364
+ linestyle="--",
365
+ )
366
+ primary_axis_used = True
367
+ plotted_params.append("T_fo")
368
+ if "T_bave" in plot_parameters and "T_bave" in df.columns:
369
+ sns.lineplot(
370
+ x="time",
371
+ y="T_bave",
372
+ data=df,
373
+ ax=ax,
374
+ label=labels[idx]["T_bave"],
375
+ color=next(color_iter),
376
+ linestyle=":",
377
+ )
378
+ primary_axis_used = True
379
+ plotted_params.append("T_bave")
380
+ ax.legend(
381
+ loc="lower left",
382
+ bbox_to_anchor=(
383
+ 0.04,
384
+ 0.02
385
+ - (0.05 * 1.25 * len(plotted_params) / len(results_dfs)),
386
+ ),
387
+ bbox_transform=fig.transFigure,
388
+ )
389
+ extra_artists.append(ax.legend_)
390
+
391
+ if "Delta_T" in plot_parameters:
392
+ color = next(color_iter)
393
+
394
+ ax_twin = ax.twinx()
395
+ ax_twin.set_ylabel("Δ Temperature (Tout - Tin)")
396
+ for idx, df in enumerate(results_dfs):
397
+ sns.lineplot(
398
+ x="time",
399
+ y="Delta_T",
400
+ data=df,
401
+ ax=ax_twin,
402
+ label=labels[idx]["Delta_T"],
403
+ linestyle="dashdot",
404
+ color=color,
405
+ )
406
+ plotted_params.append("Delta_T")
407
+
408
+ # Match axis color to line color
409
+ ax_twin.spines["right"].set_color(color)
410
+ ax_twin.yaxis.label.set_color(color)
411
+ ax_twin.tick_params(axis="y", colors=color)
412
+
413
+ ax_twin.legend(
414
+ loc="lower right",
415
+ bbox_to_anchor=(1.1, -0.25 - (0.08 * len(results_dfs))),
416
+ )
417
+ extra_artists.append(ax_twin.legend_)
418
+ if primary_axis_used:
419
+ ax.legend(
420
+ loc="lower left",
421
+ bbox_to_anchor=(
422
+ 0.03,
423
+ 0.1 - (0.02 * len(plotted_params) / len(results_dfs)),
424
+ ),
425
+ bbox_transform=fig.transFigure,
426
+ )
427
+ extra_artists.append(ax.legend_)
428
+
429
+ ax.grid()
430
+
431
+ if not primary_axis_used:
432
+ ax.set_yticklabels([])
433
+ ax.set_ylabel("")
434
+ ax.grid(False, axis="y")
435
+ ax_twin.grid(axis="both")
436
+
437
+ file_name = out_path.with_name(
438
+ out_path.name
439
+ + "_timeplot_"
440
+ + "_".join(sorted(set(plotted_params)))
441
+ + ".png"
442
+ )
443
+
444
+ fig.tight_layout()
445
+ plt.savefig(
446
+ file_name,
447
+ dpi=300,
448
+ bbox_extra_artists=(extra_artists),
449
+ bbox_inches="tight",
450
+ )
451
+ plt.close()
452
+
453
+ # Plot remaining parameters
454
+ for param, ylabel, filename, secondary_param, secondary_ylabel in plots:
455
+ if param not in plot_parameters:
456
+ continue
457
+
458
+ fig, ax = plt.subplots(figsize=figsize)
459
+ ax.set_xlabel(r"$t$ [hours]")
460
+ ax.set_ylabel(ylabel)
461
+
462
+ for idx, df in enumerate(results_dfs):
463
+ if param in df.columns:
464
+ sns.lineplot(
465
+ x="time",
466
+ y=param,
467
+ data=df,
468
+ ax=ax,
469
+ label=labels[idx][param],
470
+ color=next(color_iter),
471
+ )
472
+ if param=="COP":
473
+ if param == "COP":
474
+ ax.text(
475
+ 0.7, 0.9, # 2% from left and bottom of the axes
476
+ f"mean COP: {df['COP'].mean():.2f}",
477
+ transform=ax.transAxes,
478
+ bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.7),
479
+ fontsize=12
480
+ )
481
+
482
+ ax.legend(loc=(-0.07, -0.17 - (0.08 * len(results_dfs))))
483
+ ax.grid()
484
+ fig.tight_layout()
485
+ plt.savefig(f"{out_path_prefix}_{filename}.png", dpi=300)
486
+ plt.close()
487
+
488
+ @staticmethod
489
+ def create_depthplot(
490
+ plot_parameters: list,
491
+ times: list,
492
+ params_ds: xr.Dataset | list[xr.Dataset],
493
+ results_ds: xr.Dataset | list[xr.Dataset],
494
+ out_path: Path,
495
+ ):
496
+ """
497
+ Only compatible with (multiple) single simulations.
498
+ Generates and saves depth-profiles at different timesteps in the simulation results, for fluid temperatures,
499
+ borehole wall temperature, subsurface temperature, subsurface heat flow and subsurface bulk thermal conductivity.
500
+
501
+ Parameters
502
+ ----------
503
+ plot_parameters : List[str]
504
+ List of parameters to plot.
505
+ times : List[float]
506
+ Simulation timesteps for which profiles are plotted.
507
+ params_ds : Union[xr.Dataset, List[xr.Dataset]]
508
+ Dataset(s) containing simulation input parameters.
509
+ results_ds : Union[xr.Dataset, List[xr.Dataset]]
510
+ Dataset(s) containing simulation results.
511
+ out_path : Path
512
+ Directory to save plots.
513
+
514
+ Raises
515
+ ------
516
+ ValueError
517
+ If any required parameter is missing from a DataFrame in `results_ds` or `params_ds`.
518
+
519
+ Returns
520
+ -------
521
+ None
522
+ """
523
+ method = "nearest"
524
+ ax_colors = sns.color_palette("pastel", n_colors=10)
525
+ axtwin_colors = sns.color_palette("dark", n_colors=10)
526
+ plt.rcParams.update(
527
+ {"font.size": 16, "xtick.labelsize": 16, "ytick.labelsize": 16}
528
+ )
529
+
530
+ if not isinstance(params_ds, list):
531
+ params_ds = [params_ds]
532
+ results_ds = [results_ds]
533
+
534
+ for time in times:
535
+ fig1, ax1 = plt.subplots(
536
+ figsize=(8, 10)
537
+ ) # First plot: Temperature profiles
538
+ fig2, ax2 = plt.subplots(
539
+ figsize=(8, 10)
540
+ ) # Second plot: Heat flow & conductivity
541
+ ax2twin = ax2.twiny()
542
+
543
+ ax1.set_xlabel("Temperature [°C]")
544
+ ax1.set_ylabel("Depth from borehole head [m]")
545
+ ax1.grid()
546
+
547
+ ax2.set_xlabel("Heat flow [W/m]")
548
+ ax2.set_ylabel("Depth from borehole head [m]")
549
+ ax2.grid()
550
+ ax2twin.set_xlabel("Thermal conductivity [W/mK]")
551
+
552
+ color_iter_ax2 = iter(ax_colors)
553
+ color_iter_ax2twin = iter(axtwin_colors)
554
+
555
+ for param, results in zip(params_ds, results_ds):
556
+ file_name = str(
557
+ param["file_name"]
558
+ .isel(nPipes=0, layer_k_s=0, layer_k_g=0, layer_Tg=0)
559
+ .item()
560
+ )
561
+
562
+ ptemp = results["T_f"].sel(time=time, method=method)
563
+ Tb = results["T_b"].sel(time=time, method=method)
564
+ Tg = results.get("Tg", None)
565
+ qzb = results["qzb"].sel(time=time, method=method)
566
+ k_s = results["k_s"].values
567
+ zp = -results["z"]
568
+ zseg = -results["zseg"]
569
+
570
+ # Check if the parameter is in the list before plotting
571
+ if plot_parameters == False:
572
+ plot_parameters = ["T_f", "Tg", "T_b"]
573
+ else:
574
+ plot_parameters = plot_parameters
575
+
576
+ model_type = param["model_type"].item()
577
+ if "T_f" in plot_parameters:
578
+ for i in range(len(ptemp[0])):
579
+ ax1.plot(
580
+ ptemp[:, i], zp, label=f"{file_name}: Fluid temp Pipe {i}"
581
+ )
582
+ deltaT = ptemp[:, -1] - ptemp[:, 0]
583
+ if "Delta_T" in plot_parameters:
584
+ ax1twin = ax1.twiny()
585
+ (deltaT_line,) = ax1twin.plot(deltaT, zp, label="Delta T")
586
+
587
+ # Get the color of the Delta T line
588
+ deltaT_color = deltaT_line.get_color()
589
+
590
+ # Set axis label and tick color to match the line
591
+ ax1twin.set_xlabel(
592
+ "Temperature diff. outlet-inlet [\u00b0C]", color=deltaT_color
593
+ )
594
+ ax1twin.tick_params(axis="x", colors=deltaT_color)
595
+ ax1twin.spines["top"].set_color(
596
+ deltaT_color
597
+ ) # For the twiny axis, 'top' is used
598
+
599
+ if "T_b" in plot_parameters and len(Tb) == len(zseg):
600
+ if model_type in ["ANALYTICAL", "FINVOL"]:
601
+ ax1.plot(Tb, zseg, "k--", label=f"{file_name}: $T_b$")
602
+ elif model_type in ["PYG", "PYGFIELD"]:
603
+ Tb_plot = Tb * np.ones(len(zp))
604
+ ax1.plot(Tb_plot, zp, "k--", label=f"{file_name}: $T_b$")
605
+
606
+ if "Tg" in plot_parameters and Tg is not None and len(Tg) == len(zseg):
607
+ if model_type in ["ANALYTICAL", "FINVOL"]:
608
+ ax1.plot(
609
+ Tg, zseg, ":", label=f"{file_name}: $T_g$", color="red"
610
+ )
611
+ elif model_type in ["PYG", "PYGFIELD"]:
612
+ Tg_plot = Tg * np.ones(len(zp))
613
+ ax1.plot(
614
+ Tg_plot, zp, ":", label=f"{file_name}: $T_g$", color="red"
615
+ )
616
+
617
+ dz = zp[1] - zp[0]
618
+ if model_type in ["ANALYTICAL"]:
619
+ qbzm = qzb / dz
620
+ ax2.plot(
621
+ qbzm,
622
+ zseg,
623
+ label=f"{file_name}: Heat Flow",
624
+ color=next(color_iter_ax2),
625
+ )
626
+ elif model_type in ["FINVOL"]:
627
+ qbzm = qzb / dz
628
+ qbzm[0] *= 2
629
+ qbzm[-1] *= 2
630
+ ax2.plot(
631
+ -qbzm,
632
+ zp,
633
+ label=f"{file_name}: Heat Flow",
634
+ color=next(color_iter_ax2),
635
+ )
636
+ elif model_type in ["PYG", "PYGFIELD"]:
637
+ # only single value for Tb, Qb etc, do not plot heat flow
638
+ qbzm = qzb
639
+ q_plot = qbzm * np.ones(len(zp))
640
+ # because of UBWT condition the heat flow is not linear
641
+ # ax2.plot(q_plot, zp, label=f'{file_name}: Heat Flow', color=next(color_iter_ax2))
642
+ else:
643
+ print(f"Unrecognized model type: {model_type}")
644
+
645
+ # zseg is depth of mid point of segments
646
+ zseg = results.zseg.values
647
+ zval = k_s
648
+
649
+ if param["model_type"] == "PYG" or param["model_type"] == "PYGTILTED":
650
+ zp_plot = np.array([zseg[0], zseg[-1]])
651
+ k_s_plot = k_s * np.ones(2)
652
+ else:
653
+ interp_obj = StratInterpolator(zseg, zval)
654
+
655
+ zz = np.linspace(
656
+ int(param.D),
657
+ int(param.D) + int(param.H),
658
+ int(param.nsegments) + 1,
659
+ )
660
+
661
+ zp = interp_obj.zp
662
+ # Interpolate basecase thermal conductivity values
663
+ k_s_interpolated = interp_obj.interp_plot(zz[0:-1], zz[1:])
664
+
665
+ # select interpolated depth values for which the thermal conductivities are
666
+ zp_plot = zp[(zp >= zz[0])]
667
+ start_index_zp_plot = np.where(zp >= zz[0])[0][0]
668
+ k_s_plot = k_s_interpolated[start_index_zp_plot:]
669
+
670
+ # Plot thermal conductivity on ax2twin
671
+ if (
672
+ (param["model_type"].item() == "ANALYTICAL")
673
+ or (param["model_type"].item() == "PYG")
674
+ or (param["model_type"].item() == "PYGFIELD")
675
+ or (param["model_type"].item() == "FINVOL")
676
+ ):
677
+ ax2twin.plot(
678
+ k_s_plot,
679
+ -zp_plot,
680
+ label=f"{file_name}: Thermal conductivity",
681
+ color=next(color_iter_ax2twin),
682
+ )
683
+
684
+ legend1 = ax1.legend(bbox_to_anchor=(1, 1))
685
+ # combine the legends from the second plot primary and secondary axis
686
+ handles2, labels2 = ax2.get_legend_handles_labels()
687
+ handles3, labels3 = ax2twin.get_legend_handles_labels()
688
+ all_handles = handles2 + handles3
689
+ all_labels = labels2 + labels3
690
+ # Create a single combined legend and define legend location
691
+ if len(params_ds) > 1:
692
+ legend_loc = ((1.8 - (len(params_ds) * 0.4)), 1)
693
+ else:
694
+ legend_loc = (1.9, 1)
695
+ combined_legend = ax2.legend(
696
+ all_handles, all_labels, bbox_to_anchor=legend_loc
697
+ )
698
+
699
+ plt.figure(fig1.number) # Make fig1 the current active figure
700
+ plt.savefig(
701
+ f"{out_path}_temperature_depth_{time}.png",
702
+ dpi=300,
703
+ bbox_extra_artists=[legend1],
704
+ bbox_inches="tight",
705
+ )
706
+
707
+ plt.figure(fig2.number) # Make fig2 the current active figure
708
+ plt.savefig(
709
+ f"{out_path}_heatflow_depth_{time}.png",
710
+ dpi=300,
711
+ bbox_extra_artists=[combined_legend],
712
+ bbox_inches="tight",
713
+ )
714
+
715
+ plt.close(fig1)
716
+ plt.close(fig2)
717
+
718
+ @staticmethod
719
+ def create_borehole_temp_plot(
720
+ segindex: list[int],
721
+ times: list,
722
+ params_ds: xr.Dataset | list[xr.Dataset],
723
+ results_ds: xr.Dataset | list[xr.Dataset],
724
+ singlerun: SingleRun,
725
+ out_path: Path,
726
+ ):
727
+ """
728
+ Only compatible with (multiple) single simulations.
729
+ Generates and saves depth-profiles at different timesteps in the simulation results, for fluid temperatures,
730
+ borehole wall temperature, subsurface temperature, subsurface heat flow and subsurface bulk thermal conductivity.
731
+
732
+ Generate borehole cross-section temperature plots at different timesteps.
733
+
734
+ Parameters
735
+ ----------
736
+ segindex : List[int]
737
+ Segment indices to plot.
738
+ times : List[float]
739
+ Timesteps to plot.
740
+ params_ds : Union[xr.Dataset, List[xr.Dataset]]
741
+ Dataset(s) containing simulation input parameters.
742
+ results_ds : Union[xr.Dataset, List[xr.Dataset]]
743
+ Dataset(s) containing simulation results.
744
+ singlerun : SingleRun
745
+ SingleRun object with simulation input parameters.
746
+ out_path : Path
747
+ Directory to save plots.
748
+
749
+ Raises
750
+ ------
751
+ ValueError
752
+ If any required parameter is missing from a DataFrame in `results_ds` or `params_ds`.
753
+
754
+ Returns
755
+ -------
756
+ None (the plots are saved directly to the specified output path).
757
+ """
758
+ method = "nearest"
759
+ if not isinstance(params_ds, list):
760
+ params_ds = [params_ds]
761
+ results_ds = [results_ds]
762
+
763
+ for time in times:
764
+ for param, results in zip(params_ds, results_ds):
765
+ pipe_temp = results["T_f"].sel(time=time, method=method)
766
+ borehole_wall_temp = results["T_b"].sel(time=time, method=method)
767
+
768
+ rb_scale = 1.2
769
+
770
+ for iseg in segindex:
771
+ T_f = pipe_temp[iseg, :]
772
+ T_b = borehole_wall_temp[iseg]
773
+
774
+ singlerun.bh_design.m_flow = singlerun.sim_params.m_flow[0]
775
+ singlerun.bh_design.customPipe = (
776
+ singlerun.bh_design.get_custom_pipe()
777
+ )
778
+ custom_pipe = singlerun.bh_design.customPipe
779
+ custom_pipe.update_thermal_resistances()
780
+
781
+ R = custom_pipe.R[iseg]
782
+ Q = np.linalg.solve(R, T_f - T_b)
783
+ Q = np.asarray(Q).flatten()
784
+ N_xy = 200
785
+ r_b = custom_pipe.b.r_b
786
+ x = np.linspace(-rb_scale * r_b, rb_scale * r_b, num=N_xy)
787
+ y = np.linspace(-rb_scale * r_b, rb_scale * r_b, num=N_xy)
788
+ X, Y = np.meshgrid(x, y)
789
+
790
+ # Grid points to evaluate temperatures
791
+ position_pipes = custom_pipe.pos
792
+ pipe_r_out = custom_pipe.r_out
793
+ R_fp = custom_pipe.R_f + custom_pipe.R_p[iseg]
794
+ k_g = custom_pipe.k_g[iseg]
795
+ k_s = custom_pipe.k_s
796
+ T_b = np.asarray(T_b)
797
+ (T_f, temperature, it, eps_max) = gt.pipes.multipole(
798
+ position_pipes,
799
+ pipe_r_out,
800
+ r_b,
801
+ k_s,
802
+ k_g,
803
+ R_fp,
804
+ T_b,
805
+ Q,
806
+ J=3,
807
+ x_T=X.flatten(),
808
+ y_T=Y.flatten(),
809
+ )
810
+ distance = np.sqrt(X.flatten() ** 2 + Y.flatten() ** 2)
811
+ temperature[distance > r_b] = np.nan
812
+
813
+ # create figs
814
+ fig, ax = plt.subplots()
815
+ ax.set_xlabel("x (m)")
816
+ ax.set_ylabel("y (m)")
817
+ # Axis limits
818
+ plt.axis(
819
+ [
820
+ -rb_scale * r_b,
821
+ rb_scale * r_b,
822
+ -rb_scale * r_b,
823
+ rb_scale * r_b,
824
+ ]
825
+ )
826
+ plt.gca().set_aspect("equal", adjustable="box")
827
+ gt.utilities._format_axes(ax)
828
+
829
+ levels = np.linspace(
830
+ np.nanmin(temperature), np.nanmax(temperature), 10
831
+ )
832
+ cs = plt.contourf(
833
+ X,
834
+ Y,
835
+ temperature.reshape((N_xy, N_xy)),
836
+ levels=levels,
837
+ cmap="viridis",
838
+ )
839
+ cbar = fig.colorbar(cs)
840
+
841
+ # Borehole wall outline
842
+ borewall = plt.Circle(
843
+ (0.0, 0.0),
844
+ radius=r_b,
845
+ fill=False,
846
+ linestyle="--",
847
+ linewidth=2.0,
848
+ )
849
+ ax.add_patch(borewall)
850
+
851
+ # Pipe outlines
852
+ for pos, r_out_n in zip(position_pipes, pipe_r_out):
853
+ pipe = plt.Circle(
854
+ pos,
855
+ radius=r_out_n,
856
+ fill=False,
857
+ linestyle="-",
858
+ linewidth=4.0,
859
+ )
860
+ ax.add_patch(pipe)
861
+
862
+ # Adjust to plot window
863
+ plt.tight_layout()
864
+
865
+ # Save fig
866
+ filename = f"{out_path}_borehole_temp_{time}_seg{iseg}.png"
867
+ fig.savefig(filename)
868
+ plt.close(fig)
869
+
870
+ @staticmethod
871
+ def create_barplot(
872
+ results_df: pd.DataFrame | list[pd.DataFrame],
873
+ params_df: pd.DataFrame | list[pd.DataFrame],
874
+ y_variable: str,
875
+ outpath: Path,
876
+ ) -> None:
877
+ """
878
+ Only compatible with results and inputs of stochastic (MC) simulations.
879
+ Generates and saves a tornado bar plot, that shows the correlation of a specified simulation parameter
880
+ with other simulation input parameters and results.
881
+
882
+ This function merges simulation input parameters and results dataframes, calculates the correlation of
883
+ the specified simulation parameter (y_variable) with the other parameters, and visualizes the sensitivity of
884
+ the y_variable to changes in the other system parameters.
885
+
886
+ Parameters
887
+ ----------
888
+ results_df : Union[pd.DataFrame, List[pd.DataFrame]]
889
+ Simulation result dataframe(s), contain(s) simulation result parameters as columns.
890
+ Each DataFrame corresponds to a single simulation. It should include the columns 'samples'
891
+ and 'file_name' for merging purposes.
892
+ params_df : Union[pd.DataFrame, List[pd.DataFrame]]
893
+ Simulation input dataframe(s), contain(s) simulation input parameters as columns.
894
+ It should include the columns 'samples' and 'file_name' for merging purposes.
895
+ The column 'k_s' will be renamed to 'k_s_par'.
896
+ y_variable : str
897
+ Variable to analyze sensitivity for. The simulation parameter (either input or result) to be plotted
898
+ on the y-axis. Correlation with this parameter is visualized in the plot.
899
+ outpath : Path
900
+ Directory to save the plot.
901
+
902
+ Returns
903
+ -------
904
+ None (plots are saved directly to the specified output path)
905
+
906
+ Notes
907
+ -----
908
+ - The function selects only the simulation (input and result) parameters with numeric values and with a non-zero
909
+ standard deviation.
910
+ - If the specified y_variable does not exist in the merged DataFrame, no plot is created. No error is raised.
911
+ """
912
+
913
+ sns.set_theme(style="whitegrid")
914
+
915
+ # Convert single dataframe inputs to lists
916
+ if not isinstance(params_df, list):
917
+ params_df = [params_df]
918
+ if not isinstance(results_df, list):
919
+ results_df = [results_df]
920
+
921
+ # Rename columns if needed
922
+ params_df = [df.rename(columns={"k_s": "k_s_par"}) for df in params_df]
923
+
924
+ # Merge the dataframes on common columns
925
+ merged_dfs = [
926
+ pd.merge(df1, df2, on=["samples", "file_name"])
927
+ for df1, df2 in zip(params_df, results_df)
928
+ ]
929
+
930
+ # Concatenate merged dataframes if there are multiple
931
+ merged_df = pd.concat(merged_dfs)
932
+
933
+ # Group by 'file_name'
934
+ grouped_df = merged_df.groupby("file_name")
935
+
936
+ # Initialize the color palette for different groups
937
+ palette = sns.color_palette("husl", n_colors=len(grouped_df))
938
+
939
+ # Initialize the figure
940
+ plt.figure(figsize=(10, 6)) # Adjust the figure size if needed
941
+
942
+ # Iterate over each group and plot the data
943
+ for i, (group_name, group_df) in enumerate(grouped_df):
944
+ # Filter out columns with string values
945
+ numeric_columns = group_df.select_dtypes(
946
+ include=["float64", "int64"]
947
+ ).columns
948
+ group_numeric = group_df[numeric_columns]
949
+
950
+ # Filter out columns with zero standard deviation
951
+ non_constant_columns = group_numeric.columns[
952
+ group_numeric.std().round(7) != 0
953
+ ]
954
+ group_numeric_filtered = group_numeric[non_constant_columns]
955
+
956
+ sorted_sensitivity = pd.Series()
957
+ if y_variable in non_constant_columns:
958
+ # Calculate the correlation matrix
959
+ correlation_matrix = group_numeric_filtered.corr()
960
+ sensitivity_to_y_variable = correlation_matrix[y_variable].drop(
961
+ y_variable
962
+ ) # Remove 'y_variable' itself from the list
963
+ sorted_sensitivity = pd.concat(
964
+ (
965
+ sorted_sensitivity.astype(sensitivity_to_y_variable.dtypes),
966
+ sensitivity_to_y_variable,
967
+ )
968
+ )
969
+ sorted_sensitivity = sorted_sensitivity.sort_values(
970
+ ascending=False, na_position="last"
971
+ )
972
+ # sorted_sensitivity = sorted_sensitivity.reindex(sorted_sensitivity.abs().sort_values(ascending=False, na_position='last').index)
973
+
974
+ # Plot covariance matrix using a bar chart with different colors for each group
975
+ sns.barplot(
976
+ x=sorted_sensitivity.values,
977
+ y=sorted_sensitivity.index,
978
+ color=palette[i],
979
+ label=group_name,
980
+ alpha=0.5,
981
+ )
982
+
983
+ # Finalize the plot
984
+ if y_variable in non_constant_columns:
985
+ plt.title(f"Correlation of {y_variable} with input parameters and results")
986
+ plt.xlabel("Correlation Coefficient")
987
+ plt.ylabel("Input and results variables")
988
+ plt.legend(
989
+ bbox_to_anchor=(0, -0.1), loc="upper left"
990
+ ) # Adjust legend position if needed
991
+
992
+ save_path = outpath.with_name(outpath.name + f"_sensitivity_{y_variable}")
993
+ plt.savefig(save_path, bbox_inches="tight")
994
+ plt.close()
995
+ else:
996
+ plt.close()
997
+
998
+ @staticmethod
999
+ def plot_temperature_field(
1000
+ time: int,
1001
+ temperature_result_da: xr.DataArray,
1002
+ Tmin: float,
1003
+ Tmax: float,
1004
+ out_path: Path,
1005
+ ) -> None:
1006
+ """
1007
+ Only compatible with results and numerical (FINVOL model) simulations.
1008
+ Generates and saves a plot of the calculated 3D (z=len(1)) temperature grid around the borehole, for a specific
1009
+ timestep.
1010
+
1011
+ Parameters
1012
+ ----------
1013
+ time : int
1014
+ Index of the timestep to create the plot for.
1015
+ temperature_result_da : xr.DataArray
1016
+ Temperature DataArray, containing 3D (z=len(1)) grid of
1017
+ calculated temperature values around the borehole.
1018
+ Tmin : float
1019
+ Minimum temperature in the DataArray for color scaling.
1020
+ Tmax : float
1021
+ Maximum temperature in the DataArray for color scaling.
1022
+ out_path : Path
1023
+ Directory to save the plot.
1024
+
1025
+ Returns
1026
+ -------
1027
+ None (plots are saved directly to the specified output path)
1028
+ """
1029
+
1030
+ temperature_data = temperature_result_da.T.isel(time=time, z=0).transpose()
1031
+
1032
+ if any(i < 0 for i in temperature_result_da.x):
1033
+ ymin = min(temperature_result_da.x)
1034
+ ymax = max(temperature_result_da.x)
1035
+ else:
1036
+ ymin = max(temperature_result_da.x)
1037
+ ymax = min(temperature_result_da.x)
1038
+
1039
+ # Create a figure and set size
1040
+ fig = plt.figure(figsize=(10, 5))
1041
+
1042
+ # Add subplot
1043
+ ax1 = fig.add_subplot()
1044
+
1045
+ # Define contour levels with even spacing every 5 degrees Celsius
1046
+ levels = range(int(Tmin), int(Tmax) + 5, 5)
1047
+
1048
+ temperature_data.plot.contourf(
1049
+ ax=ax1,
1050
+ ylim=(ymin, ymax),
1051
+ cmap="magma",
1052
+ vmin=Tmin,
1053
+ vmax=Tmax,
1054
+ cbar_kwargs={"label": "Temperature [C]"},
1055
+ levels=levels,
1056
+ )
1057
+
1058
+ timestep = temperature_result_da.time.isel(time=time).values # in hours
1059
+
1060
+ timestep_days = round(int(timestep) / 24, 0)
1061
+
1062
+ plt.title(
1063
+ f"Temperature field over depth, in radial direction after {timestep_days} days"
1064
+ )
1065
+ plt.xlabel("Distance from well (m)")
1066
+ plt.ylabel("Depth from top of well (m)")
1067
+
1068
+ plt.tight_layout()
1069
+
1070
+ filename = out_path.with_name(out_path.name + f"_T_field_{timestep_days}.png")
1071
+ plt.savefig(filename)
1072
+ plt.close()
1073
+
1074
+ @staticmethod
1075
+ def create_temperature_field_movie(in_path: Path) -> None:
1076
+ """
1077
+ Only compatible with results and numerical (FINVOL model) simulations.
1078
+ Generates and saves a clip of a sequence of the temperature grid plots creates using the method plot_temperature_field.
1079
+
1080
+ Parameters
1081
+ ----------
1082
+ in_path : Path
1083
+ Directory containing temperature field images.
1084
+
1085
+ Returns
1086
+ -------
1087
+ None (plots are saved directly to the specified output path)
1088
+ """
1089
+ # Get list of image filenames
1090
+ image_filenames = [
1091
+ f.name for f in Path(in_path).iterdir() if f.suffix == ".png"
1092
+ ]
1093
+
1094
+ image_filenames.sort(key=PlotResults.extract_timestep)
1095
+
1096
+ images = []
1097
+ for image in image_filenames:
1098
+ im_obj = Image.open((in_path / image), "r")
1099
+ images.append(im_obj)
1100
+
1101
+ fig, ax = plt.subplots()
1102
+
1103
+ ims = []
1104
+ for i in range(len(images)):
1105
+ im = ax.imshow(images[i], animated=True)
1106
+ if i == 0:
1107
+ # set initial image
1108
+ ax.imshow(images[i], animated=True)
1109
+ ims.append([im])
1110
+
1111
+ ax.axis("off")
1112
+
1113
+ ani = animation.ArtistAnimation(fig, ims, interval=500, repeat_delay=100)
1114
+
1115
+ plt.tight_layout()
1116
+ plt.close()
1117
+
1118
+ movie_name = "Tfield_animation.gif"
1119
+ ani.save(in_path / movie_name)
1120
+
1121
+ @staticmethod
1122
+ def extract_timestep(filename: str) -> float:
1123
+ """
1124
+ Extracts timestep from the filename, used for the chronological ordering of temperature grid plots in the
1125
+ method create_temperature_field_movie.
1126
+
1127
+ Parameters
1128
+ ----------
1129
+ filename : str
1130
+ Name of the image file.
1131
+
1132
+ Returns
1133
+ -------
1134
+ float
1135
+ Extracted timestep or infinity if not found.
1136
+ """
1137
+ match = re.search(r"T_field_(\d+\.\d+)\.png", filename)
1138
+ if match:
1139
+ return float(match.group(1))
1140
+ else:
1141
+ # If no match is found, return a very large number
1142
+ return float("inf")