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,1142 @@
1
+ """
2
+ This module contains the pre-processing functions of BuckPy.
3
+ """
4
+ import time
5
+ import numpy as np
6
+ import pandas as pd
7
+ from scipy.stats import lognorm
8
+ import pysubsea as ss
9
+
10
+ def calc_lognorm_hoos(type_elt, length_elt, hoos_mean, hoos_std, length_ref, rcm_charac):
11
+ """
12
+ Compute the parameters of the horizontal out-of-straightness (HOOS) lognormal distribution
13
+ for different types of elements (e.g., Straight, Bend, Sleeper, RCM). This function takes into
14
+ account the scaling factor of the HOOS distribution. For RCM, the HOOS factor is not a factor
15
+ but the critical buckling force.
16
+
17
+ Parameters
18
+ ----------
19
+ type_elt : str
20
+ Type of the element.
21
+ length_elt : float
22
+ Length of the element.
23
+ hoos_mean : float
24
+ Mean of the HOOS distribution.
25
+ hoos_std : float
26
+ Standard deviation of the HOOS distribution.
27
+ length_ref : float
28
+ Reference length.
29
+ rcm_charac : float
30
+ Characteristic buckling force for the Residual Curvature Method (RCM).
31
+
32
+ Returns
33
+ -------
34
+ x_range : numpy.ndarray
35
+ An array of values representing the range of the friction factor distribution
36
+ between probabilities of exceedance between 0.01% and 99.99%.
37
+ cdf_range : numpy.ndarray
38
+ An array of cumulative density function (CDF) values corresponding to `x_range`.
39
+
40
+ Notes
41
+ -----
42
+ This function computes the parameters of a lognormal distribution for different types of
43
+ elements such as Straight, Bend, Sleeper, and RCM (Residual Curvature Method). It
44
+ calculates the cumulative density function (CDF) for the generated range of values
45
+ based on the HOOS distribution parameters.
46
+
47
+ """
48
+
49
+ # Extract the type of element (e.g., Straight, Bend, Sleeper, RCM)
50
+ type_elt_split = type_elt.split(" ")[0]
51
+
52
+ # Compute the ratio of the reference length to the element length
53
+ n = length_ref / length_elt
54
+
55
+ if type_elt_split == "Straight" or type_elt_split == "Bend":
56
+
57
+ # Calculate parameters for straight or bend elements
58
+ shape_hoos = np.sqrt(np.log(1 + hoos_std**2 / hoos_mean**2))
59
+ scale_hoos = np.log(hoos_mean**2 / (np.sqrt(hoos_mean**2 + hoos_std**2)))
60
+
61
+ # Define the range of the HOOS distribution
62
+ hoos_lower = 0.0
63
+ hoos_upper = 20.0
64
+ x = np.linspace(hoos_lower, hoos_upper, 200000)
65
+
66
+ # Calculate the cumulative density function (CDF) considering the scaling factor
67
+ cdf = 1-(1-lognorm.cdf(x, shape_hoos, 0.0, np.exp(scale_hoos)))**(1/n)
68
+
69
+ # Generate a range of CDF values
70
+ cdf_range = np.arange(0.0, 1.0, 0.0001)
71
+
72
+ # Interpolate to get the corresponding values of the distribution
73
+ x_range = np.interp(cdf_range, cdf, x)
74
+
75
+ elif type_elt_split == "Sleeper":
76
+
77
+ # Calculate parameters for sleeper elements
78
+ shape_hoos = np.sqrt(np.log(1 + hoos_std**2 / hoos_mean**2))
79
+ scale_hoos = np.log(hoos_mean**2 / (np.sqrt(hoos_mean**2 + hoos_std**2)))
80
+
81
+ # Calculate the lower and upper bounds of the distribution for sleeper elements
82
+ hoos_lower = lognorm(shape_hoos, 0.0, np.exp(scale_hoos)).ppf(0.0001)
83
+ hoos_upper = lognorm(shape_hoos, 0.0, np.exp(scale_hoos)).ppf(0.9999)
84
+
85
+ # Generate a range of values within the distribution
86
+ x_range = np.linspace(hoos_lower, hoos_upper, 10000)
87
+
88
+ # Compute the cumulative density function (CDF) for the generated range
89
+ cdf_range = lognorm.cdf(x_range, shape_hoos, 0.0, np.exp(scale_hoos))
90
+
91
+ elif type_elt_split == "RCM":
92
+
93
+ # Calculate parameters for RCM elements
94
+ shape_hoos = np.sqrt(np.log(1 + hoos_std**2 / hoos_mean**2))
95
+ scale_hoos = np.log(hoos_mean**2 / (np.sqrt(hoos_mean**2 + hoos_std**2)))
96
+ scale_hoos = scale_hoos + np.log(rcm_charac)
97
+
98
+ # Calculate the lower and upper bounds of the distribution for RCM elements
99
+ hoos_lower = lognorm(shape_hoos, 0.0, np.exp(scale_hoos)).ppf(0.0001)
100
+ hoos_upper = lognorm(shape_hoos, 0.0, np.exp(scale_hoos)).ppf(0.9999)
101
+
102
+ # Generate a range of values within the distribution
103
+ x_range = np.linspace(hoos_lower, hoos_upper, 10000)
104
+
105
+ # Compute the cumulative density function (CDF) for the generated range
106
+ cdf_range = lognorm.cdf(x_range, shape_hoos, 0.0, np.exp(scale_hoos))
107
+
108
+ return x_range, cdf_range
109
+
110
+ class PreProcessor:
111
+ """
112
+ Class to handle the pre-processing of scenario data for BuckPy simulations. This class reads
113
+ scenario data from an Excel file, extracts and processes route, pipe, operating, and soil data,
114
+ and calculates scenario data. It also converts the scenario data and end boundary conditions
115
+ to NumPy arrays for Monte Carlo simulations and processes post-processing data.
116
+
117
+ The class includes methods for calculating expanded KP values, creating element arrays,
118
+ interpolating distributions, and handling various preprocessing tasks.
119
+ """
120
+
121
+ def __init__(self, work_dir, file_name, pipeline, scenario, bl_verbose):
122
+ """
123
+ Method to initialize the PreProcessor class with the necessary parameters and attributes.
124
+
125
+ Parameters
126
+ ----------
127
+ work_dir : str
128
+ Directory where the Excel file is located.
129
+ file_name : str
130
+ Name of the Excel file.
131
+ pipeline : str
132
+ Identifier of the pipeline.
133
+ scenario : int
134
+ Identifier of the scenario.
135
+ bl_verbose : bool
136
+ True if intermediate printouts are required (False by default).
137
+
138
+ Returns
139
+ -------
140
+ None
141
+ """
142
+ # Initialize attributes for data storage
143
+ self.work_dir = work_dir
144
+ self.file_name = file_name
145
+ self.pipeline = pipeline
146
+ self.scenario = scenario
147
+ self.bl_verbose = bl_verbose
148
+
149
+ # Initialize attributes for storing dataframes and arrays
150
+ self.scen_df = None
151
+ self.route_df = None
152
+ self.route_ends_df = None
153
+ self.mitigation_df = None
154
+ self.soil_zoning_df = None
155
+ self.pipe_df = None
156
+ self.soil_df = None
157
+ self.oper_df = None
158
+ self.pp_df = None
159
+
160
+ # Initialize attributes for storing NumPy arrays used in Monte Carlo simulations
161
+ self.scen_np = None
162
+ self.dist_np = None
163
+ self.ends_np = None
164
+
165
+ def run(self):
166
+ """
167
+ Import scenario data from an Excel file and preprocess it.
168
+
169
+ Parameters
170
+ ----------
171
+ work_dir : str
172
+ Directory where the Excel file is located.
173
+ file_name : str
174
+ Name of the Excel file.
175
+ pipeline : str
176
+ Identifier of the pipeline.
177
+ scenario : int
178
+ Identifier of the scenario.
179
+ bl_verbose : bool, optional
180
+ True if intermediate printouts are required.
181
+
182
+ Returns
183
+ -------
184
+ scen_np : numpy.ndarray
185
+ NumPy array containing the scenario data for Monte Carlo simulations.
186
+ dist_np : numpy.ndarray
187
+ NumPy array containing the distribution data for Monte Carlo simulations.
188
+ ends_np : numpy.ndarray
189
+ NumPy array containing the end boundary conditions for Monte Carlo simulations.
190
+ scen_df : pandas.DataFrame
191
+ DataFrame containing the scenario data for deterministic simulations.
192
+ pp_df : pandas.DataFrame
193
+ DataFrame containing the post-processing data for the scenario.
194
+
195
+ Notes
196
+ -----
197
+ This function reads scenario data from an Excel file, extracts and processes route,
198
+ pipe, operating, and soil data, and calculates scenario data.
199
+ It also converts the scenario data and end boundary conditions to NumPy arrays for
200
+ Monte Carlo simulations and processes post-processing data.
201
+ The function prints out the time taken to create the main dataframe
202
+ if bl_verbose is set to True.
203
+
204
+ Other Parameters
205
+ ----------------
206
+ bl_verbose : boolean, optional
207
+ True if intermediate printouts are required (False by default).
208
+ """
209
+
210
+ # Print out in the terminal that the assembly of the main dataframe has started
211
+ if self.bl_verbose:
212
+ print("1. Assembly of the main dataframe")
213
+
214
+ # Starting time of the pre-processing module
215
+ start_time = time.time()
216
+
217
+ # Read data from the input Excel file
218
+ sheets = pd.read_excel(rf"{self.work_dir}/{self.file_name}", sheet_name=None)
219
+ self.scen_df = sheets["Scenario"]
220
+ self.route_df = sheets["Route"]
221
+ self.mitigation_df = sheets["Mitigation"]
222
+ self.soil_zoning_df = sheets["Soil Zoning"]
223
+ self.pipe_df = sheets["Pipe"]
224
+ self.soil_df = sheets["Soils"]
225
+ self.oper_df = sheets["Operating"]
226
+ self.pp_df = sheets["Post-Processing"]
227
+
228
+ # Filter scenario dataframe based on pipeline and scenario
229
+ self.scen_df = self.scen_df.loc[
230
+ (self.scen_df["Pipeline"] == self.pipeline) &
231
+ (self.scen_df["Scenario"] == self.scenario)
232
+ ].copy()
233
+
234
+ # Extract simulation parameters from the scenario dataframe
235
+ layout = self.scen_df["Layout Set"].values[0]
236
+ mitigation = self.scen_df["Mitigation Set"].values[0]
237
+ loadcase = self.scen_df["Loadcase Set"].values[0]
238
+
239
+ # Filter route data based on layout
240
+ self.route_df = self.route_df.loc[
241
+ (self.route_df["Pipeline"] == self.pipeline) &
242
+ (self.route_df["Layout Set"] == layout)
243
+ ].copy()
244
+ # Ensure mitigation-driven columns exist on route rows before segmentation
245
+ for col in ["Sleeper Height", "RCM Buckling Force"]:
246
+ if col not in self.route_df.columns:
247
+ self.route_df[col] = np.nan
248
+ self.route_df[["KP From", "KP To"]] = (
249
+ self.route_df[["KP From", "KP To"]].astype(float)
250
+ )
251
+
252
+ # Filter mitigation data based on mitigation
253
+ self.mitigation_df = self.mitigation_df.loc[
254
+ (self.mitigation_df["Pipeline"] == self.pipeline) &
255
+ (self.mitigation_df["Mitigation Set"] == mitigation)
256
+ ].copy()
257
+ self.mitigation_df[["KP From", "KP To", "Sleeper Height", "RCM Buckling Force"]] = (
258
+ self.mitigation_df[["KP From", "KP To", "Sleeper Height", "RCM Buckling Force"]]
259
+ .astype(float)
260
+ )
261
+
262
+ # Filter soil zoning data based on soil zoning
263
+ self.soil_zoning_df = self.soil_zoning_df.loc[
264
+ (self.soil_zoning_df["Pipeline"] == self.pipeline) &
265
+ (self.soil_zoning_df["Route Layout"] == layout)
266
+ ].copy()
267
+ self.soil_zoning_df[["KP From", "KP To"]] = (
268
+ self.soil_zoning_df[["KP From", "KP To"]].astype(float)
269
+ )
270
+
271
+ # Postprocess route data based on route, mitigation and soil zoning data
272
+ self.calc_route_data()
273
+
274
+ # Postprocess pipe data and calculate pipe properties
275
+ self.pipe_df = self.pipe_df.loc[
276
+ (self.pipe_df["Pipeline"] == self.pipeline)
277
+ ].copy()
278
+ self.calc_pipe_data()
279
+
280
+ # Postprocess soil data and calculate friction factor distributions
281
+ self.soil_df = self.soil_df.loc[
282
+ (self.soil_df["Pipeline"] == self.pipeline)
283
+ ].copy()
284
+ self.calc_soil_data()
285
+
286
+ # Postprocess operating data and calculate operating profiles and operating data
287
+ self.oper_df = self.oper_df.loc[
288
+ (self.oper_df["Pipeline"] == self.pipeline) &
289
+ (self.oper_df["Loadcase Set"] == loadcase)
290
+ ].copy()
291
+ self.calc_oper_data()
292
+
293
+ # Postprocess scenario data
294
+ self.calc_scenario_data()
295
+
296
+ # Define the NumPy arrays used in the Monte Carlo Simulations
297
+ self.calc_monte_carlo_data()
298
+
299
+ # Process post-processing data based on pipeline, layout and mitigation
300
+ mask = (
301
+ (self.pp_df["Pipeline"] == self.pipeline) &
302
+ (self.pp_df["Layout Set"] == layout)
303
+ )
304
+ if pd.isna(mitigation):
305
+ mask &= self.pp_df["Mitigation Set"].isna()
306
+ else:
307
+ mask &= self.pp_df["Mitigation Set"] == mitigation
308
+ self.pp_df = self.pp_df.loc[mask].copy()
309
+ self.calc_pp_data()
310
+
311
+ # Ensure mitigation-driven columns exist on route rows after segmentation
312
+ if "Sleeper Height" not in self.route_df.columns:
313
+ self.route_df["Sleeper Height"] = np.nan
314
+ if "RCM Buckling Force" not in self.route_df.columns:
315
+ self.route_df["RCM Buckling Force"] = np.nan
316
+
317
+ # Set "Bend Radius" to NaN for rows where "Sleeper Height" or "RCM Buckling Force" are not NaN
318
+ self.route_df.loc[~self.route_df["Sleeper Height"].isna(), "Bend Radius"] = np.nan
319
+ self.route_df.loc[~self.route_df["RCM Buckling Force"].isna(), "Bend Radius"] = np.nan
320
+
321
+ # Select specific columns for route data output
322
+ cols = [
323
+ "Pipeline", "Layout Set", "Pipe Set", "Friction Set", "Route Type", "Point ID From",
324
+ "Point ID To", "KP From", "KP To", "Bend Radius", "Sleeper Height",
325
+ "RCM Buckling Force", "HOOS Mean", "HOOS STD", "HOOS Reference Length",
326
+ "Residual Buckle Force Hydrotest", "Residual Buckle Length Hydrotest",
327
+ "Residual Buckle Force Operation", "Residual Buckle Length Operation",
328
+ "Reaction Installation", "Reaction Hydrotest", "Reaction Operation"
329
+ ]
330
+ self.route_df = self.route_df[cols].copy()
331
+
332
+ # Print out in the terminal time taken to create main dataframe
333
+ if self.bl_verbose:
334
+ print(f" Time taken to create main dataframe: {time.time() - start_time:.1f}s")
335
+
336
+ return self.scen_np, self.dist_np, self.ends_np, self.scen_df, self.route_df, self.pp_df
337
+
338
+ def calc_route_data(self):
339
+ """
340
+ Extract and process route data for calculations.
341
+
342
+ Parameters
343
+ ----------
344
+ route_df : pandas.DataFrame
345
+ DataFrame containing route data.
346
+ mitigation_df : pandas.DataFrame
347
+ DataFrame containing mitigation data.
348
+ soil_zoning_df : pandas.DataFrame
349
+ DataFrame containing soil zoning data.
350
+
351
+ Returns
352
+ -------
353
+ route_df : pandas.DataFrame
354
+ DataFrame containing route data and calculated route data.
355
+ route_ends_df : pandas.DataFrame
356
+ DataFrame containing end boundary conditions.
357
+
358
+ Notes
359
+ -----
360
+ This function extracts route ends and route data based on layout,
361
+ mitigation, and soil_zoning. It selects specific columns for route ends data.
362
+ Route Type is converted from string tofloat for numerical representation. Route ends
363
+ data is converted to a NumPy array for efficient processing.
364
+ """
365
+
366
+ # Extract route ends based on layout
367
+ self.route_ends_df = self.route_df.iloc[[0, -1]]
368
+
369
+ # Select specific columns for route ends data
370
+ self.route_ends_df = self.route_ends_df[[
371
+ "Route Type",
372
+ "KP From",
373
+ "KP To",
374
+ "Reaction Installation",
375
+ "Reaction Hydrotest",
376
+ "Reaction Operation"
377
+ ]]
378
+
379
+ # Convert "Route Type" from string to float for numerical representation
380
+ self.route_ends_df.loc[self.route_ends_df["Route Type"] == "Spool", "Route Type"] = 1
381
+ self.route_ends_df.loc[self.route_ends_df["Route Type"] == "Fixed", "Route Type"] = 2
382
+ self.route_ends_df["Route Type"] = self.route_ends_df["Route Type"].astype(float)
383
+
384
+ # Extract route data based on layout
385
+ self.route_df = self.route_df.iloc[1:-1].copy()
386
+
387
+ # Combine rows from route and mitigation, then sort by KP From
388
+ self.apply_route_mitigation()
389
+
390
+ # Extract soil zoning data based on soil_zoning
391
+ self.apply_route_soil_zoning()
392
+
393
+ def calc_pipe_data(self):
394
+ """
395
+ Calculate properties of pipes.
396
+
397
+ Parameters
398
+ ----------
399
+ pipe_df : pandas.DataFrame
400
+ DataFrame containing the pipe data.
401
+
402
+ Returns
403
+ -------
404
+ pipe_df : pandas.DataFrame
405
+ DataFrame containing the pipe data and calculated pipe properties.
406
+
407
+ Notes
408
+ -----
409
+ This function computes the inner diameter (ID), cross-sectional area (As), inner area (Ai),
410
+ moment of inertia (I), hydrotest characteristic buckling force (SChar HT),
411
+ and operation characteristic buckling force (SChar OP) of the pipe.
412
+ """
413
+
414
+ # Compute the inner diameter (ID) of the pipe
415
+ self.pipe_df["ID"] = self.pipe_df["OD"] - 2.0 * self.pipe_df["WT"]
416
+
417
+ # Compute the cross-sectional area (As) of the pipe
418
+ self.pipe_df["As"] = np.pi / 4.0 * (self.pipe_df["OD"] ** 2 - self.pipe_df["ID"] ** 2)
419
+
420
+ # Compute the inner area (Ai) of the pipe
421
+ self.pipe_df["Ai"] = np.pi / 4.0 * self.pipe_df["ID"] ** 2
422
+
423
+ # Compute the moment of inertia (I) of the pipe
424
+ self.pipe_df["I"] = np.pi / 64.0 * (self.pipe_df["OD"] ** 4 - self.pipe_df["ID"] ** 4)
425
+
426
+ # Compute the hydrotest characteristic buckling force (SChar HT) of the pipe
427
+ self.pipe_df["SChar HT"] = 2.26 * (self.pipe_df["E"] * self.pipe_df["As"]) ** 0.25 * (self.pipe_df["E"] * self.pipe_df["I"]) ** 0.25 * self.pipe_df["sw Hydrotest"] ** 0.5
428
+
429
+ # Compute the operation characteristic buckling force (SChar OP) of the pipe
430
+ self.pipe_df["SChar OP"] = 2.26 * (self.pipe_df["E"] * self.pipe_df["As"]) ** 0.25 * (self.pipe_df["E"] * self.pipe_df["I"]) ** 0.25 * self.pipe_df["sw Operation"] ** 0.5
431
+
432
+ def calc_soil_data(self):
433
+ """
434
+ Calculate soil data and axial and lateral friction factor distributions
435
+ and assign them to DataFrame columns.
436
+
437
+ Parameters
438
+ ----------
439
+ soil_df : pandas.DataFrame
440
+ DataFrame containing soil data.
441
+
442
+ Returns
443
+ -------
444
+ soil_df : pandas.DataFrame
445
+ DataFrame containing soil data and calculated friction factor distributions.
446
+
447
+ Notes
448
+ -----
449
+ This function computes lognormal distributions for axial and lateral
450
+ friction factors and assigns them to DataFrame columns.
451
+ """
452
+
453
+ # Compute lognormal distributions for axial friction and assign to DataFrame
454
+ result = ss.LBSoilDistributions(
455
+ friction_factor_le=self.soil_df["Axial LE"],
456
+ friction_factor_be=self.soil_df["Axial BE"],
457
+ friction_factor_he=self.soil_df["Axial HE"],
458
+ friction_factor_fit_type=self.soil_df["Axial Fit Bounds"]
459
+ ).friction_distribution_parameters()
460
+ self.soil_df["Axial Mean"], self.soil_df["Axial STD"] = result[:2]
461
+ muax_array = np.asarray(result[-2])
462
+ muax_cdf = np.asarray(result[-1])
463
+ self.soil_df["muax Array"] = list(np.atleast_2d(muax_array))
464
+ self.soil_df["muax CDF Array"] = list(np.atleast_2d(muax_cdf))
465
+
466
+ # Compute lognormal distributions for lateral hydrotest friction and assign to DataFrame
467
+ result = ss.LBSoilDistributions(
468
+ friction_factor_le=self.soil_df["Lateral Hydrotest LE"],
469
+ friction_factor_be=self.soil_df["Lateral Hydrotest BE"],
470
+ friction_factor_he=self.soil_df["Lateral Hydrotest HE"],
471
+ friction_factor_fit_type=self.soil_df["Lateral Hydrotest Fit Bounds"]
472
+ ).friction_distribution_parameters()
473
+ self.soil_df["Lateral Hydrotest Mean"], self.soil_df["Lateral Hydrotest STD"] = result[:2]
474
+ mul_ht_array = np.asarray(result[-2])
475
+ mul_ht_cdf = np.asarray(result[-1])
476
+ self.soil_df["mul HT Array"] = list(np.atleast_2d(mul_ht_array))
477
+ self.soil_df["mul HT CDF Array"] = list(np.atleast_2d(mul_ht_cdf))
478
+
479
+ # Compute lognormal distributions for lateral operation friction and assign to DataFrame
480
+ result = ss.LBSoilDistributions(
481
+ friction_factor_le=self.soil_df["Lateral Operation LE"],
482
+ friction_factor_be=self.soil_df["Lateral Operation BE"],
483
+ friction_factor_he=self.soil_df["Lateral Operation HE"],
484
+ friction_factor_fit_type=self.soil_df["Lateral Operation Fit Bounds"]
485
+ ).friction_distribution_parameters()
486
+ self.soil_df["Lateral Operation Mean"], self.soil_df["Lateral Operation STD"] = result[:2]
487
+ mul_op_array = np.asarray(result[-2])
488
+ mul_op_cdf = np.asarray(result[-1])
489
+ self.soil_df["mul OP Array"] = list(np.atleast_2d(mul_op_array))
490
+ self.soil_df["mul OP CDF Array"] = list(np.atleast_2d(mul_op_cdf))
491
+
492
+ def calc_oper_data(self):
493
+ """
494
+ Calculate operating data and process it.
495
+
496
+ Parameters
497
+ ----------
498
+ oper_df : pandas.DataFrame
499
+ DataFrame containing the operating data.
500
+ route_ends_df : pandas.DataFrame
501
+ DataFrame containing the end boundary conditions.
502
+
503
+ Returns
504
+ -------
505
+ df : pandas.DataFrame
506
+ DataFrame containing the operating data and calculated operating data.
507
+
508
+ Notes
509
+ -----
510
+ This function filters oper_df DataFrame based on loadcase, and "KP To".
511
+ It calculates rolling mean and difference, assigns the "Length" column,
512
+ resets the index, and drops rows with NaN values before returning the
513
+ preprocessed DataFrame.
514
+ """
515
+
516
+ # Select the "Point ID From" and "KP To" columns
517
+ route_df_temp = self.route_df[["Point ID From", "KP To"]].reset_index(drop = True).copy()
518
+
519
+ # Add the end row of route and the start KP
520
+ end_row = pd.DataFrame({"Point ID From": "End", "KP To": np.nan}, index = [99999])
521
+ route_df_temp = pd.concat([route_df_temp, end_row], ignore_index = True)
522
+
523
+ # Shift KP column 1 downwards and assign 0.0 to the first KP
524
+ route_df_temp["KP To"] = route_df_temp["KP To"].shift().fillna(0.0)
525
+
526
+ # Expand the KP array with 1000 intervals from 1000 to nearest maximum KP
527
+ route_df_temp = self.build_oper_kp_mesh_from_route(route_df_temp)
528
+
529
+ # Create the elements between each KP points
530
+ elem_array_temp = self.build_oper_element_kp_array(route_df_temp)
531
+
532
+ # Interpolate the RLT, pressure and temperature using KP and operating profile
533
+ self.interpolate_oper_profile_on_kp(elem_array_temp)
534
+
535
+ # Filter oper_df DataFrame based on loadcase and "KP To"
536
+ self.oper_df = self.oper_df.loc[
537
+ self.oper_df["KP"] <= self.route_ends_df["KP To"].iloc[-1]
538
+ ].copy()
539
+
540
+ # Calculate the rolling mean of oper_df grouped by Loadcase Set
541
+ df_rolling_mean = self.oper_df.rolling(2).mean()
542
+
543
+ # Calculate the rolling difference of oper_df grouped by Loadcase Set
544
+ df_rolling_difference = self.oper_df.rolling(2).max() - self.oper_df.rolling(2).min()
545
+
546
+ # Assign the "Length" column in df_rolling_mean
547
+ df_rolling_mean["Length"] = df_rolling_difference["KP"]
548
+
549
+ # Reset the index of df_rolling_mean and drop the "level_2" index level
550
+ df_rolling_mean = df_rolling_mean.reset_index(drop=True)
551
+
552
+ # Drop rows with NaN values
553
+ df_rolling_mean = df_rolling_mean.dropna()
554
+
555
+ self.oper_df = df_rolling_mean.copy()
556
+
557
+ def calc_scenario_data(self):
558
+ """
559
+ Calculate scenario data based on route, pipe, operating, and soil data.
560
+
561
+ Parameters
562
+ ----------
563
+ route_df : pandas.DataFrame
564
+ DataFrame containing route data.
565
+ pipe_df : pandas.DataFrame
566
+ DataFrame containing pipe data.
567
+ oper_df : pandas.DataFrame
568
+ DataFrame containing operating data.
569
+ soil_df : pandas.DataFrame
570
+ DataFrame containing soil data.
571
+
572
+ Returns
573
+ -------
574
+ df: pandas.DataFrame
575
+ DataFrame containing the calculated scenario data.
576
+
577
+ Notes
578
+ -----
579
+ This function merges route, pipe, operating, and soil data to compute various scenario
580
+ parameters. It calculates various attributes such as lognormal distributions,
581
+ buckling forces, and section counts. The resulting DataFrame includes a subset of
582
+ calculated columns and is filled with 0 for missing values.
583
+ """
584
+
585
+ # Merge operating data with route data using an asof merge to align KPs and route segments
586
+ temp_df = pd.merge_asof(
587
+ left=self.oper_df,
588
+ right=self.route_df,
589
+ left_on="KP",
590
+ right_on="KP From",
591
+ direction="backward",
592
+ )
593
+
594
+ # Merge resulting DataFrame with pipe data based on Pipe Set
595
+ temp_df = pd.merge(
596
+ left=temp_df,
597
+ right=self.pipe_df,
598
+ left_on="Pipe Set",
599
+ right_on="Pipe Set"
600
+ )
601
+
602
+ # Merge resulting DataFrame with soil data based on Friction Set
603
+ temp_df = pd.merge(
604
+ left=temp_df,
605
+ right=self.soil_df,
606
+ left_on="Friction Set",
607
+ right_on="Friction Set"
608
+ )
609
+
610
+ # Compute lognormal distributions for soil properties and assign to DataFrame columns
611
+ temp_df["HOOS X Array"], temp_df["HOOS CDF Array"] = zip(
612
+ *temp_df.apply(
613
+ lambda x: calc_lognorm_hoos(
614
+ x["Route Type"],
615
+ x["Length"],
616
+ x["HOOS Mean"],
617
+ x["HOOS STD"],
618
+ x["HOOS Reference Length"],
619
+ x.get("RCM Buckling Force", np.nan),
620
+ ),
621
+ axis=1
622
+ ).apply(np.array)
623
+ )
624
+
625
+ # Compute various buckling forces based on calculated parameters
626
+ temp_df["FRF HT"] = (
627
+ temp_df["RLT"] +
628
+ temp_df["E"] * temp_df["Alpha"] * temp_df["As"] * (temp_df["Temperature Hydrotest"] - temp_df["Temperature Installation"]) +
629
+ (1 - 2 * temp_df["Poisson"]) * (temp_df["Pressure Hydrotest"] - temp_df["Pressure Installation"]) * temp_df["Ai"]
630
+ )
631
+ temp_df["FRF OP"] = (
632
+ temp_df["RLT"] +
633
+ temp_df["E"] * temp_df["Alpha"] * temp_df["As"] * (temp_df["Temperature Operation"] - temp_df["Temperature Installation"]) +
634
+ (1 - 2 * temp_df["Poisson"]) * (temp_df["Pressure Operation"] - temp_df["Pressure Installation"]) * temp_df["Ai"]
635
+ )
636
+ temp_df["FRF OP Pressure"] = (
637
+ temp_df["RLT"] +
638
+ (1 - 2 * temp_df["Poisson"]) * temp_df["Pressure Operation"] * temp_df["Ai"]
639
+ )
640
+ temp_df["FRF OP Temperature"] = (
641
+ temp_df["E"] * temp_df["As"] * temp_df["Alpha"] * (temp_df["Temperature Operation"] - temp_df["Temperature Installation"])
642
+ )
643
+
644
+ # Calculate the hydrotest and operation buckling forces (Sv)
645
+ sleeper_height = temp_df.get("Sleeper Height", pd.Series(np.nan, index=temp_df.index))
646
+ temp_df["Sv HT"] = 4.0 * np.sqrt(temp_df["E"] * temp_df["I"] * temp_df["sw Hydrotest"] / sleeper_height)
647
+ temp_df["Sv OP"] = 4.0 * np.sqrt(temp_df["E"] * temp_df["I"] * temp_df["sw Operation"] / sleeper_height)
648
+
649
+ # Calculate section-related parameters
650
+ temp_df["KP Section"] = temp_df["KP"] - temp_df["KP From"]
651
+ temp_df["Reference Section"] = (temp_df["KP Section"] / temp_df["HOOS Reference Length"]).apply(np.floor)
652
+ temp_df["Section Count"] = 0.0
653
+ temp_df.loc[
654
+ (temp_df["Route Type"] != temp_df["Route Type"].shift()) |
655
+ (temp_df["Reference Section"] != temp_df["Reference Section"].shift()), "Section Count"
656
+ ] = 1.0
657
+ temp_df["Section Count"] = temp_df["Section Count"].cumsum()
658
+
659
+ # Calculate the residual buckle length and force for hydrotest and operation
660
+ if "RCM Buckling Force" not in temp_df.columns:
661
+ temp_df["RCM Buckling Force"] = np.nan
662
+
663
+ # Select relevant columns and rename them for clarity
664
+ temp_df = temp_df[[
665
+ "KP", "Length", "Route Type", "KP From", "KP To", "Point ID From", "Point ID To",
666
+ "Bend Radius", "muax Array", "muax CDF Array",
667
+ "mul HT Array", "mul HT CDF Array", "mul OP Array", "mul OP CDF Array",
668
+ "HOOS X Array", "HOOS CDF Array", "sw Installation", "sw Hydrotest", "sw Operation",
669
+ "SChar HT", "SChar OP", "Sv HT", "Sv OP", "RCM Buckling Force", "RLT", "FRF HT",
670
+ "FRF OP Pressure", "FRF OP Temperature", "FRF OP", "Residual Buckle Length Hydrotest",
671
+ "Residual Buckle Force Hydrotest", "Residual Buckle Length Operation",
672
+ "Residual Buckle Force Operation", "Section Count", "KP Section", "Reference Section",
673
+ "Axial Mean", "Lateral Hydrotest Mean", "Lateral Operation Mean", "HOOS Mean"
674
+ ]]
675
+
676
+ temp_df = temp_df.rename(columns={
677
+ "sw Installation": "sw IN",
678
+ "sw Hydrotest": "sw HT",
679
+ "sw Operation": "sw OP",
680
+ "Residual Buckle Length Hydrotest": "buckleLength HT",
681
+ "Residual Buckle Force Hydrotest": "buckleEAF HT",
682
+ "Residual Buckle Length Operation": "buckleLength OP",
683
+ "Residual Buckle Force Operation": "buckleEAF OP"
684
+ })
685
+
686
+ # Convert route type strings to numerical representation
687
+ temp_df.loc[temp_df["Route Type"] == "Straight", "Route Type"] = 1
688
+ temp_df.loc[temp_df["Route Type"] == "Bend", "Route Type"] = 2
689
+ temp_df.loc[temp_df["Route Type"] == "Sleeper", "Route Type"] = 3
690
+ temp_df.loc[temp_df["Route Type"] == "RCM", "Route Type"] = 4
691
+ temp_df["Route Type"] = temp_df["Route Type"].astype(float)
692
+
693
+ # Fill missing values with 0
694
+ temp_df = temp_df.fillna(0)
695
+
696
+ # Add scenario parameters to the DataFrame
697
+ temp_df["Pipeline"] = self.scen_df["Pipeline"].values[0]
698
+ temp_df["Scenario"] = self.scen_df["Scenario"].values[0]
699
+ temp_df["Layout Set"] = self.scen_df["Layout Set"].values[0]
700
+ temp_df["Simulations"] = self.scen_df["Simulations"].values[0]
701
+ temp_df["Friction Sampling"] = self.scen_df["Friction Sampling"].values[0]
702
+ temp_df["Char. Friction Prob."] = self.scen_df["Char. Friction Prob."].values[0]
703
+
704
+ self.scen_df = temp_df.copy()
705
+
706
+ def calc_pp_data(self):
707
+ """
708
+ Calculate post-processing data set for a given layout set.
709
+
710
+ Parameters
711
+ ----------
712
+ df : pandas.DataFrame
713
+ DataFrame containing post-processing data.
714
+ np_array : numpy.ndarray
715
+ NumPy array containing pipeline end boundary conditions.
716
+ pipeline_id : str
717
+ Identifier of the pipeline.
718
+ layout_set : str
719
+ Identifier of the layout set.
720
+
721
+ Returns
722
+ -------
723
+ df : pandas.DataFrame
724
+ DataFrame containing calculated post-processing data.
725
+
726
+ Notes
727
+ -----
728
+ This function filters the DataFrame based on the layout set. It resets the index, renames
729
+ columns, and selects relevant columns. Adjusts the last 'KP_to' value if it is smaller
730
+ than the maximum value in np_array. Converts data types of columns to appropriate numeric
731
+ types.
732
+ """
733
+
734
+ # Reset index, rename columns, and select relevant columns
735
+ self.pp_df = self.pp_df.reset_index(drop=True).rename(columns={
736
+ 'Post-Processing Set': 'pp_set',
737
+ 'KP From': 'KP_from',
738
+ 'KP To': 'KP_to',
739
+ 'Post-Processing Description': 'description'
740
+ })
741
+ self.pp_df = self.pp_df[
742
+ ['pp_set', 'KP_from', 'KP_to', 'description', 'Characteristic VAS Probability']
743
+ ]
744
+
745
+ # Convert columns to appropriate numeric types
746
+ self.pp_df['pp_set'] = self.pp_df['pp_set'].astype(np.int64)
747
+ self.pp_df['KP_from'] = self.pp_df['KP_from'].astype(np.float64)
748
+ self.pp_df['KP_to'] = self.pp_df['KP_to'].astype(np.float64)
749
+
750
+ def calc_monte_carlo_data(self):
751
+ """
752
+ Convert the scenario data and end boundary conditions data to NumPy arrays for
753
+ Monte Carlo simulations.
754
+
755
+ Parameters
756
+ ----------
757
+ scen_df : pandas.DataFrame
758
+ DataFrame containing the scenario data.
759
+ route_ends_df : pandas.DataFrame
760
+ DataFrame containing the end boundary conditions data.
761
+
762
+ Returns
763
+ -------
764
+ dist_np : numpy.ndarray
765
+ 2D array with probabilistic distributions (rows) along the route mesh (columns).
766
+ scen_np : numpy.ndarray
767
+ 2D array with scenario properties (rows) along the route mesh (columns).
768
+ ends_np : numpy.ndarray
769
+ 2D array with end properties (rows) for the ends.
770
+
771
+ Notes
772
+ -----
773
+ The arrays have the following row layout (index : meaning):
774
+
775
+ scen_np:
776
+ - 0 : KP
777
+ - 1 : LENGTH
778
+ - 2 : ROUTE_TYPE
779
+ - 3 : BEND_RADIUS
780
+ - 4 : SW_INST
781
+ - 5 : SW_HT
782
+ - 6 : SW_OP
783
+ - 7 : SCHAR_HT
784
+ - 8 : SCHAR_OP
785
+ - 9 : SV_HT
786
+ - 10 : SV_OP
787
+ - 11 : CBF_RCM
788
+ - 12 : RLT
789
+ - 13 : FRF_HT
790
+ - 14 : FRF_P_OP
791
+ - 15 : FRF_T_OP
792
+ - 16 : FRF_OP
793
+ - 17 : L_BUCKLE_HT
794
+ - 18 : EAF_BUCKLE_HT
795
+ - 19 : L_BUCKLE_OP
796
+ - 20 : EAF_BUCKLE_OP
797
+ - 21 : SECTION_ID
798
+ - 22 : SECTION_KP
799
+ - 23 : SECTION_REF
800
+ - 24 : MUAX_MEAN
801
+ - 25 : MULAT_HT_MEAN
802
+ - 26 : MULAT_OP_MEAN
803
+ - 27 : HOOS_MEAN
804
+
805
+ dist_np:
806
+ - 0 : MUAX_ARRAY
807
+ - 1 : MUAX_CDF_ARRAY
808
+ - 2 : MULAT_ARRAY_HT
809
+ - 3 : MULAT_CDF_ARRAY_HT
810
+ - 4 : MULAT_ARRAY_OP
811
+ - 5 : MULAT_CDF_ARRAY_OP
812
+ - 6 : HOOS_ARRAY
813
+ - 7 : HOOS_CDF_ARRAY
814
+
815
+ ends_np:
816
+ - 0 : ROUTE_TYPE
817
+ - 1 : KP_FROM
818
+ - 2 : KP_TO
819
+ - 3 : REAC_INST
820
+ - 4 : REAC_HT
821
+ - 5 : REAC_OP
822
+ """
823
+
824
+ # Create a list to store the distribution arrays and define their column labels
825
+ dist_list = []
826
+ dist_list_columns = [
827
+ "muax Array",
828
+ "muax CDF Array",
829
+ "mul HT Array",
830
+ "mul HT CDF Array",
831
+ "mul OP Array",
832
+ "mul OP CDF Array",
833
+ "HOOS X Array",
834
+ "HOOS CDF Array"
835
+ ]
836
+
837
+ # Loop through the distribution columns and convert each column to a list
838
+ for list_label in dist_list_columns:
839
+ dist_list_temp = []
840
+ for i in range(self.scen_df[list_label].size):
841
+ dist_list_temp.append(self.scen_df[list_label][i])
842
+ dist_list.append(dist_list_temp)
843
+
844
+ # Convert the list of distribution arrays to a NumPy array
845
+ self.dist_np = np.array(dist_list, dtype="float64")
846
+
847
+ # Add extra columns to remove
848
+ dist_array_columns_drop = [
849
+ "Pipeline", "Scenario", "Simulations", "Friction Sampling", "Char. Friction Prob.",
850
+ "KP From", "KP To", "Point ID From", "Point ID To"
851
+ ]
852
+ dist_array_columns_drop = np.append(dist_array_columns_drop, dist_list_columns)
853
+
854
+ # Convert scenario properties to numpy array
855
+ self.scen_np = self.scen_df.drop(dist_array_columns_drop, axis=1).to_numpy().transpose()
856
+
857
+ # Convert end properties to numpy array
858
+ self.ends_np = self.route_ends_df.to_numpy().transpose()
859
+
860
+ def apply_route_mitigation(self):
861
+ """
862
+ Function to combine rows from route and mitigation, then sort by KP From.
863
+
864
+ Parameters
865
+ ----------
866
+ route_df : pandas Dataframe
867
+ Dataframe containing the route data.
868
+ mitigation_df : pandas Dataframe
869
+ Dataframe containing the mitigation data.
870
+
871
+ Returns
872
+ -------
873
+ route_df : pandas Dataframe
874
+ Dataframe containing the combined route and mitigation data, sorted by KP From.
875
+ """
876
+
877
+ rows = []
878
+
879
+ for _, r in self.route_df.iterrows():
880
+
881
+ # Route segment start and end KP and point IDs
882
+ seg_start = r["KP From"]
883
+ seg_end = r["KP To"]
884
+ seg_from_point = r["Point ID From"]
885
+
886
+ # Mitigation rows that overlap this route segment
887
+ overlaps = self.mitigation_df[
888
+ (self.mitigation_df["KP To"] > seg_start) &
889
+ (self.mitigation_df["KP From"] < seg_end)
890
+ ].sort_values("KP From")
891
+
892
+ for _, m in overlaps.iterrows():
893
+
894
+ # Calculate the overlapping KP range between the route and mitigation
895
+ m_from = max(seg_start, m["KP From"])
896
+ m_to = min(seg_end, m["KP To"])
897
+ if m_to <= m_from:
898
+ continue
899
+
900
+ # Part before mitigation
901
+ if m_from > seg_start:
902
+ pre = r.copy()
903
+ pre["KP From"] = seg_start
904
+ pre["KP To"] = m_from
905
+ pre["Point ID From"] = seg_from_point
906
+ pre["Point ID To"] = m["Point ID From"]
907
+ rows.append(pre)
908
+
909
+ # Mitigation part (override key fields from mitigation)
910
+ mid = r.copy()
911
+ mid["KP From"] = m_from
912
+ mid["KP To"] = m_to
913
+
914
+ # Copy every mitigation column except the KP boundaries, which are determined
915
+ # by the overlap with the route segment.
916
+ for col in m.index:
917
+ if col not in {"KP From", "KP To"}:
918
+ mid[col] = m[col]
919
+
920
+ rows.append(mid)
921
+
922
+ seg_start = m_to
923
+ seg_from_point = m["Point ID To"]
924
+
925
+ # Part after last mitigation
926
+ if seg_start < seg_end:
927
+ post = r.copy()
928
+ post["KP From"] = seg_start
929
+ post["KP To"] = seg_end
930
+ post["Point ID From"] = seg_from_point
931
+ rows.append(post)
932
+
933
+ self.route_df = (
934
+ pd.DataFrame(rows)
935
+ .sort_values("KP From", kind="mergesort")
936
+ .reset_index(drop=True)
937
+ )
938
+
939
+ def apply_route_soil_zoning(self):
940
+ """
941
+ Function to combine rows from route and soil zoning, then sort by KP From.
942
+
943
+ Parameters
944
+ ----------
945
+ route_df : pandas Dataframe
946
+ Dataframe containing the route data.
947
+ soil_zoning_df : pandas Dataframe
948
+ Dataframe containing the soil zoning data.
949
+
950
+ Returns
951
+ -------
952
+ route_df : pandas Dataframe
953
+ Dataframe containing the combined route and soil zoning data, sorted by KP From.
954
+ """
955
+
956
+ # Copy the route and soil zoning dataframes
957
+ route = self.route_df.copy()
958
+ zones_all = self.soil_zoning_df.copy()
959
+ zones = zones_all.iloc[1:].copy()
960
+
961
+ base_friction = zones_all.iloc[0]["Friction Set"]
962
+
963
+ rows = []
964
+
965
+ for _, r in route.iterrows():
966
+
967
+ original_start = r["KP From"]
968
+ original_end = r["KP To"]
969
+
970
+ seg_start = r["KP From"]
971
+ seg_end = r["KP To"]
972
+ current_friction = base_friction
973
+
974
+ # Zones overlapping this route segment
975
+ overlaps = zones[
976
+ (zones["KP To"] > seg_start) &
977
+ (zones["KP From"] < seg_end)
978
+ ].sort_values("KP From")
979
+
980
+ # No overlap: keep whole segment with current/base friction
981
+ if overlaps.empty:
982
+ row = r.copy()
983
+ row["Friction Set"] = current_friction
984
+ rows.append(row)
985
+ continue
986
+
987
+ for _, z in overlaps.iterrows():
988
+ z_from = max(seg_start, z["KP From"])
989
+ z_to = min(seg_end, z["KP To"])
990
+ if z_to <= z_from:
991
+ continue
992
+
993
+ # Before zone: keep previous friction
994
+ if z_from > seg_start:
995
+ pre = r.copy()
996
+ pre["KP From"] = seg_start
997
+ pre["KP To"] = z_from
998
+ pre["Friction Set"] = current_friction
999
+ pre["Point ID From"] = (
1000
+ r["Point ID From"] if seg_start == original_start else "Soil Change"
1001
+ )
1002
+ pre["Point ID To"] = "Soil Change"
1003
+ rows.append(pre)
1004
+
1005
+ # Inside zone: apply zone friction
1006
+ mid = r.copy()
1007
+ mid["KP From"] = z_from
1008
+ mid["KP To"] = z_to
1009
+ mid["Friction Set"] = z["Friction Set"]
1010
+ mid["Point ID From"] = (
1011
+ r["Point ID From"] if z_from == original_start else "Soil Change"
1012
+ )
1013
+ mid["Point ID To"] = (
1014
+ r["Point ID To"] if z_to == original_end else "Soil Change"
1015
+ )
1016
+ rows.append(mid)
1017
+
1018
+ seg_start = z_to
1019
+ current_friction = z["Friction Set"]
1020
+
1021
+ # Tail after last overlapping zone
1022
+ if seg_start < seg_end:
1023
+ post = r.copy()
1024
+ post["KP From"] = seg_start
1025
+ post["KP To"] = seg_end
1026
+ post["Friction Set"] = current_friction
1027
+ post["Point ID From"] = (
1028
+ r["Point ID From"] if seg_start == original_start else "Soil Change"
1029
+ )
1030
+ post["Point ID To"] = r["Point ID To"]
1031
+ rows.append(post)
1032
+
1033
+ self.route_df = (
1034
+ pd.DataFrame(rows)
1035
+ .sort_values("KP From", kind="mergesort")
1036
+ .reset_index(drop=True)
1037
+ )
1038
+
1039
+ def build_oper_kp_mesh_from_route(self, route_df):
1040
+ """
1041
+ Function to expand the KP array with 1000 intervals from 1000 to nearest maximum KP.
1042
+
1043
+ Parameters
1044
+ ----------
1045
+ route_df : pandas Dataframe
1046
+ Dataframe containing the route data.
1047
+
1048
+ Returns
1049
+ -------
1050
+ route_df : pandas Dataframe
1051
+ Dataframe containing the route data with expanded KP values, calculated lengths,
1052
+ element numbers, and element sizes.
1053
+ """
1054
+
1055
+ # Rename kp_col to "KP From"
1056
+ route_df = route_df.rename(columns = {"KP To": "KP From"})
1057
+
1058
+ # Expand the KP array with 1000 intervals from 1000 to nearest maximum KP
1059
+ max_kp = np.floor(route_df["KP From"].max() / 1000.0) * 1000.0
1060
+ kp_array = np.arange(1000, max_kp + 1.0, 1000)
1061
+
1062
+ # Create a dataframe for the expanded kp
1063
+ expand_df = pd.DataFrame({"Point ID From": [np.nan] * len(kp_array), "KP From": kp_array})
1064
+ route_df = pd.concat(
1065
+ [route_df, expand_df], ignore_index = True
1066
+ ).sort_values(by = "KP From").drop_duplicates("KP From").reset_index(drop = True).ffill()
1067
+
1068
+ # Calculate relative length between KP and KP To
1069
+ route_df["KP To"] = route_df["KP From"].shift(-1)
1070
+ route_df = route_df.dropna()
1071
+ route_df["Length"] = route_df["KP To"] - route_df["KP From"]
1072
+
1073
+ # Calculate element number and element size
1074
+ route_df["Elem No."] = np.ceil(route_df["Length"] / 100.0)
1075
+ route_df["Elem Size"] = route_df["Length"] / route_df["Elem No."]
1076
+
1077
+ return route_df
1078
+
1079
+ def build_oper_element_kp_array(self, route_df):
1080
+ """
1081
+ Function to create element array based on KP, KP TO and element number.
1082
+
1083
+ Parameters
1084
+ ----------
1085
+ route_df : pandas Dataframe
1086
+ Dataframe containing the route data with expanded KP values, calculated lengths,
1087
+ element numbers, and element sizes.
1088
+
1089
+ Returns
1090
+ -------
1091
+ elem_array : numpy Array
1092
+ """
1093
+
1094
+ # Create the elements between each KP points
1095
+ elem_values = []
1096
+
1097
+ for _, x in route_df.iterrows():
1098
+ elem_values.extend(
1099
+ np.linspace(x["KP From"], x["KP To"], int(x["Elem No."] + 1.0))
1100
+ )
1101
+
1102
+ # Convert the list of element values to a NumPy array, remove duplicates and NaN values
1103
+ elem_array = np.array(elem_values, dtype=float)
1104
+ elem_array = np.unique(elem_array)
1105
+ elem_array = elem_array[~np.isnan(elem_array)]
1106
+
1107
+ return elem_array
1108
+
1109
+ def interpolate_oper_profile_on_kp(self, elem_array):
1110
+ """
1111
+ Function to interpolate the RLT, pressure and temperature using KP and operating profile.
1112
+
1113
+ Parameters
1114
+ ----------
1115
+ elem_array : numpy Array
1116
+ Array containing the KP values for interpolation.
1117
+
1118
+ Returns
1119
+ -------
1120
+ oper_df : pandas Dataframe
1121
+ Dataframe containing the interpolated RLT, pressure and temperature values based on KP and operating profile.
1122
+ """
1123
+
1124
+ # Define the columns to interpolate
1125
+ interp_columns = [
1126
+ "Pressure Installation",
1127
+ "Pressure Hydrotest",
1128
+ "Pressure Operation",
1129
+ "Temperature Installation",
1130
+ "Temperature Hydrotest",
1131
+ "Temperature Operation",
1132
+ "RLT",
1133
+ ]
1134
+
1135
+ # Create a dataframe for the interpolated values
1136
+ interp_df= pd.DataFrame({"KP": elem_array})
1137
+
1138
+ # Interpolate the RLT, pressure and temperature using KP and operating profile
1139
+ for column in interp_columns:
1140
+ interp_df[column] = np.interp(interp_df["KP"], self.oper_df["KP"], self.oper_df[column])
1141
+
1142
+ self.oper_df = interp_df.copy()