buckpy-dev 0.0.1__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.
@@ -0,0 +1,777 @@
1
+ """
2
+ This module contains the solver functions of BuckPy.
3
+ """
4
+
5
+ import time
6
+ from multiprocess import Process, Queue, cpu_count
7
+ import numpy as np
8
+ import pandas as pd
9
+ from numba import jit
10
+ from .buckpy_variables import *
11
+
12
+ def exec_buckpy(np_scen, np_distr, np_ends, n_sim, fric_sampling, bl_verbose):
13
+
14
+ """
15
+ Execute buckpy methodology
16
+
17
+ Parameters
18
+ ----------
19
+ np_scen : numpy.ndarray
20
+ Array containing the pipeline desing data on assessed mesh.
21
+ np_distr : numpy.ndarray
22
+ 3-D array describing the stochastic distributions:
23
+ - dim 1: properties
24
+ - dim 2: elements
25
+ - dim 3: values
26
+ np_ends : numpy.ndarray
27
+ Array containing information about the boundary conditions at the ends of the pipeline.
28
+ n_sim : int
29
+ Number of Monte-Carlo simulations to be run.
30
+ fric_sampling : int
31
+ Switch to select the sampling method of the lateral friction: 0 for 'per Element'
32
+ or 1 'per OOS Reference Length'
33
+
34
+ Returns
35
+ -------
36
+ df_pp_plot : DataFrame
37
+ Definition on assessed mesh of CBF and EAF in different conditions
38
+ for case to plot.
39
+ df_VAP_plot : DataFrame
40
+ Definition of virtual anchor points for case to plot.
41
+ df_pp_buckle_prop : DataFrame
42
+ Results for all buckles triggered.
43
+
44
+ Other Parameters
45
+ ----------------
46
+ bl_verbose : boolean, optional
47
+ True if intermediate printouts are required (False by default).
48
+ """
49
+
50
+ # Starting time of the solver module
51
+ start_time = time.time()
52
+
53
+ # Run deterministic case
54
+ if bl_verbose:
55
+ print("2. Run the deterministic case")
56
+ np_prob_var_det = np.empty(0)
57
+ np_case_det = preprocess_case(np_prob_var_det, np_scen, np_ends, bl_det = True)
58
+ np_pp_buckle_det, np_case_det_updated, vap_list_det = solve_case(np_scen, np_case_det)
59
+
60
+ # To be discussed: This block has been replaced to enable a like to like comparison with
61
+ # Buckfast.
62
+
63
+ # # Print-out force profiles for graphical display
64
+ # # (Deterministic case if it buckles, or otherwise first random case that buckles)
65
+ # np_pp_plot = np.empty((9, np_scen[KP].size))
66
+ # vap_list = []
67
+ # df_VAP_plot = pd.DataFrame(vap_list, columns = ['ielt VAP', 'KP VAP', 'ESF VAP'])
68
+ # n_buckle = np_pp_buckle_det.shape[0]
69
+ # if n_buckle > 0: # Deterministic case buckled
70
+ # for i_buckle in range(n_buckle):
71
+ # np_pp_buckle_det[i_buckle, SIM_PP] = 1
72
+ # np_pp_plot[P_KP] = np_scen[KP]
73
+ # np_pp_plot[P_CBF_HT] = np_case_det[CBF_HT]
74
+ # np_pp_plot[P_CBF_OP] = np_case_det[CBF_OP]
75
+ # np_pp_plot[P_EAF_INST] = np_case_det[EAF_INST]
76
+ # np_pp_plot[P_EAF_HT] = np_case_det_updated[EAF_HT]
77
+ # np_pp_plot[P_EAF_P_OP] = np_case_det_updated[EAF_P_OP]
78
+ # np_pp_plot[P_EAF_OP] = np_case_det_updated[EAF_OP]
79
+ # np_pp_plot[P_EAF_OP_UNBUCK] = np_case_det[EAF_OP_UNBUCK]
80
+ # df_VAP_plot = pd.DataFrame(vap_list_det, columns = ['ielt VAP', 'KP VAP', 'ESF VAP'])
81
+ # print(' The deterministic case has buckled')
82
+ # if bl_verbose:
83
+ # print(f' Time taken to run the deterministic case: {time.time()-start_time:.1f}s')
84
+
85
+ # # If there is no buckling on deterministic case then for result display
86
+ # # generate a random case until buckling is triggered (max iteration<n_sim)
87
+ # else:
88
+ # for i_sim in range(n_sim):
89
+ # np_prob_var = sample_randomness_case(fric_sampling, np_distr, np_scen)
90
+ # np_case = preprocess_case(np_prob_var, np_scen, np_ends, bl_det = False)
91
+ # np_pp_buckle, np_case_updated, vap_list = solve_case(np_scen, np_case)
92
+ # n_buckle = np_pp_buckle.shape[0]
93
+ # if n_buckle > 0: # Found random case buckling
94
+ # for i_buckle in range(n_buckle):
95
+ # np_pp_buckle[i_buckle, SIM_PP] = i_sim
96
+ # np_pp_plot[P_KP] = np_scen[KP]
97
+ # np_pp_plot[P_CBF_HT] = np_case[CBF_HT]
98
+ # np_pp_plot[P_CBF_OP] = np_case[CBF_OP]
99
+ # np_pp_plot[P_EAF_INST] = np_case[EAF_INST]
100
+ # np_pp_plot[P_EAF_HT] = np_case_updated[EAF_HT]
101
+ # np_pp_plot[P_EAF_P_OP] = np_case_updated[EAF_P_OP]
102
+ # np_pp_plot[P_EAF_OP] = np_case_updated[EAF_OP]
103
+ # np_pp_plot[P_EAF_OP_UNBUCK] = np_case[EAF_OP_UNBUCK]
104
+ # #
105
+ # df_VAP_plot = pd.DataFrame(vap_list, columns = ['ielt VAP', 'KP VAP', 'ESF VAP'])
106
+ # print(f' The random case {i_sim} has buckled')
107
+ # break
108
+
109
+ # Print-out force profiles for graphical display (deterministic simulation only)
110
+ np_pp_plot = np.empty((9, np_scen[KP].size))
111
+ vap_list = []
112
+ df_VAP_plot = pd.DataFrame(vap_list, columns = ['ielt VAP', 'KP VAP', 'ESF VAP'])
113
+ n_buckle = np_pp_buckle_det.shape[0]
114
+ for i_buckle in range(n_buckle):
115
+ np_pp_buckle_det[i_buckle, SIM_PP] = 1
116
+ np_pp_plot[P_KP] = np_scen[KP]
117
+ np_pp_plot[P_CBF_HT] = np_case_det[CBF_HT]
118
+ np_pp_plot[P_CBF_OP] = np_case_det[CBF_OP]
119
+ np_pp_plot[P_EAF_INST] = np_case_det[EAF_INST]
120
+ np_pp_plot[P_EAF_HT] = np_case_det_updated[EAF_HT]
121
+ np_pp_plot[P_EAF_P_OP] = np_case_det_updated[EAF_P_OP]
122
+ np_pp_plot[P_EAF_OP] = np_case_det_updated[EAF_OP]
123
+ np_pp_plot[P_EAF_OP_UNBUCK] = np_case_det[EAF_OP_UNBUCK]
124
+ df_VAP_plot = pd.DataFrame(vap_list_det, columns = ['ielt VAP', 'KP VAP', 'ESF VAP'])
125
+ print(' The deterministic case has buckled')
126
+ if bl_verbose:
127
+ print(f' Time taken to run the deterministic case: {time.time()-start_time:.1f}s')
128
+
129
+ # Run Monte-Carlo loop
130
+ if bl_verbose:
131
+ print("3. Run the Monte-Carlo loop")
132
+ start_time = time.time()
133
+ if n_sim <= 10000:
134
+ N_WORKER = 1
135
+ else:
136
+ N_WORKER = cpu_count()
137
+ list_buckle_prop = run_monte_carlo(N_WORKER, n_sim, np_distr, np_scen,
138
+ np_ends, fric_sampling, bl_verbose = bl_verbose)
139
+ if bl_verbose:
140
+ print(f' Time taken to extract {len(list_buckle_prop)}'
141
+ f' simulations: {time.time()-start_time:.1f}s')
142
+
143
+ # Post-process 'df_pp_buckle_prop' and 'df_pp_buckle_prop'
144
+ column_list = ['isim', 'KP', 'route_type', 'muax', 'mulat_op', 'HOOS', 'CBF_op', 'VAS_op']
145
+ df_pp_buckle_prop = pd.DataFrame(np.concatenate(list_buckle_prop), columns = column_list)
146
+ column_list = ['KP', 'CBF_ht', 'CBF_op', 'EAF_inst', 'EAF_ht',
147
+ 'EAF_p_op','EAF_op', 'beta2', 'EAF_op_unbuck']
148
+ df_pp_plot = pd.DataFrame(np.transpose(np_pp_plot), columns = column_list)
149
+
150
+ return df_pp_plot, df_VAP_plot, df_pp_buckle_prop
151
+
152
+ @jit(nopython=True)
153
+ def sample_randomness_case(fric_sampling, np_distr, np_scen):
154
+
155
+ """
156
+ Samples the stochastic variables (HOOS and friction factors).
157
+
158
+ Parameters
159
+ ----------
160
+ fric_sampling : int
161
+ Switch to select the sampling method of the lateral friction: 0 for 'per Element'
162
+ or 1 'per OOS Reference Length'
163
+ np_distr : numpy.ndarray
164
+ 3-D array describing the stochastic distributions: Dim 1 = Properties, Dim 2 = Elements and
165
+ Dim 3 = values
166
+ np_scen : numpy.ndarray
167
+ Array containing the pipeline desing data on assessed mesh.
168
+
169
+ Returns
170
+ -------
171
+ np_prob_var : numpy.ndarray
172
+ Definition of probabilistic variables on assessed mesh.
173
+ Rows correspond to properties: MUAX = 0, MULAT_HT = 1, MULAT_OP = 2, HOOS = 3
174
+
175
+ Notes
176
+ -----
177
+ The returned `np_prob_var` has values by columns corresponding to mesh elements
178
+ over 4 rows corresponding to properties defined above.
179
+
180
+ | MUAX: Axial friction coefficient
181
+ | MULAT_HT: Lateral friction coefficient for Hydrotest condition
182
+ | MULAT_OP: Lateral friction coefficient for Operation condition
183
+ | HOOS: Soil property indicator
184
+
185
+ """
186
+
187
+ # Number of elements in the assessed mesh
188
+ n_elts = np_scen[KP].size
189
+ # Initialize the array to store probabilistic variables
190
+ np_prob_var = np.empty((4, n_elts))
191
+
192
+ # Sample axial friction coefficient
193
+ muax_rand = np.random.rand()
194
+
195
+ # Sample lateral friction coefficient
196
+ if fric_sampling == 0: # Sample per element
197
+ mul_rand = np.random.rand(n_elts)
198
+ elif fric_sampling == 1: # Sample per OOS Reference Length
199
+ mul_rand = np.full(n_elts, np.random.rand())
200
+ for i in range(2, int(np.amax(np_scen[SECTION_ID])) + 1):
201
+ mul_rand[np_scen[SECTION_ID] == i] = np.random.rand()
202
+
203
+ # Sample HOOS factor
204
+ hoos_rand = np.random.rand(n_elts)
205
+
206
+ # Interpolate sampled values based on CDF arrays to get probabilistic variables
207
+ for i in range(n_elts):
208
+ np_prob_var[MUAX, i] = np.interp(
209
+ muax_rand, np_distr[MUAX_CDF_ARRAY][i], np_distr[MUAX_ARRAY][i])
210
+ np_prob_var[MULAT_HT, i] = np.interp(
211
+ mul_rand[i], np_distr[MULAT_CDF_ARRAY_HT][i], np_distr[MULAT_ARRAY_HT][i])
212
+ np_prob_var[MULAT_OP, i] = np.interp(
213
+ mul_rand[i], np_distr[MULAT_CDF_ARRAY_OP][i], np_distr[MULAT_ARRAY_OP][i])
214
+ np_prob_var[HOOS, i] = np.interp(
215
+ hoos_rand[i], np_distr[HOOS_CDF_ARRAY][i], np_distr[HOOS_ARRAY][i])
216
+
217
+ return np_prob_var
218
+
219
+ def preprocess_case(np_prob_var, np_scen, np_ends, bl_det = False):
220
+ """
221
+ Preprocess case based on scenario definition and probabilistic variables.
222
+
223
+ Parameters
224
+ ----------
225
+ np_prob_var : numpy.ndarray
226
+ Probabilistic variables for the case (shape: 4 x n_elements).
227
+ np_scen : numpy.ndarray
228
+ Scenario data array (rows = properties, columns = elements).
229
+ np_ends : numpy.ndarray
230
+ End boundary condition array (rows = properties, columns = ends).
231
+ bl_det : bool, optional
232
+ If True, use mean values; if False, use sampled values. Default is False.
233
+
234
+ Returns
235
+ -------
236
+ np_case : numpy.ndarray
237
+ Case array (21 x n_elements) with rows:
238
+ - 0 : MUAX
239
+ - 1 : MULAT_HT
240
+ - 2 : MULAT_OP
241
+ - 3 : HOOS
242
+ - 4 : CBF_HT
243
+ - 5 : CBF_OP
244
+ - 6 : LFF_INST
245
+ - 7 : LFF_HT
246
+ - 8 : LFF_OP
247
+ - 9 : RFF_INST
248
+ - 10 : RFF_HT
249
+ - 11 : RFF_OP
250
+ - 12 : EAF_INST
251
+ - 13 : EAF_HT
252
+ - 14 : EAF_P_OP
253
+ - 15 : EAF_OP
254
+ - 16 : EAF_OP_UNBUCK
255
+ - 17 : LDELTAF_HT
256
+ - 18 : RDELTAF_HT
257
+ - 19 : LDELTAF_OP
258
+ - 20 : RDELTAF_OP
259
+
260
+ Notes
261
+ -----
262
+ Computes CBF and frictional forces based on route type and boundary conditions,
263
+ and builds effective axial force (EAF) profiles for installation, hydrotest, and operation.
264
+ """
265
+
266
+ # Extracting the number of elements from the scenario array
267
+ n_elts = np_scen[KP].size
268
+
269
+ # Creating an empty array to store processed case-specific data
270
+ np_case = np.empty((21, np_scen.shape[1]))
271
+
272
+ if bl_det: # Alocating mean values to probabilistic variables
273
+ np_case[MUAX] = np_scen[MUAX_MEAN]
274
+ np_case[MULAT_HT] = np_scen[MULAT_HT_MEAN]
275
+ np_case[MULAT_OP] = np_scen[MULAT_OP_MEAN]
276
+ np_case[HOOS] = np.where(np_scen[ROUTE_TYPE] < 4, np_scen[HOOS_MEAN], np_scen[CBF_RCM])
277
+
278
+ else: # Copying probabilistic variables to the case array
279
+ np_case[MUAX] = np_prob_var[MUAX]
280
+ np_case[MULAT_HT] = np_prob_var[MULAT_HT]
281
+ np_case[MULAT_OP] = np_prob_var[MULAT_OP]
282
+ np_case[HOOS] = np_prob_var[HOOS]
283
+
284
+ # Calculating buckling forces based on route types
285
+ for i in range(n_elts):
286
+ if np_scen[ROUTE_TYPE, i] == 1: # Straight section
287
+ np_case[CBF_HT, i] = np_case[HOOS, i] * np_scen[SCHAR_HT, i] \
288
+ * np_case[MULAT_HT, i]**0.5
289
+ np_case[CBF_OP, i] = np_case[HOOS, i] * np_scen[SCHAR_OP, i] \
290
+ * np_case[MULAT_OP, i]**0.5
291
+ elif np_scen[ROUTE_TYPE, i] == 2: # Curve section
292
+ np_case[CBF_HT, i] = np_case[HOOS, i] * np_case[MULAT_HT, i] \
293
+ * np_scen[SW_HT, i] * np_scen[BEND_RADIUS, i]
294
+ np_case[CBF_OP, i] = np_case[HOOS, i] * np_case[MULAT_OP, i] \
295
+ * np_scen[SW_OP, i] * np_scen[BEND_RADIUS, i]
296
+ elif np_scen[ROUTE_TYPE, i] == 3: # Sleeper
297
+ np_case[CBF_HT, i] = np_case[HOOS, i] * np_scen[SV_HT, i]
298
+ np_case[CBF_OP, i] = np_case[HOOS, i] * np_scen[SV_OP, i]
299
+ elif np_scen[ROUTE_TYPE, i] == 4: # RCM
300
+ np_case[CBF_HT, i] = np_case[HOOS, i]
301
+ np_case[CBF_OP, i] = np_case[HOOS, i]
302
+
303
+ # Calculate friction forces accounting for boundary conditions
304
+ np_case[LFF_INST], np_case[RFF_INST], junk1, junk2 = calc_fric_forces(
305
+ -np_case[MUAX] * np_scen[SW_INST] * np_scen[LENGTH],
306
+ np_ends[REAC_INST, 0], np_ends[REAC_INST, 1])
307
+ np_case[LFF_HT], np_case[RFF_HT], np_case[LDELTAF_HT], np_case[RDELTAF_HT] = calc_fric_forces(
308
+ np_case[MUAX] * np_scen[SW_HT] * np_scen[LENGTH],
309
+ np_ends[REAC_HT, 0], np_ends[REAC_HT, 1])
310
+ np_case[LFF_OP], np_case[RFF_OP], np_case[LDELTAF_OP], np_case[RDELTAF_OP] = calc_fric_forces(
311
+ np_case[MUAX] * np_scen[SW_OP] * np_scen[LENGTH],
312
+ np_ends[REAC_OP, 0], np_ends[REAC_OP, 1])
313
+
314
+ # Adjust friction forces based on boundary conditions
315
+ if np_ends[ROUTE_TYPE_BC, 0] == 2: # Fixed BC at "left" end side
316
+ np_case[LFF_INST] = np.full(n_elts, -9.0E+09)
317
+ np_case[LFF_HT] = np.full(n_elts, 9.0E+09)
318
+ np_case[LFF_OP] = np.full(n_elts, 9.0E+09)
319
+ if np_ends[ROUTE_TYPE_BC, 1] == 2: # Fixed BC at "right" end side
320
+ np_case[RFF_INST] = np.full(n_elts, -9.0E+09)
321
+ np_case[RFF_HT] = np.full(n_elts, 9.0E+09)
322
+ np_case[RFF_OP] = np.full(n_elts, 9.0E+09)
323
+
324
+ # Build up frictional effective forces
325
+ np_case[EAF_INST] = np.maximum(
326
+ np.maximum(np_case[LFF_INST], np_case[RFF_INST]), np_scen[RLT])
327
+ np_case[EAF_HT] = np.minimum(
328
+ np.minimum(np_case[LFF_HT], np_case[RFF_HT]), np_scen[FRF_HT])
329
+ np_case[EAF_P_OP] = np.minimum(
330
+ np.minimum(np_case[LFF_OP], np_case[RFF_OP]), np_scen[FRF_P_OP])
331
+ np_case[EAF_OP] = np.minimum(
332
+ np.minimum(np_case[LFF_OP], np_case[RFF_OP]), np_scen[FRF_OP])
333
+ np_case[EAF_OP_UNBUCK] = np.minimum(
334
+ np.minimum(np_case[LFF_OP], np_case[RFF_OP]), np_scen[FRF_OP])
335
+
336
+ return np_case
337
+
338
+ def calc_fric_forces(np_array, restr_left, restr_right):
339
+
340
+ """
341
+ Accumulate friction forces from each end of the array. The calculated values are at the center
342
+ of each element, accounting for the average increase between the element and its surrounding
343
+ elements.
344
+
345
+ Parameters
346
+ ----------
347
+ np_array : numpy.ndarray
348
+ Array containing the local friction force for each element without accounting for
349
+ surrounding values.
350
+ restr_left : float
351
+ Constant force to be added at the left of the force profile.
352
+ restr_right : float
353
+ Constant force to be added at the right of the force profile.
354
+
355
+ Returns
356
+ -------
357
+ np_cumsum_left: numpy.ndarray
358
+ Cumulated friction force from the left.
359
+ np_cumsum_right: numpy.ndarray
360
+ Cumulated friction force from the right.
361
+ np_rolling_mean_left: numpy.ndarray
362
+ Average friction force increment from the left.
363
+ np_rolling_mean_right: numpy.ndarray
364
+ Average friction force increment from the right.
365
+ """
366
+
367
+ # Cumulate the effective axial friction forces
368
+ ret = np.cumsum(np_array, dtype=float)
369
+
370
+ # Calculate the average increase between each element and its surrounding elements
371
+ ret[2:] = ret[2:] - ret[:-2]
372
+
373
+ # Compute the rolling mean for the left and right ends
374
+ np_rolling_mean_left = np.append(restr_left + np_array[0] / 2.0, ret[1:] / 2.0)
375
+ np_rolling_mean_right = np.append(ret[1:] / 2.0, restr_right + np_array[-1] / 2.0)
376
+
377
+ # Compute the cumulative effective axial forces from the left and right
378
+ np_cumsum_left = np.cumsum(np_rolling_mean_left)
379
+ np_cumsum_right = np.cumsum(np_rolling_mean_right[::-1])[::-1]
380
+
381
+ return np_cumsum_left, np_cumsum_right, np_rolling_mean_left, np_rolling_mean_right
382
+
383
+ @jit(nopython=True)
384
+ def solve_case(np_scen, np_case):
385
+ """
386
+ Calculate effective force profiles during hydrotest and operation and identify buckle properties.
387
+
388
+ Parameters
389
+ ----------
390
+ np_scen : numpy.ndarray
391
+ Scenario array (rows = properties, columns = elements).
392
+ np_case : numpy.ndarray
393
+ Case array (rows = properties, columns = elements).
394
+
395
+ Returns
396
+ -------
397
+ np_pp_buckle : numpy.ndarray
398
+ 2D array (n_buckles x 8) with columns:
399
+ - 0 : SIM_PP
400
+ - 1 : KP_PP
401
+ - 2 : ROUTE_TYPE_PP
402
+ - 3 : MUAX_PP
403
+ - 4 : MULAT_OP_PP
404
+ - 5 : HOOS_PP
405
+ - 6 : CBF_OP_PP
406
+ - 7 : VAS_PP
407
+ np_case : numpy.ndarray
408
+ Updated case array (21 x n_elements).
409
+ vap_list : list[list]
410
+ [element_index, KP, ESF_op] for virtual anchor points (sorted by KP).
411
+
412
+ Notes
413
+ -----
414
+ Buckle identification is performed in stages (HT, OP pressure-only, OP pressure+temperature),
415
+ updating EAF profiles after each buckle is found.
416
+ """
417
+
418
+ # Step 1: Get order and location of potential buckles from HT step
419
+ np_beta = (np_case[CBF_HT] - np_case[EAF_INST]) / (np_scen[FRF_HT] - np_scen[RLT])
420
+ np_beta = np.around(np_beta, decimals = 6)
421
+ sorted_beta_array = np.argsort(np_beta, kind = "mergesort")
422
+
423
+ # Step 2: Compare local driving force during HT with CBF to identify actual buckles,
424
+ # in the order of the potential buckles
425
+ np_buckle_id = np.empty(0)
426
+ for i in sorted_beta_array:
427
+ if np_case[EAF_HT, i] >= np_case[CBF_HT, i]:
428
+ np_buckle_id = np.append(np_buckle_id, i)
429
+ np_ff_buckle = calc_friction_force_from_buckle(np_scen, np_case, i, "HT")
430
+ np_case[EAF_HT] = np.minimum(np_ff_buckle, np_case[EAF_HT])
431
+
432
+ # Step 3a: Set up the EAF OP Pressure profile with the buckle locations from HT
433
+ np_ff_buckle = calc_friction_force_from_buckle(np_scen, np_case, i, "OP")
434
+ np_case[EAF_P_OP] = np.minimum(np_ff_buckle, np_case[EAF_P_OP])
435
+
436
+ # Step 5a: Set up the EAF OP profile with the buckle locations from HT
437
+ np_case[EAF_OP] = np.minimum(np_ff_buckle, np_case[EAF_OP])
438
+
439
+ # Step 3b: Identify the location and order of additional potential buckles
440
+ # during OP (Pressure only)
441
+ np_beta = (np_case[CBF_OP] - np_case[EAF_INST]) / (np_scen[FRF_P_OP] - np_scen[RLT])
442
+ np_beta = np.around(np_beta, decimals = 6)
443
+ sorted_beta_array = np.argsort(np_beta, kind = "mergesort")
444
+
445
+ # Step 4: Compare the local driving force with the CBF during OP (pressure only) to identify
446
+ # actual buckles, taking into account the actual buckles from HT
447
+ for i in sorted_beta_array:
448
+ if np_case[EAF_P_OP, i] >= np_case[CBF_OP, i]:
449
+ np_buckle_id = np.append(np_buckle_id, i)
450
+ np_ff_buckle = calc_friction_force_from_buckle(np_scen, np_case, i, "OP")
451
+ np_case[EAF_P_OP] = np.minimum(np_ff_buckle, np_case[EAF_P_OP])
452
+
453
+ # Step 5a: Set up the EAF OP profile with the buckle locations from OP pressure only
454
+ np_case[EAF_OP]=np.minimum(np_ff_buckle, np_case[EAF_OP])
455
+
456
+ # Step 5b: Calculate Beta2 to identify the order and location of the potential buckles from the
457
+ # temperature in operation
458
+ # TODO: Explain in the manual that this calculation assumes linear temperature variations.
459
+ np_beta = (
460
+ np_case[CBF_OP] - np_case[EAF_INST] - (
461
+ np_scen[FRF_P_OP] - np_scen[RLT])) / np_scen[FRF_T_OP]
462
+ np_beta = np.around(np_beta, decimals = 6)
463
+ sorted_beta_array = np.argsort(np_beta, kind = "mergesort")
464
+
465
+ # Step 6: Compare the local driving force with the CBF (pressure + temperature) to find
466
+ # actual buckles
467
+ for i in sorted_beta_array:
468
+ if np_case[EAF_OP, i] >= np_case[CBF_OP, i]:
469
+ np_buckle_id = np.append(np_buckle_id, i)
470
+ np_ff_buckle = calc_friction_force_from_buckle(np_scen, np_case, i, "OP")
471
+ np_case[EAF_OP] = np.minimum(np_ff_buckle, np_case[EAF_OP])
472
+
473
+ # Extract the number of elements from the scenario array
474
+ n_elts = np_scen[KP].size
475
+
476
+ # Drop buckle elements duplicated between hydrotest and operation steps
477
+ # [can happen if the CBF < Buckle residual force]
478
+ np_buckle_id = np.unique(np_buckle_id)
479
+
480
+ # Initiate post-processing outputs array
481
+ np_pp_buckle = np.empty((np_buckle_id.size, 8))
482
+ vap_list = []
483
+
484
+ if len(np_buckle_id) > 0: # At least one buckle detected
485
+ for i_buckle in range(len(np_buckle_id)):
486
+ ielt_buckle = np.sort(np_buckle_id.astype(np.int64))[i_buckle]
487
+
488
+ # Identify possible VAP (surrounding buckling susceptibility areas)
489
+ np_ff_buckle = calc_friction_force_from_buckle(np_scen, np_case, ielt_buckle, "OP")
490
+ np_possible_vap_id = np.where(
491
+ np.isclose(np_case[EAF_OP], np_ff_buckle.astype(np.float64)) == False)[0]
492
+
493
+ # Add first and last elements to possible VAP for fixed end cases
494
+ if not (0 in np_possible_vap_id):
495
+ np_possible_vap_id = np.append(np_possible_vap_id, 0)
496
+ if not (n_elts in np_possible_vap_id):
497
+ np_possible_vap_id = np.append(np_possible_vap_id, n_elts)
498
+ np_possible_vap_id=np.sort(np_possible_vap_id)
499
+
500
+ # For each buckle, look for surrounding VAP and calculate KP
501
+ if ielt_buckle <= np.min(np_possible_vap_id):
502
+ ivap_left = ielt_buckle
503
+ else:
504
+ np_distance_left = np.absolute(
505
+ np_possible_vap_id[np_possible_vap_id < ielt_buckle]-ielt_buckle)
506
+ ivap_left = min(
507
+ ielt_buckle, np_possible_vap_id[
508
+ np_possible_vap_id < ielt_buckle][np_distance_left.argmin()] + 1)
509
+
510
+ if ielt_buckle >= np.max(np_possible_vap_id):
511
+ ivap_right = ielt_buckle
512
+ else:
513
+ np_distance_right = np.absolute(
514
+ np_possible_vap_id[np_possible_vap_id > ielt_buckle]-ielt_buckle)
515
+ ivap_right = max(
516
+ ielt_buckle, np_possible_vap_id[
517
+ np_possible_vap_id > ielt_buckle][np_distance_right.argmin()] - 1)
518
+
519
+ if ivap_left < ivap_right: # Buckle detected
520
+ # TODO: add switch to calculate KP of actual VAP instead of centroid of elements
521
+ # TODO: ivap_left and ivap_right where VAP are located
522
+ if ivap_left == 0:
523
+ kp_vap_left = np_scen[KP][ivap_left] - np_scen[LENGTH][ivap_left]
524
+ else:
525
+ kp_vap_left = np_scen[KP][ivap_left] - np_scen[LENGTH][ivap_left]
526
+ if ivap_right == (n_elts - 1):
527
+ kp_vap_right = np_scen[KP][ivap_right] + np_scen[LENGTH][ivap_right]
528
+ else:
529
+ kp_vap_right = np_scen[KP][ivap_right] + np_scen[LENGTH][ivap_right]
530
+
531
+ # Add new VAP to list if not already identified
532
+ temp_list = [ivap_left, kp_vap_left, np_case[EAF_OP][ivap_left]]
533
+ if temp_list not in vap_list:
534
+ vap_list.append(temp_list)
535
+ temp_list = [ivap_right, kp_vap_right, np_case[EAF_OP][ivap_right]]
536
+ if temp_list not in vap_list:
537
+ vap_list.append(temp_list)
538
+ else: # not really triggered buckle
539
+ kp_vap_right = np_scen[KP, ielt_buckle]
540
+ kp_vap_left = kp_vap_right
541
+
542
+ # Fill up the array of outputs for post-processing
543
+ np_pp_buckle[i_buckle, KP_PP] = np_scen[KP, ielt_buckle]
544
+ np_pp_buckle[i_buckle, ROUTE_TYPE_PP] = np_scen[ROUTE_TYPE, ielt_buckle]
545
+ np_pp_buckle[i_buckle, MUAX_PP] = np_case[MUAX, ielt_buckle]
546
+ np_pp_buckle[i_buckle, MULAT_OP_PP] = np_case[MULAT_OP, ielt_buckle]
547
+ np_pp_buckle[i_buckle, HOOS_PP] = np_case[HOOS, ielt_buckle]
548
+ np_pp_buckle[i_buckle, CBF_OP_PP] = np_case[CBF_OP, ielt_buckle]
549
+ np_pp_buckle[i_buckle, VAS_PP] = kp_vap_right - kp_vap_left
550
+
551
+ # End of condition on presence of buckle
552
+ vap_list = sorted(vap_list, key = lambda x: x[1])
553
+
554
+ return np_pp_buckle, np_case, vap_list
555
+
556
+ @jit(nopython=True)
557
+ def calc_friction_force_from_buckle(np_scen, np_case, ielt_buckle, phase_str):
558
+
559
+ """
560
+ Calculate the effective axial frictional forces for each element in a scenario.
561
+
562
+ Parameters
563
+ ----------
564
+ np_scen : numpy.ndarray
565
+ 2D array containing scenario-specific data, where rows represent different parameters and
566
+ columns represent elements.
567
+ np_case : numpy.ndarray
568
+ 2D array containing case-specific data, where rows represent different parameters and
569
+ columns represent elements.
570
+ ielt_buckle : int
571
+ Index of the buckle element.
572
+ phase_str : str
573
+ Phase label indicating whether the calculation is for "HT" (hydrotest) or
574
+ "OP" (operational) phase.
575
+
576
+ Returns
577
+ -------
578
+ np_ff_buckle : numpy.ndarray
579
+ 1D array containing the computed friction forces for each element in the scenario.
580
+
581
+ """
582
+
583
+ # Extract the number of elements from the scenario array
584
+ n_elts = np_scen[KP].size
585
+
586
+ if phase_str == "HT":
587
+ cbf = np_case[CBF_HT, ielt_buckle]
588
+ residual_buckle_force = np_scen[EAF_BUCKLE_HT, ielt_buckle]
589
+ residual_buckle_length = np_scen[L_BUCKLE_HT, ielt_buckle]
590
+ i_sw = SW_HT
591
+ i_ldeltaf = LDELTAF_HT
592
+ i_rdeltaf = RDELTAF_HT
593
+
594
+ elif phase_str == "OP":
595
+ cbf = np_case[CBF_OP, ielt_buckle]
596
+ residual_buckle_force = np_scen[EAF_BUCKLE_OP, ielt_buckle]
597
+ residual_buckle_length = np_scen[L_BUCKLE_OP, ielt_buckle]
598
+ i_sw = SW_OP
599
+ i_ldeltaf = LDELTAF_OP
600
+ i_rdeltaf = RDELTAF_OP
601
+
602
+ else:
603
+ print(f'Unrecognised phase label {phase_str}')
604
+
605
+ # The residual buckle force should be capped by the minimum CBF
606
+ residual_buckle_force = min(residual_buckle_force, cbf)
607
+
608
+ # Find the index of elements closer to "left" side of buckle
609
+ kp_flat_left = np_scen[KP, ielt_buckle] - residual_buckle_length / 2.0
610
+ np_distance_left = np.absolute(np_scen[KP] - kp_flat_left)
611
+ ielt_left = np_distance_left.argmin()
612
+ if np_scen[KP,ielt_left] > kp_flat_left:
613
+ ielt_left = max(0, ielt_left-1)
614
+
615
+ # Find the index of elements closer to "right" side of buckle
616
+ kp_flat_right = np_scen[KP, ielt_buckle] + residual_buckle_length / 2.0
617
+ np_distance_right = np.absolute(np_scen[KP] - kp_flat_right)
618
+ ielt_right = np_distance_right.argmin()
619
+ if np_scen[KP, ielt_right] < kp_flat_right:
620
+ ielt_right = min(ielt_right + 1, n_elts - 1)
621
+
622
+ # Calculate the incremental effective axial frictional forces
623
+ eaf_left_centroid = residual_buckle_force + np_case[MUAX, ielt_left] \
624
+ * np_scen[i_sw, ielt_left] * (kp_flat_left - np_scen[KP, ielt_left])
625
+ eaf_right_centroid = residual_buckle_force + np_case[MUAX, ielt_right] \
626
+ * np_scen[i_sw, ielt_right] * (np_scen[KP, ielt_right] - kp_flat_right)
627
+
628
+ # Compute rolling means for the left and right sections around the buckle
629
+ np_rolling_mean_left = np.append(np.zeros(ielt_right+1-1), eaf_right_centroid)
630
+ np_rolling_mean_left = np.append(np_rolling_mean_left, np_case[i_ldeltaf][(ielt_right+1):])
631
+ np_rolling_mean_right = np.append(np_case[i_rdeltaf][:ielt_left], eaf_left_centroid)
632
+ np_rolling_mean_right = np.append(np_rolling_mean_right, np.zeros(n_elts - (ielt_left+1)))
633
+
634
+ # Compute left and right cumulative sums of the effective axial forces for each element
635
+ np_lff_buckle = np.cumsum(np_rolling_mean_left)
636
+ np_rff_buckle = np.cumsum(np_rolling_mean_right[::-1])[::-1]
637
+
638
+ # Determine the maximum effective axial frictional force for each element
639
+ np_ff_buckle = np.maximum(np.maximum(np_lff_buckle,residual_buckle_force), np_rff_buckle)
640
+
641
+ return np_ff_buckle
642
+
643
+ def run_monte_carlo(n_worker, n_sim, np_distr, np_scen, np_ends, fric_sampling, bl_verbose):
644
+
645
+ """
646
+ Set to run the Monte-Carlo simulations using multiple workers and gather the properties of
647
+ the buckles that have triggered.
648
+
649
+ Parameters
650
+ ----------
651
+ n_worker : int
652
+ Number of worker processes spawned in the multiprocess.
653
+ n_sim : int
654
+ Number of Monte-Carlo simulations to be run.
655
+ np_distr: NumPy array
656
+ 3-D numpy array containing the stochastic distributions
657
+ (dim 1: properties, dim 2: elements, dim 3: values).
658
+ np_scen : NumPy array
659
+ Numpy array containing the design data along the pipeline
660
+ route (mesh) that remains constant among deterministic and
661
+ Monte-Carlo simulations (except the stochastic distributions).
662
+ np_ends : NumPy array
663
+ Returns a numpy array with the definition of the tie-ins at
664
+ the pipeline ends.
665
+ fric_sampling : int
666
+ Switch to select the sampling method of the soil lateral friction
667
+ factor (0 for 'per Element' or 1 'per OOS Reference Length').
668
+
669
+ Returns
670
+ ----------
671
+ list_buckle_prop : List[np.ndarray]
672
+ Array containing for each simulation the properties of
673
+ the buckle(s) that has(ve) triggered.
674
+
675
+ Other Parameters
676
+ ----------------
677
+ bl_verbose : boolean, optional
678
+ True if intermediate printouts are required (False by default).
679
+ """
680
+
681
+ mp_output = Queue()
682
+ processes = []
683
+ if bl_verbose:
684
+ start_time = time.time()
685
+
686
+ # Distribute simulations across worker processes
687
+ for i_worker in range(n_worker):
688
+ if i_worker < (n_worker - 1):
689
+ processes.append(Process(target=exec_worker_stack,
690
+ args=(int(i_worker*int(n_sim/n_worker)),
691
+ int((i_worker+1)*int(n_sim/n_worker)),
692
+ np_distr, np_scen, np_ends, fric_sampling, mp_output)))
693
+ else:
694
+ processes.append(Process(target=exec_worker_stack,
695
+ args=(int(i_worker*int(n_sim/n_worker)),
696
+ int(n_sim), np_distr, np_scen, np_ends, fric_sampling, mp_output)))
697
+
698
+ # Start worker processes
699
+ for p in processes:
700
+ p.start()
701
+
702
+ if bl_verbose:
703
+ print(f' Time taken to start the queue and {n_worker} worker process: {time.time() - start_time:.1f}s')
704
+
705
+ # Wait for output from worker processes
706
+ while mp_output.qsize() == 0:
707
+ time.sleep(1)
708
+
709
+ buckle_prop = []
710
+
711
+ # Collect buckle properties from the output queue
712
+ if bl_verbose:
713
+ n_sim_display_list = [10**i for i in range(1+round(np.log10(n_sim)))]
714
+
715
+ while mp_output:
716
+ try:
717
+ buckle_prop.append(mp_output.get(timeout=10))
718
+ except:
719
+ break
720
+
721
+ if bl_verbose:
722
+ for n_sim_display in n_sim_display_list:
723
+ if len(buckle_prop) >= n_sim_display:
724
+ print(f' Time taken to solve {n_sim_display:.0f} / {n_sim:.0f} simulations: {time.time() - start_time:.1f}s')
725
+ n_sim_display_list.remove(n_sim_display)
726
+
727
+ return buckle_prop
728
+
729
+ def exec_worker_stack(n_sim_start, n_sim_end, np_distr, np_scen, np_ends, fric_sampling, mp_output):
730
+
731
+ """
732
+ Run a stack of random simulations using a single worker and add the results to the
733
+ post-processing queue mp_output.
734
+
735
+ Parameters
736
+ ----------
737
+ n_sim_start : int
738
+ Number of the first simulation to be executed by the current worker.
739
+ n_sim_end : int
740
+ Number of the last simulation to be executed by the current worker (i.e., this simulation
741
+ number is not executed in this call to the function).
742
+ np_distr: NumPy array
743
+ 3-D numpy array containing the stochastic distributions
744
+ (dim 1: properties, dim 2: elements, dim 3: values).
745
+ np_scen : NumPy array
746
+ Numpy array containing the design data along the pipeline route (mesh) that remains
747
+ constant among deterministic and Monte-Carlo simulations
748
+ (except the stochastic distributions).
749
+ np_ends : NumPy array
750
+ Returns a numpy array with the definition of the tie-ins at the pipeline ends.
751
+ fric_sampling : int
752
+ Switch to select the sampling method of the soil lateral friction factor
753
+ (0 for 'per Element' or 1 'per OOS Reference Length').
754
+ mp_output: Queue
755
+ Results are added to this Queue for subsequent post-processing.
756
+ """
757
+
758
+ # Iterate over the range of simulations assigned to the worker
759
+ for i_sim in range(n_sim_start, n_sim_end):
760
+
761
+ # Sample randomness for the current simulation
762
+ np_prob_var = sample_randomness_case(fric_sampling, np_distr, np_scen)
763
+
764
+ # Preprocess the simulation case
765
+ np_case = preprocess_case(np_prob_var, np_scen, np_ends, bl_det = False)
766
+
767
+ # Solve the simulation case to find potential buckle properties
768
+ np_pp_buckle, junk1, junk2 = solve_case(np_scen, np_case)
769
+
770
+ # Get the number of buckles triggered in the simulation
771
+ n_buckle = np_pp_buckle.shape[0]
772
+
773
+ # If buckles triggered, adjust the simulation number of the buckle and add to the queue
774
+ if n_buckle > 0:
775
+ for i_buckle in range(n_buckle):
776
+ np_pp_buckle[i_buckle, SIM_PP] = i_sim
777
+ mp_output.put(np_pp_buckle)