Source code for membrane

"""
**Summary**

A statistical model is used to predict the probability of failure for the membrane. 

+ **Resistance**: membrane service life

+ **Load**: age 

+ **limit-state**: age >= service life. 

"""

import matplotlib.pyplot as plt
from copy import deepcopy
from scipy import stats
import numpy as np

import rational_rc.math_helper as mh

# special functions for this module

[docs]def pf_RS_special(R_info, S, R_distrib_type="normal", plot=False): """Calculate the probability of failure given the resistance R and load S using three methods. Parameters ---------- R_info : tuple distribution of Resistance, for this special case, the membrane service life. R_distrib_type='normal' -> tuple(m,s) for normal m: mean s: standard deviation other distribution form will be ignored. S : numpy array distribution of load, for this special case, the membrane age. R_distrib_type : str, optional by default 'normal' plot : bool, optional Whether to plot the distributions. Default is False. Returns ------- tuple Probability of failure (Pf), beta factor, and R distribution Note ---- It is a special case of math_helper.Pf_RS, here the "load" S is a number and it calculates the probability of failure Pf = P(R-S<0), given the R(resistance) and S(load) with three three methods and use method 3 if it is checked "OK" with the other two 1. crude monte carlo 2. numerical integral of g kernel fit 3. R S integral: $F_R(S)$, reliability index(beta factor) is calculated with simple 1st order g.mean()/g.std() R_info only supports the two-parameter normal distribution. """ from scipy import integrate if isinstance(int(S), int): if R_distrib_type == "normal": # R = (mu, std) (m, s) = R_info R_distrib = stats.norm(m, s) R = R_distrib.rvs(size=mh.N_SAMPLE) # Calculate probability of failure pf_RS = R_distrib.cdf(S) else: R = None pf_RS = None R_distrib = None print("R is not configured because R_distrib is not normal") else: R = None S = None pf_RS = None R_distrib = None print("S is not configured") # compare with numerical g g = R - S g = g[~np.isnan(g)] # numerical kernel fit g_kde_fit = mh.fit_distribution(g, fit_type="kernel", plot=False) pf_kde = integrate.quad(g_kde_fit, g.min(), 0)[0] pf_sample = len(g[g < 0]) / len(g) beta_factor = g.mean() / g.std() # first order if plot: print("Pf(g = R-S < 0) from various methods") print(" sample count: {}".format(pf_sample)) print(" g integral: {}".format(pf_kde)) print(" R S integral: {}".format(pf_RS)) # printmd('$\int\limits_{-\infty}^{\infty} F_R(x)f_S(x)dx$') print(" beta_factor: {}".format(beta_factor)) # Plot R S fig, [ax1, ax2] = plt.subplots(ncols=2, figsize=(10, 3)) # R R_plot = np.linspace(R.min(), R.max(), 100) ax1.plot(R_plot, R_distrib.pdf(R_plot), color="C0") ax1.hist( R, bins=min(mh.N_SAMPLE // 100, 100), density=True, alpha=0.5, color="C0", label="R", ) # S updated ax1.vlines( x=S, ymin=0, ymax=R_distrib.pdf(R_info[0]), color="C1", alpha=1, label="S" ) ax1.set_title("S: {:.1f}".format(S)) ax1.legend() plt.tight_layout() # plot g g_plot = np.linspace(g.min(), g.max(), 100) ax2.plot(g_plot, g_kde_fit(g_plot), color="C2", alpha=1) ax2.hist( g, density=True, bins=min(mh.N_SAMPLE // 100, 100), color="C2", alpha=0.5, label="g=R-S", ) ax2.vlines(x=0, ymin=0, ymax=g_kde_fit(0)[0], linestyles="--", alpha=0.5) ax2.vlines( x=g.mean(), ymin=0, ymax=g_kde_fit(g.mean())[0], linestyles="--", alpha=0.5 ) ax2.annotate( text=r"$\{mu}_g$", xy=(0, g.mean()), xytext=(g.mean(), g_kde_fit(0)[0]), va="center", ) ax2.legend() ax2.set_title("Limit-state P(g<0)={}".format(pf_RS)) plt.show() return pf_RS, beta_factor, R_distrib
[docs]def plot_RS_special(model, ax=None, t_offset=0, amplify=1): # updated! """Plot R-S distribution vertically at a time to an axis (special case: S is a number). Parameters: ----------- model : model object instance Model object instance containing the following attributes: - model.R_distrib: scipy.stats._continuous_distns, normal or beta distribution (calculated in pf_RS_special() through model.postproc()) - model.S: Single number for this special case ax : axes, optional Subplot axis. If not provided, the current axis will be used. t_offset : int or float, optional Time offset to move the plot along the t-axis. Default is zero. amplify : int, optional Scale the height of the PDF plot. """ R_distrib = model.R_distrib S = model.S S_dropna = S[~np.isnan(S)] # Plot R S R = R_distrib.rvs(size=mh.N_SAMPLE) if ax == None: ax = plt.gca() # plot R R_plot = np.linspace(R.min(), R.max(), 100) ax.plot(R_distrib.pdf(R_plot) * amplify + t_offset, R_plot, color="C0") ax.fill_betweenx( R_plot, t_offset, R_distrib.pdf(R_plot) * amplify + t_offset, color="C0", alpha=0.5, label="R", ) # plot S S_plot = np.linspace(S_dropna.min(), S_dropna.max(), 100) ax.hlines( y=S, xmin=0 * amplify + t_offset, xmax=R_distrib.pdf(R.mean()) * amplify + t_offset, color="C1", alpha=1, label="S", )
# model function
[docs]def membrane_age(t): """ Return the membrane age as the "resistance". Parameters: ----------- t : int or float Membrane age. Returns: -------- int or float Membrane age. Notes: ------ This function is a placeholder for more complex age input. """ return t
[docs]def membrane_life(pars): """Calculate the mean value of the service life from the manufacturer's service life label (e.g., 30 years with 95% confidence) with the given standard deviation. Parameters: ----------- pars : parameter object instance Raw parameters. - pars.life_product_label_life - pars.life_confidence - pars.life_std Returns: -------- float Service life mean value. """ life_mean = mh.find_mean( val=pars.life_product_label_life, s=pars.life_std, confidence_one_tailed=pars.life_confidence, ) return life_mean
# calibrate Resistance to match the probability
[docs]def calibrate_f( model_raw, t, membrane_failure_ratio_field, tol=1e-6, max_count=100, print_out=True ): """ Calibrate the membrane model to field conditions by finding the corresponding membrane service life std that matches the failure ratio in the field. Parameters: ----------- model_raw : model instance Model to be calibrated. t : int, float Membrane age when membrane failure rate is surveyed [year]. membrane_failure_ratio_field : float Failure rate, e.g., 0.1 for 10%. tol : float, optional Optimization tolerance, default is 1e-6. max_count : int, optional Maximum iteration number for optimization, default is 100. print_out : bool, optional If True, print out the model vs field comparison, default is True. Returns: -------- membrane model object instance Calibrated model. """ model = model_raw.copy() std_min = 0.0 std_max = 100.0 # year, unrealistic large safe ceiling # optimization count = 0 while std_max - std_min > tol: # update guess std_guess = 0.5 * (std_min + std_max) model.pars.life_std = std_guess model.run(t) model.postproc() # compare if model.pf < membrane_failure_ratio_field: # narrow the cap std_min = max(std_guess, std_min) else: std_max = min(std_guess, std_max) # print([std_min, std_max]) count += 1 if count > max_count: break if print_out: print("probability of failure:") print("model: {}".format(model.pf)) print("field: {}".format(membrane_failure_ratio_field)) return model
[docs]def membrane_failure_year(model, year_lis, plot=True, amplify=30): """ Run the model over a list of time steps. Parameters: ----------- model : class instance Membrane_model class instance. year_lis : list, array-like A list of time steps. plot : bool, optional If True, plot the Pf, beta, R S distribution. Default is True. amplify : int, optional The arbitrary comparable size of the distribution curve. Default is 30. Returns: -------- tuple (pf list, beta list) """ t_lis = year_lis M_cal = model M_lis = [] for t in t_lis: M_cal.run(t) M_cal.postproc() M_lis.append(M_cal.copy()) if plot: fig, [ax1, ax2, ax3] = plt.subplots( nrows=3, figsize=(8, 8), sharex=True, gridspec_kw={"height_ratios": [1, 1, 3]}, ) # plot a few distribution indx = np.linspace(0, len(year_lis) - 1, min(6, len(year_lis))).astype("int")[ 1: ] M_sel = [M_lis[i] for i in indx] ax1.plot([this_M.t for this_M in M_lis], [this_M.pf for this_M in M_lis], "k--") ax1.plot( [this_M.t for this_M in M_sel], [this_M.pf for this_M in M_sel], "k|", markersize=15, ) ax1.set_ylabel("Probability of failure $P_f$") ax2.plot( [this_M.t for this_M in M_lis], [this_M.beta_factor for this_M in M_lis], "k--", ) ax2.plot( [this_M.t for this_M in M_sel], [this_M.beta_factor for this_M in M_sel], "k|", markersize=15, ) ax2.set_ylabel(r"Reliability factor $\beta$") # plot mean results ax3.plot(t_lis, [M.pars.life_mean for M in M_lis], "--C0") ax3.plot(t_lis, [M.age for M in M_lis], "--C1") # plot distribution for this_M in M_sel: plot_RS_special(this_M, ax=ax3, t_offset=this_M.t, amplify=amplify) import matplotlib.patches as mpatches R_patch = mpatches.Patch(color="C0", label="R: membrane life", alpha=0.8) S_patch = mpatches.Patch(color="C1", label="S: age", alpha=0.8) ax3.set_xlabel("Time[year]") ax3.set_ylabel("age/membrane life [year]") ax3.legend(handles=[R_patch, S_patch], loc="upper left") plt.tight_layout() return [this_M.pf for this_M in M_lis], [this_M.beta_factor for this_M in M_lis]
[docs]class MembraneModel: def __init__(self, pars): """ Initialize the object with raw parameter object (pars) and mean membrane life. Parameters: ----------- pars : parameter object instance Raw parameters. """ self.pars = pars self.pars.life_mean = membrane_life(self.pars)
[docs] def run(self, t): """ Attach the resistance: membrane age. Parameters: ----------- t : int, float Membrane age. """ self.t = t self.age = membrane_age(t)
[docs] def postproc(self, plot=False): """ Solve pf, beta, attach R distribution with plot option. Parameters: ----------- plot : bool, optional If True, plot the distributions. Default is False. """ sol = pf_RS_special( (self.pars.life_mean, self.pars.life_std), self.age, R_distrib_type="normal", plot=plot, ) self.pf = sol[0] self.beta_factor = sol[1] self.R_distrib = sol[2] self.S = self.age
[docs] def membrane_failure_with_year(self, year_lis, plot=True, amplify=80): """Solve pf, beta at a list of time steps with plot option. Parameters: ----------- year_lis : list, array-like A list of time steps. plot : bool, optional If True, plot the Pf, beta, R S distribution. Default is True. amplify : int, optional The arbitrary comparable size of the distribution curve. Default is 80. Returns: -------- tuple (pf array, beta array) """ pf_lis, beta_lis = membrane_failure_year( self, year_lis, plot=plot, amplify=amplify ) return np.array(pf_lis), np.array(beta_lis)
[docs] def copy(self): """create a deepcopy""" return deepcopy(self)
[docs] def calibrate(self, membrane_age_field, membrane_failure_ratio_field): """Calibrate membrane model to field condition Parameters ---------- membrane_age_field : float, int membrane age when membrane failure rate is surveyed membrane_failure_ratio_field : float failure rate e.g. 0.1 for 10% Returns ------- membrane model object instance calibrated model """ M_cal = calibrate_f(self,membrane_age_field, membrane_failure_ratio_field) return M_cal