Source code for pysubsea.lateral_buckling_tools

'''
This module provides classes and functions for lateral buckling calculations and friction factor
distribution fitting for subsea pipelines.

**Features:**

- The `LBForceDistributions` class calculates lateral buckling forces.
- The `LBSoilDistributions` class implements lognormal distribution fitting for geotechnical
  friction factors, supporting low, best, and high estimates (LE, BE, HE) and multiple fit
  types.
  Designed for use in pipeline lateral buckling reliability analysis and geotechnical parameter
  estimation.
- All calculations are vectorized using NumPy and leverage SciPy for statistical fitting.

.. raw:: html

   <hr style="height:6px; background-color:#888; border:none; margin:1.5em 0;" />

'''

import numpy as np
from scipy.stats import lognorm
from scipy.optimize import minimize
from .linepipe_tools import Pipe

[docs] class LBForceDistributions: # pylint: disable=too-many-instance-attributes, too-many-arguments """ Class for lateral buckling force calculations. Parameters ---------- oos_section_type : str or array-like, optional Out-of-straightness (OOS) section type: 'straight', 'curve', or 'sleeper'. oos_factor_mean : float or array-like, optional Mean OOS factor for the condition of interest. oos_factor_std : float or array-like, optional Standard deviation of the OOS factor for the condition of interest. oos_reference_length : float or array-like, optional Reference length for the OOS section. friction_factor_mean : float or array-like, optional Mean friction factor for the condition of interest. friction_factor_std : float or array-like, optional Standard deviation of the friction factor for the condition of interest. outer_diameter : float or array-like, optional Pipe outer diameter, used to compute section properties from `Pipe`. wall_thickness : float or array-like, optional Pipe wall thickness, used to compute section properties from `Pipe`. youngs_modulus : float or array-like, optional Young's modulus of the material. submerged_weight : float or array-like, optional Submerged weight for the condition of interest. curve_radius : float or array-like, optional Curve radius for the OOS section, if applicable. sleeper_height : float or array-like, optional Sleeper height for the OOS section, if applicable. """ def __init__( self, *, oos_section_type=None, oos_factor_mean=0.0, oos_factor_std=0.0, oos_reference_length=0.0, friction_factor_mean=0.0, friction_factor_std=0.0, outer_diameter=0.0, wall_thickness=0.0, youngs_modulus=0.0, submerged_weight=0.0, curve_radius=0.0, sleeper_height=0.0 ): """ Initialize with section, material, and submerged weight properties. """ self.oos_section_type = np.asarray(oos_section_type, dtype = object) self.oos_factor_mean = np.asarray(oos_factor_mean, dtype = float) self.oos_factor_std = np.asarray(oos_factor_std, dtype = float) self.oos_reference_length = np.asarray(oos_reference_length, dtype = object) self.friction_factor_mean = np.asarray(friction_factor_mean, dtype = object) self.friction_factor_std = np.asarray(friction_factor_std, dtype = object) self.outer_diameter = np.asarray(outer_diameter, dtype = float) self.wall_thickness = np.asarray(wall_thickness, dtype = float) self.youngs_modulus = np.asarray(youngs_modulus, dtype = float) self.submerged_weight = np.asarray(submerged_weight, dtype = float) self.curve_radius = np.asarray(curve_radius, dtype = object) self.sleeper_height = np.asarray(sleeper_height, dtype = object) def _section_properties(self): """ Calculate the steel cross-sectional area and area moment of inertia from `Pipe`. Returns ------- steel_area : np.ndarray Steel cross-sectional area. area_moment_inertia : np.ndarray Area moment of inertia. """ pipe = Pipe( outer_diameter=self.outer_diameter, wall_thickness=self.wall_thickness, youngs_modulus=self.youngs_modulus ) steel_area = pipe.steel_area() bending_stiffness = pipe.bending_stiffness() return steel_area, bending_stiffness
[docs] def characteristic_buckling_force(self): """ Compute characteristic buckling force. Returns ------- characteristic_buckling_force : np.ndarray Characteristic lateral buckling force. Examples -------- >>> lb = LBForceDistributions( ... outer_diameter=[0.2731, 0.3239], ... wall_thickness=[0.0127, 0.0159], ... youngs_modulus=[207.0e+09, 207.0e+09], ... submerged_weight=[695.39794758, 1029.76124826] ... ) >>> lb.characteristic_buckling_force() array([ 839099.6561..., 1351458.0306...]) """ steel_area, bending_stiffness = self._section_properties() return ( 2.26 * (self.youngs_modulus * steel_area) ** 0.25 * bending_stiffness ** 0.25 * self.submerged_weight ** 0.5 )
[docs] def nominal_straight_section_buckling_force(self): """ Compute the mean nominal straight-section buckling force. This is calculated as the product of the mean OOS factor, the square root of the mean friction factor, and the characteristic buckling force. """ return ( self.oos_factor_mean * np.sqrt(self.friction_factor_mean) * self.characteristic_buckling_force() )
@staticmethod def _sqrt_lognorm_moments(mean_value, std_value): """ Compute mean and standard deviation of sqrt(X) from mean and std of lognormal X. """ sigma2_ln = np.log(1.0 + (std_value**2) / (mean_value**2)) mu_ln = np.log(mean_value) - 0.5 * sigma2_ln sqrt_mean = np.exp(0.5 * mu_ln + 0.125 * sigma2_ln) sqrt_var = np.exp(mu_ln + 0.25 * sigma2_ln) * (np.exp(0.25 * sigma2_ln) - 1.0) sqrt_std = np.sqrt(np.maximum(sqrt_var, 0.0)) return sqrt_mean, sqrt_std
[docs] def route_curve_buckling_force(self): """ Compute the mean route-curve buckling force. This is calculated as the product of the mean OOS factor, the mean friction factor, the submerged weight, and the curve radius. """ return ( self.oos_factor_mean * self.friction_factor_mean * self.submerged_weight * self.curve_radius )
[docs] def sleeper_buckling_force(self): """ Compute the mean sleeper buckling force. This is calculated as the product of the mean OOS factor and ``4 * sqrt(bending_stiffness * submerged_weight / sleeper_height)``. """ _, bending_stiffness = self._section_properties() return ( self.oos_factor_mean * 4.0 * np.sqrt( bending_stiffness * self.submerged_weight / self.sleeper_height ) )
@staticmethod def _lognorm_parameters(mean_value, std_value): """ Compute lognormal location and scale parameters from mean and standard deviation. """ scale_param = np.sqrt(np.log(1 + std_value**2 / mean_value**2)) location_param = np.log(mean_value**2 / np.sqrt(mean_value**2 + std_value**2)) return location_param, scale_param @staticmethod def _fitted_values(location_param, scale_param, scale=1.0): """ Compute LE/BE/HE fitted values from lognormal parameters. """ le_fit = scale * lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.05) be_fit = scale * lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.50) he_fit = scale * lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.95) return le_fit, be_fit, he_fit @staticmethod def _calc_lognorm_product_distribution(mean_value, std_value): """ Compute a lognormal distribution curve from mean and standard deviation. Returns ------- distribution_range : np.ndarray Range spanning the 0.01% to 99.99% quantiles. distribution_cdf : np.ndarray CDF values corresponding to the returned range. distribution_pdf : np.ndarray PDF values corresponding to the returned range. """ location_param, scale_param = LBForceDistributions._lognorm_parameters( mean_value, std_value ) lower = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.0001) upper = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.9999) distribution_range = np.linspace(lower, upper, 10000) distribution_cdf = lognorm.cdf( distribution_range, scale_param, 0.0, np.exp(location_param) ) distribution_pdf = lognorm.pdf( distribution_range, scale_param, 0.0, np.exp(location_param) ) return distribution_range, distribution_cdf, distribution_pdf def _single_case_product_distribution(self, mean_value, std_value): """ Compute one distribution from a mean and standard deviation. """ location_param, scale_param = self._lognorm_parameters(mean_value, std_value) le_fit, be_fit, he_fit = self._fitted_values(location_param, scale_param) ( distribution_range, distribution_cdf, distribution_pdf, ) = self._calc_lognorm_product_distribution( mean_value, std_value ) return ( mean_value, std_value, location_param, scale_param, le_fit, be_fit, he_fit, distribution_range, distribution_cdf, distribution_pdf )
[docs] def nominal_straight_buckling_force_distribution_parameters(self): """ Compute nominal straight-section buckling force distribution parameters. The nominal straight-section buckling force is treated as the product of the OOS factor, the square root of friction factor, and the characteristic buckling force. Returns ------- mean_nominal_straight_section_buckling : np.ndarray Array of mean nominal straight-section buckling force values. std_nominal_straight_section_buckling : np.ndarray Array of standard deviation values. location_param : np.ndarray Array of location parameters of the lognormal distribution. scale_param : np.ndarray Array of scale parameters of the lognormal distribution. le_fit : np.ndarray Array of fitted 5th-percentile values. be_fit : np.ndarray Array of fitted 50th-percentile values. he_fit : np.ndarray Array of fitted 95th-percentile values. nominal_straight_section_buckling_range : np.ndarray 2D array with shape (n_cases, 10000), one range per case. nominal_straight_section_buckling_cdf : np.ndarray 2D array with shape (n_cases, 10000), one CDF per case. nominal_straight_section_buckling_pdf : np.ndarray 2D array with shape (n_cases, 10000), one PDF per case. Notes ----- This method mirrors the output structure of `LBOOSDistributions`, but the mean and standard deviation are propagated through the product of OOS factor, friction factor, and characteristic buckling force instead of being fitted from discrete estimates. """ mean_value_list = [] std_value_list = [] location_param_list = [] scale_param_list = [] le_fit_list = [] be_fit_list = [] he_fit_list = [] nominal_straight_section_buckling_range_list = [] nominal_straight_section_buckling_cdf_list = [] nominal_straight_section_buckling_pdf_list = [] characteristic_force = self.characteristic_buckling_force() for oos_factor_mean, oos_factor_std in zip( self.oos_factor_mean, self.oos_factor_std ): sqrt_friction_mean, sqrt_friction_std = self._sqrt_lognorm_moments( self.friction_factor_mean, self.friction_factor_std ) mean_value = characteristic_force * oos_factor_mean * sqrt_friction_mean variance_factor = ( (oos_factor_std**2 + oos_factor_mean**2) * (sqrt_friction_std**2 + sqrt_friction_mean**2) - (oos_factor_mean**2 * sqrt_friction_mean**2) ) variance_factor = np.maximum(variance_factor, 0.0) std_value = characteristic_force * np.sqrt(variance_factor) ( mean_value, std_value, location_param, scale_param, le_fit, be_fit, he_fit, nominal_straight_section_buckling_range, nominal_straight_section_buckling_cdf, nominal_straight_section_buckling_pdf ) = self._single_case_product_distribution(mean_value, std_value) mean_value_list.append(mean_value) std_value_list.append(std_value) location_param_list.append(location_param) scale_param_list.append(scale_param) le_fit_list.append(le_fit) be_fit_list.append(be_fit) he_fit_list.append(he_fit) nominal_straight_section_buckling_range_list.append( nominal_straight_section_buckling_range ) nominal_straight_section_buckling_cdf_list.append( nominal_straight_section_buckling_cdf ) nominal_straight_section_buckling_pdf_list.append( nominal_straight_section_buckling_pdf ) mean_value = np.array(mean_value_list) std_value = np.array(std_value_list) location_param = np.array(location_param_list) scale_param = np.array(scale_param_list) le_fit = np.array(le_fit_list) be_fit = np.array(be_fit_list) he_fit = np.array(he_fit_list) nominal_straight_section_buckling_range = np.array( nominal_straight_section_buckling_range_list ) nominal_straight_section_buckling_cdf = np.array( nominal_straight_section_buckling_cdf_list ) nominal_straight_section_buckling_pdf = np.array( nominal_straight_section_buckling_pdf_list ) return ( mean_value, std_value, location_param, scale_param, le_fit, be_fit, he_fit, nominal_straight_section_buckling_range, nominal_straight_section_buckling_cdf, nominal_straight_section_buckling_pdf )
[docs] def route_curve_buckling_force_distribution_parameters(self): """ Compute route-curve buckling forcedistribution parameters. The route-curve buckling force is treated as the product of the OOS factor, the friction factor, the submerged weight, and the curve radius. Returns ------- mean_route_curve_buckling : np.ndarray Array of mean route-curve buckling force values. std_route_curve_buckling : np.ndarray Array of standard deviation values. location_param : np.ndarray Array of location parameters of the lognormal distribution. scale_param : np.ndarray Array of scale parameters of the lognormal distribution. le_fit : np.ndarray Array of fitted 5th-percentile values. be_fit : np.ndarray Array of fitted 50th-percentile values. he_fit : np.ndarray Array of fitted 95th-percentile values. route_curve_buckling_range : np.ndarray 2D array with shape (n_cases, 10000), one range per case. route_curve_buckling_cdf : np.ndarray 2D array with shape (n_cases, 10000), one CDF per case. route_curve_buckling_pdf : np.ndarray 2D array with shape (n_cases, 10000), one PDF per case. Notes ----- This method mirrors the output structure of `LBOOSDistributions`, but the mean and standard deviation are propagated through the product of OOS factor, friction factor, submerged weight, and curve radius instead of being fitted from discrete estimates. """ mean_value_list = [] std_value_list = [] location_param_list = [] scale_param_list = [] le_fit_list = [] be_fit_list = [] he_fit_list = [] route_curve_buckling_range_list = [] route_curve_buckling_cdf_list = [] route_curve_buckling_pdf_list = [] for oos_factor_mean, oos_factor_std in zip( self.oos_factor_mean, self.oos_factor_std ): mean_value = ( oos_factor_mean * self.friction_factor_mean * self.submerged_weight * self.curve_radius ) variance_factor = ( (oos_factor_std**2 + oos_factor_mean**2) * (self.friction_factor_std**2 + self.friction_factor_mean**2) - (oos_factor_mean**2 * self.friction_factor_mean**2) ) variance_factor = np.maximum(variance_factor, 0.0) std_value = self.submerged_weight * self.curve_radius * np.sqrt(variance_factor) ( mean_value, std_value, location_param, scale_param, le_fit, be_fit, he_fit, route_curve_buckling_range, route_curve_buckling_cdf, route_curve_buckling_pdf ) = self._single_case_product_distribution(mean_value, std_value) mean_value_list.append(mean_value) std_value_list.append(std_value) location_param_list.append(location_param) scale_param_list.append(scale_param) le_fit_list.append(le_fit) be_fit_list.append(be_fit) he_fit_list.append(he_fit) route_curve_buckling_range_list.append(route_curve_buckling_range) route_curve_buckling_cdf_list.append(route_curve_buckling_cdf) route_curve_buckling_pdf_list.append(route_curve_buckling_pdf) mean_value = np.array(mean_value_list) std_value = np.array(std_value_list) location_param = np.array(location_param_list) scale_param = np.array(scale_param_list) le_fit = np.array(le_fit_list) be_fit = np.array(be_fit_list) he_fit = np.array(he_fit_list) route_curve_buckling_range = np.array(route_curve_buckling_range_list) route_curve_buckling_cdf = np.array(route_curve_buckling_cdf_list) route_curve_buckling_pdf = np.array(route_curve_buckling_pdf_list) return ( mean_value, std_value, location_param, scale_param, le_fit, be_fit, he_fit, route_curve_buckling_range, route_curve_buckling_cdf, route_curve_buckling_pdf )
[docs] def sleeper_buckling_force_distribution_parameters(self): """ Compute sleeper buckling force distribution parameters. The sleeper buckling force is treated as: ``oos_factor * 4 * sqrt(bending_stiffness * submerged_weight / sleeper_height)``. Returns ------- mean_sleeper_buckling : np.ndarray Array of mean sleeper buckling force values. std_sleeper_buckling : np.ndarray Array of standard deviation values. location_param : np.ndarray Array of location parameters of the lognormal distribution. scale_param : np.ndarray Array of scale parameters of the lognormal distribution. le_fit : np.ndarray Array of fitted 5th-percentile values. be_fit : np.ndarray Array of fitted 50th-percentile values. he_fit : np.ndarray Array of fitted 95th-percentile values. sleeper_buckling_range : np.ndarray 2D array with shape (n_cases, 10000), one range per case. sleeper_buckling_cdf : np.ndarray 2D array with shape (n_cases, 10000), one CDF per case. sleeper_buckling_pdf : np.ndarray 2D array with shape (n_cases, 10000), one PDF per case. """ mean_value_list = [] std_value_list = [] location_param_list = [] scale_param_list = [] le_fit_list = [] be_fit_list = [] he_fit_list = [] sleeper_buckling_range_list = [] sleeper_buckling_cdf_list = [] sleeper_buckling_pdf_list = [] _, bending_stiffness = self._section_properties() base_factor = 4.0 * np.sqrt( bending_stiffness * self.submerged_weight / self.sleeper_height ) for oos_factor_mean, oos_factor_std in zip( self.oos_factor_mean, self.oos_factor_std ): mean_value = oos_factor_mean * base_factor std_value = oos_factor_std * base_factor ( mean_value, std_value, location_param, scale_param, le_fit, be_fit, he_fit, sleeper_buckling_range, sleeper_buckling_cdf, sleeper_buckling_pdf ) = self._single_case_product_distribution(mean_value, std_value) mean_value_list.append(mean_value) std_value_list.append(std_value) location_param_list.append(location_param) scale_param_list.append(scale_param) le_fit_list.append(le_fit) be_fit_list.append(be_fit) he_fit_list.append(he_fit) sleeper_buckling_range_list.append(sleeper_buckling_range) sleeper_buckling_cdf_list.append(sleeper_buckling_cdf) sleeper_buckling_pdf_list.append(sleeper_buckling_pdf) mean_value = np.array(mean_value_list) std_value = np.array(std_value_list) location_param = np.array(location_param_list) scale_param = np.array(scale_param_list) le_fit = np.array(le_fit_list) be_fit = np.array(be_fit_list) he_fit = np.array(he_fit_list) sleeper_buckling_range = np.array(sleeper_buckling_range_list) sleeper_buckling_cdf = np.array(sleeper_buckling_cdf_list) sleeper_buckling_pdf = np.array(sleeper_buckling_pdf_list) return ( mean_value, std_value, location_param, scale_param, le_fit, be_fit, he_fit, sleeper_buckling_range, sleeper_buckling_cdf, sleeper_buckling_pdf )
[docs] def buckling_force_distribution_parameters(self): """ Compute section buckling force distribution parameters based on ``oos_section_type``. Valid values are ``Straight``, ``Curve``, and ``Sleeper`` (case-insensitive). Returns ------- tuple Output from: - ``nominal_straight_buckling_force_distribution_parameters`` for ``Straight`` - ``route_curve_buckling_force_distribution_parameters`` for ``Curve`` - ``sleeper_buckling_force_distribution_parameters`` for ``Sleeper`` Notes ----- Mixed section types are supported in one call. Each case is evaluated with the corresponding section formula. """ section_type = np.atleast_1d(np.asarray(self.oos_section_type, dtype=object)) if section_type.size == 0: raise ValueError( "oos_section_type must be provided as 'Straight', 'Curve', or 'Sleeper'." ) _, bending_stiffness = self._section_properties() characteristic_force = self.characteristic_buckling_force() ( section_type, oos_mean, oos_std, friction_mean, friction_std, submerged_weight, curve_radius, sleeper_height, characteristic_force, bending_stiffness ) = np.broadcast_arrays( section_type, self.oos_factor_mean, self.oos_factor_std, self.friction_factor_mean, self.friction_factor_std, self.submerged_weight, self.curve_radius, self.sleeper_height, characteristic_force, bending_stiffness, ) mean_value_list = [] std_value_list = [] location_param_list = [] scale_param_list = [] le_fit_list = [] be_fit_list = [] he_fit_list = [] distribution_range_list = [] distribution_cdf_list = [] distribution_pdf_list = [] for ( section, oos_mean_i, oos_std_i, friction_mean_i, friction_std_i, submerged_weight_i, curve_radius_i, sleeper_height_i, characteristic_force_i, bending_stiffness_i, ) in zip( section_type, oos_mean, oos_std, friction_mean, friction_std, submerged_weight, curve_radius, sleeper_height, characteristic_force, bending_stiffness, ): section_lower = str(section).strip().lower() if section_lower == "straight": sqrt_friction_mean_i, sqrt_friction_std_i = self._sqrt_lognorm_moments( friction_mean_i, friction_std_i, ) mean_value = characteristic_force_i * oos_mean_i * sqrt_friction_mean_i variance_factor = ( (oos_std_i**2 + oos_mean_i**2) * (sqrt_friction_std_i**2 + sqrt_friction_mean_i**2) - (oos_mean_i**2 * sqrt_friction_mean_i**2) ) variance_factor = max(variance_factor, 0.0) std_value = characteristic_force_i * np.sqrt(variance_factor) elif section_lower == "curve": mean_value = ( oos_mean_i * friction_mean_i * submerged_weight_i * curve_radius_i ) variance_factor = ( (oos_std_i**2 + oos_mean_i**2) * (friction_std_i**2 + friction_mean_i**2) - (oos_mean_i**2 * friction_mean_i**2) ) variance_factor = max(variance_factor, 0.0) std_value = submerged_weight_i * curve_radius_i * np.sqrt(variance_factor) elif section_lower == "sleeper": base_factor = 4.0 * np.sqrt( bending_stiffness_i * submerged_weight_i / sleeper_height_i ) mean_value = oos_mean_i * base_factor std_value = oos_std_i * base_factor else: raise ValueError( "Invalid oos_section_type. Allowed values are 'Straight', 'Curve', or 'Sleeper'." ) ( mean_value, std_value, location_param, scale_param, le_fit, be_fit, he_fit, distribution_range, distribution_cdf, distribution_pdf, ) = self._single_case_product_distribution(mean_value, std_value) mean_value_list.append(mean_value) std_value_list.append(std_value) location_param_list.append(location_param) scale_param_list.append(scale_param) le_fit_list.append(le_fit) be_fit_list.append(be_fit) he_fit_list.append(he_fit) distribution_range_list.append(distribution_range) distribution_cdf_list.append(distribution_cdf) distribution_pdf_list.append(distribution_pdf) return ( np.array(mean_value_list), np.array(std_value_list), np.array(location_param_list), np.array(scale_param_list), np.array(le_fit_list), np.array(be_fit_list), np.array(he_fit_list), np.array(distribution_range_list), np.array(distribution_cdf_list), np.array(distribution_pdf_list), )
[docs] class LBOOSDistributions: # pylint: disable=too-many-instance-attributes, too-many-arguments """ Class for lateral buckling calculations, including out-of-straightness (OOS) distribution fitting. Parameters ---------- oos_factor_mean : float, optional Mean OOS factor for the condition of interest. oos_factor_std : float, optional Standard deviation of the OOS factor for the condition of interest. """ def __init__( self, *, oos_factor_mean, oos_factor_std ): """ Initialize with OOS factor mean and standard deviation. """ self.oos_factor_mean = np.asarray(oos_factor_mean, dtype = float) self.oos_factor_std = np.asarray(oos_factor_std, dtype = float) @staticmethod def _lognorm_parameters(oos_factor_mean, oos_factor_std): """ Compute lognormal location and scale parameters from OOS mean and standard deviation. """ scale_param = np.sqrt(np.log(1 + oos_factor_std**2 / oos_factor_mean**2)) location_param = np.log( oos_factor_mean**2 / np.sqrt(oos_factor_mean**2 + oos_factor_std**2) ) return location_param, scale_param @staticmethod def _fitted_values(location_param, scale_param): """ Compute LE/BE/HE OOS values from lognormal parameters. """ le_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.05) be_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.50) he_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.95) return le_fit, be_fit, he_fit @staticmethod def _calc_lognorm_oos(oos_factor_mean, oos_factor_std): """ Compute a lognormal OOS curve from mean and standard deviation. Parameters ---------- oos_factor_mean : float Mean value of the OOS factor. oos_factor_std : float Standard deviation of the OOS factor. Returns ------- oos_factor_range : np.ndarray OOS-factor range spanning the 0.01% to 99.99% quantiles. oos_factor_cdf : np.ndarray CDF values corresponding to `oos_factor_range`. """ location_param, scale_param = LBOOSDistributions._lognorm_parameters( oos_factor_mean, oos_factor_std ) oos_lower = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.0001) oos_upper = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.9999) oos_factor_range = np.linspace(oos_lower, oos_upper, 10000) oos_factor_cdf = lognorm.cdf( oos_factor_range, scale_param, 0.0, np.exp(location_param) ) return oos_factor_range, oos_factor_cdf def _single_case_distribution(self, oos_factor_mean, oos_factor_std): """ Compute OOS distribution from mean and standard deviation. """ location_param, scale_param = self._lognorm_parameters( oos_factor_mean, oos_factor_std ) le_fit, be_fit, he_fit = self._fitted_values(location_param, scale_param) oos_factor_range, oos_factor_cdf = self._calc_lognorm_oos( oos_factor_mean, oos_factor_std ) return ( oos_factor_mean, oos_factor_std, location_param, scale_param, le_fit, be_fit, he_fit, oos_factor_range, oos_factor_cdf )
[docs] def oos_distribution_parameters(self): """ Compute the parameters of the OOS lognormal distribution directly from the specified mean and standard deviation. Returns ------- mean_oos : np.ndarray Array of mean OOS-factor values. std_oos : np.ndarray Array of standard deviation OOS-factor values. location_param : np.ndarray Array of location parameters of the lognormal OOS distribution. scale_param : np.ndarray Array of scale parameters of the lognormal OOS distribution. le_fit : np.ndarray Array of fitted 5th-percentile OOS values. be_fit : np.ndarray Array of fitted 50th-percentile OOS values. he_fit : np.ndarray Array of fitted 95th-percentile OOS values. oos_factor_range : np.ndarray 2D array with shape (n_cases, 10000), one range per case. oos_factor_cdf : np.ndarray 2D array with shape (n_cases, 10000), one CDF per case. Notes ----- This method mirrors the output structure of `LBSoilDistributions`, but it does not perform an optimization step because the OOS mean and standard deviation are already provided as inputs. Examples -------- >>> lb = LBOOSDistributions( ... oos_factor_mean=[1.26], ... oos_factor_std=[0.33] ... ) >>> result = lb.oos_distribution_parameters() >>> result[:7] (array([1.26]), array([0.33]), array([0.19793979]), array([0.25757303]), array([0.79793341]), array([1.218889]), array([1.86192279])) >>> result[7].shape, result[8].shape ((1, 10000), (1, 10000)) """ mean_oos_list = [] std_oos_list = [] location_param_list = [] scale_param_list = [] le_fit_list = [] be_fit_list = [] he_fit_list = [] oos_factor_range_list = [] oos_factor_cdf_list = [] for oos_factor_mean, oos_factor_std in zip( self.oos_factor_mean, self.oos_factor_std ): ( mean_oos, std_oos, location_param, scale_param, le_fit, be_fit, he_fit, oos_factor_range, oos_factor_cdf ) = self._single_case_distribution(oos_factor_mean, oos_factor_std) mean_oos_list.append(mean_oos) std_oos_list.append(std_oos) location_param_list.append(location_param) scale_param_list.append(scale_param) le_fit_list.append(le_fit) be_fit_list.append(be_fit) he_fit_list.append(he_fit) oos_factor_range_list.append(oos_factor_range) oos_factor_cdf_list.append(oos_factor_cdf) mean_oos = np.array(mean_oos_list) std_oos = np.array(std_oos_list) location_param = np.array(location_param_list) scale_param = np.array(scale_param_list) le_fit = np.array(le_fit_list) be_fit = np.array(be_fit_list) he_fit = np.array(he_fit_list) oos_factor_range = np.array(oos_factor_range_list) oos_factor_cdf = np.array(oos_factor_cdf_list) return ( mean_oos, std_oos, location_param, scale_param, le_fit, be_fit, he_fit, oos_factor_range, oos_factor_cdf )
[docs] class LBSoilDistributions: # pylint: disable=too-many-instance-attributes, too-many-arguments """ Class for lateral buckling calculations, including friction factor distribution fitting. Parameters ---------- friction_factor_le : float, optional Low estimate (LE) friction factor, representing the 5th percentile. friction_factor_be : float, optional Best estimate (BE) friction factor, representing the 50th percentile. friction_factor_he : float, optional High estimate (HE) friction factor, representing the 95th percentile. friction_factor_fit_type : str, optional Type of fit to perform: 'LE_BE_HE', 'LE_BE', or 'BE_HE'. """ def __init__( self, *, friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ): """ Initialize with geotechnical friction factor estimates and fit type. """ self.friction_factor_le = np.asarray(friction_factor_le, dtype = float) self.friction_factor_be = np.asarray(friction_factor_be, dtype = float) self.friction_factor_he = np.asarray(friction_factor_he, dtype = float) self.friction_factor_fit_type = np.asarray(friction_factor_fit_type, dtype = object) @staticmethod def _objective_rmse( params, friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ): """ Objective function used to fit lognormal parameters by RMSE minimization. """ location_param, scale_param = params if friction_factor_fit_type == 'LE_BE_HE': le_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.05) be_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.50) he_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.95) error = np.sqrt( ((le_fit - friction_factor_le)**2 + (be_fit - friction_factor_be)**2 + (he_fit - friction_factor_he)**2) / 3.0 ) elif friction_factor_fit_type == 'LE_BE': le_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.05) be_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.50) error = np.sqrt( ((le_fit - friction_factor_le)**2 + (be_fit - friction_factor_be)**2) / 2.0 ) elif friction_factor_fit_type == 'BE_HE': be_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.50) he_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.95) error = np.sqrt( ((be_fit - friction_factor_be)**2 + (he_fit - friction_factor_he)**2) / 2.0 ) else: error = np.nan return error @staticmethod def _initial_guess(friction_factor_le, friction_factor_be, friction_factor_he): """ Compute initial guess for location and scale parameters. """ initial_location = np.mean( [np.log(friction_factor_le), np.log(friction_factor_be), np.log(friction_factor_he)] ) initial_scale = np.std( [np.log(friction_factor_le), np.log(friction_factor_be), np.log(friction_factor_he)], ddof=1 ) return [initial_location, initial_scale] @staticmethod def _fitted_values(location_param, scale_param): """ Compute LE/BE/HE fitted values from lognormal parameters. """ le_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.05) be_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.50) he_fit = lognorm(scale_param, 0.0, np.exp(location_param)).ppf(0.95) return le_fit, be_fit, he_fit @staticmethod def _compute_rmse( le_fit, be_fit, he_fit, friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ): """ Compute RMSE for the selected fit type. """ if friction_factor_fit_type == 'LE_BE_HE': rmse = np.sqrt( ((le_fit - friction_factor_le)**2 + (be_fit - friction_factor_be)**2 + (he_fit - friction_factor_he)**2) / 3.0 ) elif friction_factor_fit_type == 'LE_BE': rmse = np.sqrt( ((le_fit - friction_factor_le)**2 + (be_fit - friction_factor_be)**2) / 2.0 ) elif friction_factor_fit_type == 'BE_HE': rmse = np.sqrt( ((be_fit - friction_factor_be)**2 + (he_fit - friction_factor_he)**2) / 2.0 ) else: rmse = np.nan return rmse @staticmethod def _compute_r2( le_fit, be_fit, he_fit, friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ): """ Compute the coefficient of determination R² for the selected fit type. """ if friction_factor_fit_type == 'LE_BE_HE': observed = np.array([friction_factor_le, friction_factor_be, friction_factor_he]) fitted = np.array([le_fit, be_fit, he_fit]) elif friction_factor_fit_type == 'LE_BE': observed = np.array([friction_factor_le, friction_factor_be]) fitted = np.array([le_fit, be_fit]) elif friction_factor_fit_type == 'BE_HE': observed = np.array([friction_factor_be, friction_factor_he]) fitted = np.array([be_fit, he_fit]) else: return np.nan sst = np.sum((observed - np.mean(observed))**2) if np.isclose(sst, 0.0): return np.nan ss_res = np.sum((observed - fitted)**2) return 1.0 - ss_res / sst def _fit_single_case( self, friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ): """ Fit one LE/BE/HE set and return all derived values. """ initial_guess = self._initial_guess( friction_factor_le, friction_factor_be, friction_factor_he ) result = minimize( self._objective_rmse, initial_guess, args=( friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ), method='Nelder-Mead' ) location_param, scale_param = result.x mean_friction = np.exp(location_param + scale_param**2 / 2) std_friction = np.sqrt( (np.exp(scale_param**2) - 1) * np.exp(2 * location_param + scale_param**2) ) le_fit, be_fit, he_fit = self._fitted_values(location_param, scale_param) rmse = self._compute_rmse( le_fit, be_fit, he_fit, friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ) r2 = self._compute_r2( le_fit, be_fit, he_fit, friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ) return ( mean_friction, std_friction, location_param, scale_param, le_fit, be_fit, he_fit, rmse, r2 ) @staticmethod def _calc_lognorm_soil(mu_mean, mu_std): """ Compute a lognormal friction-factor curve from mean and standard deviation. Parameters ---------- mu_mean : float Mean value of the friction factor. mu_std : float Standard deviation of the friction factor. Returns ------- friction_factor_range : np.ndarray Friction-factor range spanning the 0.01% to 99.99% quantiles. friction_factor_cdf : np.ndarray CDF values corresponding to `friction_factor_range`. """ mu_shape = np.sqrt(np.log(1 + mu_std**2 / mu_mean**2)) mu_scale = np.log(mu_mean**2 / np.sqrt(mu_mean**2 + mu_std**2)) mu_lower = lognorm(mu_shape, 0.0, np.exp(mu_scale)).ppf(0.0001) mu_upper = lognorm(mu_shape, 0.0, np.exp(mu_scale)).ppf(0.9999) friction_factor_range = np.linspace(mu_lower, mu_upper, 10000) friction_factor_cdf = lognorm.cdf( friction_factor_range, mu_shape, 0.0, np.exp(mu_scale) ) return friction_factor_range, friction_factor_cdf
[docs] def friction_distribution_parameters(self): """ Compute the parameters of the lognormal friction factor distribution (axial or lateral) by minimizing the root mean square error (RMSE) between geotechnical estimates and back-calculated friction factors from the lognormal distribution. Returns ------- mean_friction : np.ndarray Array of mean values of the lognormal friction factor distribution. std_friction : np.ndarray Array of standard deviation values of the lognormal friction factor distribution. location_param : np.ndarray Array of location parameters of the lognormal friction factor distribution. scale_param : np.ndarray Array of scale parameters of the lognormal friction factor distribution. le_fit : np.ndarray Array of fitted LE values. be_fit : np.ndarray Array of fitted BE values. he_fit : np.ndarray Array of fitted HE values. rmse : np.ndarray Array of RMSE values for the best fit type. r2 : np.ndarray Array of R² values for the best fit type. friction_factor_range : np.ndarray 2D array with shape (n_cases, 10000), one range per case. friction_factor_cdf : np.ndarray 2D array with shape (n_cases, 10000), one CDF per case. Notes ----- The function calculates the parameters of the lognormal friction factor distribution based on LE at 5th percentile, BE at 50th percentile, and HE at 95th percentile Examples -------- >>> lb = LBSoilDistributions( ... friction_factor_le=[0.5], ... friction_factor_be=[1.0], ... friction_factor_he=[1.5], ... friction_factor_fit_type=['LE_BE_HE'] ... ) >>> result = lb.friction_distribution_parameters() >>> result[:8] (array([0.9684083]), array([0.30043236]), array([-0.07804666]), array([0.3031342]), array([0.56177265]), array([0.92492127]), array([1.52282131]), array([0.05765844])) >>> result[9].shape, result[10].shape ((1, 10000), (1, 10000)) """ # Initialize lists to store results mean_friction_list = [] std_friction_list = [] location_param_list = [] scale_param_list = [] le_fit_list = [] be_fit_list = [] he_fit_list = [] rmse_list = [] r2_list = [] friction_factor_range_list = [] friction_factor_cdf_list = [] # Loop through the friction factor arrays for _, ( friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ) in enumerate( zip( self.friction_factor_le, self.friction_factor_be, self.friction_factor_he, self.friction_factor_fit_type ) ): ( mean_friction, std_friction, location_param, scale_param, le_fit, be_fit, he_fit, rmse, r2 ) = self._fit_single_case( friction_factor_le, friction_factor_be, friction_factor_he, friction_factor_fit_type ) friction_factor_range, friction_factor_cdf = self._calc_lognorm_soil( mean_friction, std_friction ) # Append results for this iteration mean_friction_list.append(mean_friction) std_friction_list.append(std_friction) location_param_list.append(location_param) scale_param_list.append(scale_param) le_fit_list.append(le_fit) be_fit_list.append(be_fit) he_fit_list.append(he_fit) rmse_list.append(rmse) r2_list.append(r2) friction_factor_range_list.append(friction_factor_range) friction_factor_cdf_list.append(friction_factor_cdf) # Convert lists to NumPy arrays mean_friction = np.array(mean_friction_list) std_friction = np.array(std_friction_list) location_param = np.array(location_param_list) scale_param = np.array(scale_param_list) le_fit = np.array(le_fit_list) be_fit = np.array(be_fit_list) he_fit = np.array(he_fit_list) rmse = np.array(rmse_list) r2 = np.array(r2_list) friction_factor_range = np.array(friction_factor_range_list) friction_factor_cdf = np.array(friction_factor_cdf_list) return ( mean_friction, std_friction, location_param, scale_param, le_fit, be_fit, he_fit, rmse, r2, friction_factor_range, friction_factor_cdf )