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