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.
- geoloop/axisym/AxisymetricEL.py +751 -0
- geoloop/axisym/__init__.py +3 -0
- geoloop/bin/Flowdatamain.py +89 -0
- geoloop/bin/Lithologymain.py +84 -0
- geoloop/bin/Loadprofilemain.py +100 -0
- geoloop/bin/Plotmain.py +250 -0
- geoloop/bin/Runbatch.py +81 -0
- geoloop/bin/Runmain.py +86 -0
- geoloop/bin/SingleRunSim.py +928 -0
- geoloop/bin/__init__.py +3 -0
- geoloop/cli/__init__.py +0 -0
- geoloop/cli/batch.py +106 -0
- geoloop/cli/main.py +105 -0
- geoloop/configuration.py +946 -0
- geoloop/constants.py +112 -0
- geoloop/geoloopcore/CoaxialPipe.py +503 -0
- geoloop/geoloopcore/CustomPipe.py +727 -0
- geoloop/geoloopcore/__init__.py +3 -0
- geoloop/geoloopcore/b2g.py +739 -0
- geoloop/geoloopcore/b2g_ana.py +535 -0
- geoloop/geoloopcore/boreholedesign.py +683 -0
- geoloop/geoloopcore/getloaddata.py +112 -0
- geoloop/geoloopcore/pyg_ana.py +280 -0
- geoloop/geoloopcore/pygfield_ana.py +519 -0
- geoloop/geoloopcore/simulationparameters.py +130 -0
- geoloop/geoloopcore/soilproperties.py +152 -0
- geoloop/geoloopcore/strat_interpolator.py +194 -0
- geoloop/lithology/__init__.py +3 -0
- geoloop/lithology/plot_lithology.py +277 -0
- geoloop/lithology/process_lithology.py +697 -0
- geoloop/loadflowdata/__init__.py +3 -0
- geoloop/loadflowdata/flow_data.py +161 -0
- geoloop/loadflowdata/loadprofile.py +325 -0
- geoloop/plotting/__init__.py +3 -0
- geoloop/plotting/create_plots.py +1137 -0
- geoloop/plotting/load_data.py +432 -0
- geoloop/utils/RunManager.py +164 -0
- geoloop/utils/__init__.py +0 -0
- geoloop/utils/helpers.py +841 -0
- geoloop-1.0.0b1.dist-info/METADATA +112 -0
- geoloop-1.0.0b1.dist-info/RECORD +46 -0
- geoloop-1.0.0b1.dist-info/entry_points.txt +2 -0
- geoloop-0.0.1.dist-info/licenses/LICENSE → geoloop-1.0.0b1.dist-info/licenses/LICENSE.md +2 -1
- geoloop-0.0.1.dist-info/METADATA +0 -10
- geoloop-0.0.1.dist-info/RECORD +0 -6
- {geoloop-0.0.1.dist-info → geoloop-1.0.0b1.dist-info}/WHEEL +0 -0
- {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")
|