Source code for corrosion

"""
**Summary**

2D electrochemical model and its regressed solution \n
icorr = f(moisture, temperature, oxygen availability)

**Field data**

+ Volumetric water content (TDR moisture sensor)

+ corrosion rate (LPR, corrosion sensor) to validate the model

"""


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

import rational_rc.math_helper as mh

[docs]def icorr_to_mmpy(icorr): """Converts icorr [A/m^2] to corrosion rate [mm/year] using Faraday's laws Parameters ---------- icorr : float or numpy array Corrosion current density [A/m^2] Returns ------- float or numpy array Corrosion rate, section loss [mm/year] """ M_Fe = 55.8e-3 # kg/mol rho_Fe = 7.874e3 # kg/m^3 n = 2.0 F = 96485.33212 # C/mol return icorr * M_Fe / (n * F * rho_Fe) * 3600 * 24 * 365 * 1000.0
[docs]def mmpy_to_icorr(rate): """Converts corrosion rate [mm/year] to icorr [A/m^2] using Faraday's laws Parameters ---------- rate : float or numpy array Corrosion rate, section loss [mm/year] Returns ------- float or numpy array Corrosion current density [A/m^2] """ M_Fe = 55.8e-3 # kg/mol rho_Fe = 7.874e3 # kg/m^3 n = 2.0 F = 96485.33212 # C/mol return rate * n * F * rho_Fe / (M_Fe * 3600 * 24 * 365 * 1000.0)
[docs]def icorr_base(rho, T, iL, d): # SI units # regressed model for the ref """Calculate averaged corrosion current density over the rebar-concrete interface from resistivity, temperature, limiting current, and cover thickness. Parameters ---------- rho : float or numpy array Resistivity [ohm.m] T : float or numpy array Temperature [K] iL : float or numpy array Limiting current, oxygen diffusion [A/m^2] d : float or numpy array Concrete cover depth [m] Returns ------- numpy array icorr : Corrosion current density, treated as uniform corrosion [A/m^2] Notes ----- Reference: Pour-Ghaz, M., Isgor, O. B., & Ghods, P. (2009). The effect of temperature on the corrosion of steel in concrete. Part 1: Simulated polarization resistance tests and model development. Corrosion Science, 51(2), 415–425. https://doi.org/10.1016/j.corsci.2008.10.034 Parameters from the reference. SI units """ # constants tau = 1.181102362e-3 eta = 1.414736274e-5 c = -0.00121155206 kappa = 0.0847693074 lam = 0.130025167 gamma = 0.800505851 mu = 1.23199829e-11 theta = -0.000102886027 V = 0.475258097 X = 5.03368481e-7 nu = 90487 W = 0.0721605536 icorr = ( 1 / (tau * rho ** gamma) * ( eta * T * d ** kappa * iL ** lam + mu * T * nu ** (iL ** W) + theta * (T * iL) ** V + X * rho ** gamma + c ) ) return icorr
[docs]def theta2rho_fun(theta_water, a, b): """Convert volumetric water content to resistivity using an exponential function. Parameters ---------- theta_water : float or numpy array Volumetric water content a : float Regression coefficient b : float Regression coefficient Returns ------- float or numpy array Resistivity """ rho = a * theta_water ** b return rho
[docs]def icorr_f(pars): """Calculate the corrosion current density using icorr_base() with volumetric water content. Parameters ---------- pars : Param An instance of the Param class containing the following attributes: pars.theta_water : float Volumetric water content pars.T : float or numpy array Temperature [K] pars.iL : float or numpy array Limiting current, oxygen diffusion [A/m^2] pars.d : float or numpy array Concrete cover depth [m] pars.theta2rho_coeff_a : float Regression coefficient of theta2rho_fun pars.theta2rho_coeff_b : float Regression coefficient of theta2rho_fun Returns ------- float, numpy array icorr : corrosion current density [A/m^2] """ pars.iL = iL_f(pars) rho = theta2rho_fun(pars.theta_water, pars.theta2rho_coeff_a, pars.theta2rho_coeff_b) icorr = icorr_base(rho, pars.T, pars.iL, pars.d) return icorr
[docs]def iL_f(pars): """calculate O2 limiting current density Parameters ---------- pars : instance of Param object parameter object that contains the material properties listed in the note. Returns ------- float, numpy array O2 limiting current density [A/m^2] Note ---- intermediate parameters + z : number of charge, 4 for oxygen + delta : thickness of diffusion layer [m] + pars.De_O2 : diffusivity [m^2/s] + pars.Cs_g : bulk concentration [mol/m^3] + pars.epsilon_g : gas phase fraction """ F = 96485.3329 # s*A/mol z = 4 # effective diffusivity averaged for the whole concrete medium pars.De_O2 = De_O2_f(pars) delta = pars.d pars.Cs_g = Cs_g_f() pars.epsilon_g = pars.epsilon - pars.theta_water # assume quick dissolution between gas and liquid phase # liquid phase diffusion is neglected, very slow iL = 0 when epsilon_g = 0 # pars.epsilon_g * pars.Cs is the concentration of concrete iL = z * F * (pars.epsilon_g * pars.De_O2 * pars.Cs_g / delta) return iL
[docs]def Cs_g_f(): """Calculate the atmospheric O2 concentration in the gas phase on the boundary [mol/m^3], converted from 20.95% by volume""" O2_fraction = 20.95 / 100 air_molar_vol = 22.4 # [L/mol] Cs_g = 1 / air_molar_vol * O2_fraction * 1000 # mol/m^3 return Cs_g
[docs]def De_O2_f(pars): """calculate the O2 effective diffusivity of concrete Parameters ---------- pars : instance of Param object Returns ------- float, numpy array O2 effective diffusivity of concrete Notes ----- important intermediate Parameters + epsilon_p : porosity of hardened cement paste, + RH : relative humidity [-%] Gas diffusion along the aggregate-paste interface makes up for the lack of diffusion through the aggregate particles themselves. Therefore, the value of effective diffusivity is considered herein as a function of the porosity of hardened cement paste. [TODO: add temperature dependence] """ epsilon_p = epsilon_p_f(pars) pars.epsilon_p = epsilon_p # calculate internal RH with retention curve/ adsoption curve waterByMassHCP = theta_water_to_waterByMassHCP( pars ) # water content g/g hardened cement paste pars.waterByMassHCP = waterByMassHCP RH = waterByMassHCP_to_RH(pars) pars.RH = RH # [TODO] D_O2_T0 * np.e**(dU_D/R*(1/T0-1/T)) Pour-Ghaz, M., Burkan Isgor, O., & Ghods, P. (2009). The effect of temperature on the corrosion of steel in concrete. Part 2: Model verification and parametric study. Corrosion Science, 51(2), 426–433. https://doi.org/10.1016/j.corsci.2008.10.036 De_O2 = 1.92e-6 * epsilon_p ** 1.8 * (1 - RH / 100) ** 2.2 # Papadakis 1991 return De_O2
[docs]def epsilon_p_f(pars): """Calculate the porosity of the hardened cement paste from the concrete porosity Parameters --------- pars : instance of Param object Returns ------- float, numpy array Note ---- [TODO: when the concrete porosity is not known, the calculated porosity is time dependent at young age, a function of concrete mix and t] """ if isinstance(int(pars.epsilon), int): # concrete porosity, epsilon is given a_c = pars.a_c # aggregate cement ratio w_c = pars.w_c # water cement ratio rho_c = pars.rho_c # density of cement rho_a = pars.rho_a # density of aggregate rho_w = 1000.0 epsilon_p = pars.epsilon * ( 1 + (a_c * rho_c / rho_a) / (1 + w_c * rho_c / rho_w) ) elif 1 == 0: # use calculation from mix proportioning # [TODO: epsilon is time dependent, a function of concrete mix and t] epsilon_p = None else: epsilon_p = None print("cement paste porosity, epsilon_p is not configured!") return epsilon_p
[docs]def calibrate_f(raw_model, field_data): """[TODO] A placeholder function for future development. field_data: temperature, theta_water, icorr_list""" model = raw_model.copy() return model
# RH and water theta is related. Use theoretical model adsorption isotherm or empirical van-Genutchten model
[docs]def RH_to_waterByMassHCP(pars): """Return water content(g/g hardened cement paste) from RH in pores/environment based on water-cement ratio w_c, cement_type, temperature by using modified BET model Note ---- Reference: Xi, Y., Bazant, Z. P., & Jennings, H. M. (1993). Moisture Diffusion in Cementitious Materials Adsorption Isotherms. """ V_m = V_m_f(pars.t, pars.w_c, pars.cement_type) pars.V_m = V_m C_mean, C = C_f(pars.T) # mean, distribution sample pars.C = C k = k_f(C_mean, pars.w_c, pars.t, pars.cement_type) pars.k = k RH_divided_by_100 = pars.RH / 100 waterByMassHCP = ( V_m * C * k * RH_divided_by_100 / ((1 - k * RH_divided_by_100) * (1 + (C - 1) * k * RH_divided_by_100)) ) return waterByMassHCP
[docs]def waterByMassHCP_to_RH(pars): """Return RH in pores/environment from water content(g/g hardened cement paste) based on water-cement ratio w_c, cement type, temperature, a reverse function of RH_to_waterByMassHCP()""" V_m = V_m_f(pars.t, pars.w_c, pars.cement_type) pars.V_m = V_m C_mean, C = C_f(pars.T) # mean, distribution sample pars.C = C k = k_f(C_mean, pars.w_c, pars.t, pars.cement_type) pars.k = k waterByMassHCP = pars.waterByMassHCP r1, r2 = mh.f_solve_poly2( -(C - 1) * k ** 2, (C - 2 - C * V_m / waterByMassHCP) * k, 1 ) if r1.mean() > 0: RH_divided_by_100 = r1 else: RH_divided_by_100 = r2 RH = RH_divided_by_100 * 100 return RH
[docs]def V_m_f(t, w_c, cement_type): """Calculate V_m, a BET model parameter. Parameters ---------- t : float Curing time/concrete age [day] w_c : float Water-cement ratio cement_type : str ASTM C150 cement type, see note Returns ------- numpy array V_m : BET model parameter Note ---- ASTM C150 cement type:\n Cement Type Description\n Type I : Normal\n Type II : Moderate Sulfate Resistance\n Type II (MH) : Moderate Heat of Hydration (and Moderate Sulfate Resistance)\n Type III : High Early Strength\n Type IV : Low Heat Hydration\n Type V : High Sulfate Resistance """ if t < 5: t = 5 if w_c < 0.3: w_c = 0.3 if w_c > 0.7: w_c = 0.7 V_ct_cement_type_dict = { "Type I": 0.9, "Type II": 1, "Type III": 0.85, "Type IV": 0.6, } # default value is 0.9, returns default when type is not found V_ct = V_ct_cement_type_dict.get(cement_type, 0.9) V_m_mean = (0.068 - 0.22 / t) * (0.85 + 0.45 * w_c) * V_ct V_m_std = 0.016 * V_m_mean # COV V_m = mh.normal_custom(V_m_mean, V_m_std) return V_m
[docs]def C_f(T): """Return BET model parameter C sampled from a normal distribution. Parameters ---------- T : float temperature [K] Note ---- C varies from 10 to 50. This function is not applicable for elevated temperatures """ C_0 = 855 C_mean = np.e ** (C_0 / T) C_std = C_mean * 0.12 # COV 0.12 C = mh.normal_custom(C_mean, C_std) return C_mean, C
[docs]def k_f(C_mean, w_c, t, cement_type): """Return BET model parameter k Parameters ---------- C_mean : float Mean value of BET model parameter C w_c : float Water-cement ratio t : float Curing time/concrete age [day] cement_type : str ASTM C150 cement type Returns ------- numpy array k : BET model parameter """ if t < 5: t = 5 if w_c < 0.3: w_c = 0.3 if w_c > 0.7: w_c = 0.7 N_ct_cement_type_dict = { "Type I": 1.1, "Type II": 1, "Type III": 1.15, "Type IV": 1.5, } # default value is 1.1, returns default when type is not found N_ct = N_ct_cement_type_dict.get(cement_type, 1.1) n = (2.5 + 15 / t) * (0.33 + 2.2 * w_c) * N_ct k_mean = ((1 - 1 / n) * C_mean - 1) / (C_mean - 1) k_std = k_mean * 0.007 k = mh.normal_custom(k_mean, k_std) return k
[docs]def waterByMassHCP_to_theta_water(pars): """Convert water content from g/g hardened cement paste (HCP) to volumetric in HCP to volumetric in concrete Parameters ---------- pars : Param An instance of the Param class containing the following attributes: pars.waterByMassHCP : float Water content by mass in hardened cement paste [g/g] pars.rho_c : float Density of cement [kg/m^3] pars.rho_a : float Density of aggregate [kg/m^3] pars.a_c : float aggregate-cement ratio pars.w_c : float Water-cement ratio Returns ------- float theta_water : volumetric water content in concrete """ rho_w = 1000 waterByMassHCP = pars.waterByMassHCP rho_c = pars.rho_c rho_a = pars.rho_a a_c = pars.a_c w_c = pars.w_c theta_water_hcp = 1 / (1 + (1 / waterByMassHCP - 1) * rho_w / rho_c) theta_water = theta_water_hcp / ( 1 + (a_c * rho_c / rho_a) / (1 + w_c * rho_c / rho_w) ) return theta_water
[docs]def theta_water_to_waterByMassHCP(pars): """ convert water content from volumetric by concrete to volumetric in HCP to g/g in HCP, a reverse function of waterByMassHCP_to_theta_water() Parameters ---------- pars : Param An instance of the Param class containing the following attributes: pars.theta_water : float volumetric water content in concrete pars.rho_c : float Density of cement [kg/m^3] pars.rho_a : float Density of aggregate [kg/m^3] pars.a_c : float Aggregate-cement ratio pars.w_c : float Water-cement ratio Returns ------- float waterByMassHCP : Water content by mass in hardened cement paste [g/g] """ rho_w = 1000 theta_water = pars.theta_water rho_c = pars.rho_c rho_a = pars.rho_a a_c = pars.a_c w_c = pars.w_c theta_water_hcp = theta_water * ( 1 + (a_c * rho_c / rho_a) / (1 + w_c * rho_c / rho_w) ) waterByMassHCP = 1 / ((1 / theta_water_hcp - 1) * rho_c / rho_w + 1) return waterByMassHCP
[docs]class CorrosionModel: def __init__(self, pars): """Initialize the model with Param object and built-in coefficient""" pars.theta2rho_coeff_a = 18.71810174 # [TODO: future updates: uncertainty for a and b] pars.theta2rho_coeff_b = -1.37938931 self.pars = pars self.icorr = None self.x_loss_rate = None
[docs] def run(self): """Solve for icorr and the corresponding section loss rate""" self.icorr = icorr_f(self.pars) self.x_loss_rate = icorr_to_mmpy(self.icorr) # [mm/year]
[docs] def calibrate(self, field_data): # place holder function for future update# # update parameters a and b pass
[docs] def copy(self): return deepcopy(self)
###### section loss ########## # output section loss distribution at time t
[docs]def x_loss_t_fun(t_end, n_step, x_loss_rate, p_active_t_curve): """Return samples of x_loss at a given time t_end. The samples represent the distribution of all possible x_loss with different corrosion history Parameters ---------- t_end : float Year in which the x_loss is reported n_step : int Number of time steps x_loss_rate : float Averaged corrosion rate (x-loss rate) p_active_t_curve : tuple (t_lis_curve, pf_lis_curve) - Probability curve data Returns ------- numpy array Section loss at t_end year, a large sample from the distribution """ # probability curve data t_lis_curve, pf_lis_curve = p_active_t_curve # time step of interest (usually finer step) t = np.linspace(0,t_end, n_step) # at this_year, (t_end), calculate the accumulated section loss for each time step age_lis = t_end - t age_lis = age_lis[age_lis>=0] x_loss_lis = age_lis * x_loss_rate # probability of newly active corrosion onset for each time step pf_lis = np.interp(t,t_lis_curve, pf_lis_curve) p_corr_onset_lis = np.diff(pf_lis,prepend=0) # sample the accumulated section loss with the the corresponding probability from random import choices x_loss_at_t = choices(x_loss_lis , p_corr_onset_lis, k = mh.N_SAMPLE) return np.array(x_loss_at_t)
[docs]def x_loss_year(model, year_lis, plot=True, amplify=80): """Run x_loss_t_fun() function over time. Parameters ---------- model : SectionLossModel object An instance of the SectionLossModel class year_lis : list List of years plot : bool, optional Flag indicating whether to plot the results, by default True amplify : int, optional Amplification factor for plotting, by default 80 Returns ------- list List of probabilities of failure (Pf) at each year list List of reliability factors (beta) at each year """ 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.x_loss_limit_mean for M in M_lis], '--C0') ax3.plot(t_lis, [mh.get_mean(M.x_loss_t) for M in M_lis], '--C1') # plot distribution for this_M in M_sel: mh.plot_RS(this_M, ax=ax3, t_offset=this_M.t, amplify=amplify) import matplotlib.patches as mpatches R_patch = mpatches.Patch(color='C0', label='R: limit',alpha=0.8) S_patch = mpatches.Patch(color='C1', label='S: section loss',alpha=0.8) ax3.set_xlabel('Time[year]') ax3.set_ylabel('section loss/limit [mm]') 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 SectionLossModel: def __init__(self, pars): self.pars = pars # pars with user-input
[docs] def run(self, t_end): """run model to solve the accumulated section loss at t_end by using x_loss_t_fun() Parameters ---------- t_end : int, float year """ self.t = t_end self.x_loss_t = x_loss_t_fun(t_end, mh.N_SAMPLE, self.pars.x_loss_rate, self.pars.p_active_t_curve)
[docs] def postproc(self, plot=False): """calculate the Pf and beta from accumulated section loss and section loss limit Parameters ---------- plot : bool, optional if true plot the R S curve, by default False """ sol = mh.pf_RS((self.pars.x_loss_limit_mean, self.pars.x_loss_limit_std), self.x_loss_t, plot=plot) self.pf = sol[0] self.beta_factor = sol[1] self.R_distrib = sol[2] self.S_kde_fit = sol[3] self.S = self.x_loss_t
[docs] def copy(self): """Return a deep copy of the object """ return deepcopy(self)
[docs] def section_loss_with_year(self, year_lis, plot=True, amplify=1): """use x_loss_year() to report the accumulated section loss at each time step and the corresponding Pf and beta. Parameters ---------- year_lis : list a list of time step [year] plot : bool, optional if true, plot the RS, pf, beta with time, by default True amplify : int, optional scale factor to adjust the height of the distribution curve, by default 1 Returns ------- tuple (pf_list, beta_list) """ pf_lis, beta_lis = x_loss_year(self, year_lis, plot=plot, amplify=amplify) return np.array(pf_lis), np.array(beta_lis)