Source code for carbonation

"""
**Summary**

Modified analytical solution of Fick’s law (square root of time)\n
Proportional constant is modified by material properties and exposure environments

+ **Resistance**: 	cover depth

+ **Load**: 		carbonation depth

+ **limit-state**: 	carbonation depth >= cover depth

+ **Field data**: 	carbonation depths (repeated measurements)

"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import sys
from copy import deepcopy
import logging

import rational_rc.math_helper as mh

# logger
# log levels: NOTSET, DEBUG, INFO, WARNING, ERROR, and CRITICAL
LOG_FORMAT = "%(levelname)s %(asctime)s - %(message)s"
logging.basicConfig(
    filename="mylog.log",
    # level=logging.DEBUG,
    format=LOG_FORMAT,
)

logger = logging.getLogger(__name__)
logger.setLevel(
    logging.CRITICAL
)  # set logging level here to work in jupyter notebook where maybe a default setting was there


# model functions
[docs]def carb_depth(t, pars): """ Calculate carbonation depth at a given time based on the parameters. The derived parameters (including the k constant of sqrt of time) are also calculated within this function. Caution: The pars instance is mutable,so a deepcopy of the original instance should be used if the calculation is not intended for "inplace". Parameters ---------- t : time [year] pars : object/instance of wrapper class (empty class) a wrapper of all material and environmental parameters deep-copied from the raw data Returns ------- xc_t : carbonation depth at time t [mm] Note ---- intermediate parameters calculated and attached to pars k_e : environmental function [-] k_c : execution transfer parameter [-] account for curing measures k_t : regression parameter [-] R_ACC_0_inv: inverse effective carbonation resistance of concrete(accelerated) [(mm^2/year)/(kg/m^3)] eps_t : error term [-] C_S : CO2 concentration [$kg/m^3$] W_t : weather function [-] k : constant before the sqrt of time(time[year], carbonation depth[mm]) [mm/year^0.5] typical value of k =3~4 for unit mm,year [https://www.researchgate.net/publication/272174090_Carbonation_Coefficient_of_Concrete_in_Dhaka_City] """ # Calculate intermediate parameters pars.t = t pars.k_e = k_e(pars) pars.k_c = k_c(pars) pars.k_t = k_t() pars.R_ACC_0_inv = R_ACC_0_inv(pars) pars.eps_t = eps_t() pars.C_S = C_S(C_S_emi=pars.C_S_emi) pars.W_t = W_t(t, pars) # Calculate carbonation depth pars.k = ( 2 * pars.k_e * pars.k_c * (pars.k_t * pars.R_ACC_0_inv + pars.eps_t) * pars.C_S ) ** 0.5 * pars.W_t xc_t = pars.k * t ** 0.5 return xc_t
# data import function
[docs]def load_df_R_ACC(): """load the data table of the accelerated carbonation test for R_ACC interpolation. Parameters ---------- Returns ------- pandas.DataFrame Dataframe containing the accelerated carbonation test data. Notes ----- w/c 0.45 cemI is comparable to ACC of 3 mm. """ wc_eqv = np.arange(0.35, 0.60 + (0.05 / 2), 0.05) data = { "wc_eqv": wc_eqv, "CEM_I_42.5_R": [np.nan, 3.1, 5.2, 6.8, 9.8, 13.4], "CEM_I_42.5_R+FA": [np.nan, 0.3, 1.9, 2.4, 6.5, 8.3], "CEM_I_42.5_R+SF": [3.5, 5.5, np.nan, np.nan, 16.5, np.nan], "CEM_III/B_42.5": [np.nan, 8.3, 16.9, 26.6, 44.3, 80.0] } df = pd.DataFrame(data) df.set_index("wc_eqv", inplace=True) return df
[docs]def k_e(pars): """ Calculate k_e[-], environmental factor, effect of relative humidity Parameters ---------- pars.RH_ref : float Reference relative humidity 65 [%] g_e : 2.5 [-] f_e : 5.0 [-] Returns ------- float Calculated environmental factor k_e[-] """ RH_real = pars.RH_real RH_ref = 65.0 g_e = 2.5 f_e = 5.0 k_e = ((1 - (RH_real / 100) ** f_e) / (1 - (RH_ref / 100) ** f_e)) ** g_e return k_e
[docs]def k_c(pars): """ calculate k_c: execution transfer parameter [-], effect of period of curing for the accelerated carbonation test Parameters ---------- pars.t_c : float Period of curing [d] b_c: [built-in] exponent of regression [-] normal distribution, m: -0.567 s: 0.024 Returns ------- float Calculated execution transfer parameter k_c[-] """ t_c = pars.t_c b_c = mh.normal_custom(m=-0.567, s=0.024) k_c = (t_c / 7.0) ** b_c return k_c
[docs]def R_ACC_0_inv(pars): """ Calculate R_ACC_0_inv[(mm^2/year)/(kg/m^3)], the inverse effective carbonation resistance of concrete(accelerated) From ACC test or from existing empirical data interpolation for orientation purpose test condition: duration time = 56 days CO2 = 2.0 vol%, T =25 degC RH_ref =65 Parameters ---------- pars.x_c : float Measured carbonation depth in the accelerated test [m] pars.option.choose : bool If True, choose to use interpolation method pars.option.df_R_ACC : pandas.DataFrame Data table for interpolation, loaded by function load_df_R_ACC, interpolated by function interp_extrap_f Returns ------- out: numpy arrays Calculated inverse effective carbonation resistance [mm^2/year)/(kg/m^3] with sample number = N_SAMPLE (defined globally) Notes ----- Pay special attention to the units in the source code """ x_c = pars.x_c if isinstance(x_c, int) or isinstance(x_c, float): # Through acc-test tau = 420.0 # tau: 'time constant' in [(s/kg/m^3)^0.5], for described test conditions tau = 420 R_ACC_0_inv_mean = (x_c / tau) ** 2 # [(m^2/s)/(kg/m^3)] # R_ACC_0_inv[10^-11*(m^2/s)/(kg/m^3)] ND(s = 0.69*m**0.78) R_ACC_0_inv_stdev = ( 1e-11 * 0.69 * (R_ACC_0_inv_mean * 1e11) ** 0.78 ) # [(m^2/s)/(kg/m^3)] R_ACC_0_inv_temp = mh.normal_custom( R_ACC_0_inv_mean, R_ACC_0_inv_stdev ) # [(m^2/s)/(kg/m^3)] elif pars.option.choose: # 'No test data, interpolate: orientation purpose' logger.warning("No test data, interpolate: orientation purpose") df = pars.option.df_R_ACC fit_df = df[pars.option.cement_type].dropna() # Curve fit x = fit_df.index.astype(float).values y = fit_df.values R_ACC_0_inv_mean = ( mh.interp_extrap_f(x, y, pars.option.wc_eqv, plot=False) * 1e-11 ) # [(m^2/s)/(kg/m^3)] #interp_extrap_f: defined function # R_ACC_0_inv[10^-11*(m^2/s)/(kg/m^3)] ND(s = 0.69*m**0.78) R_ACC_0_inv_stdev = ( 1e-11 * 0.69 * (R_ACC_0_inv_mean * 1e11) ** 0.78 ) # [(m^2/s)/(kg/m^3)] R_ACC_0_inv_temp = mh.normal_custom( R_ACC_0_inv_mean, R_ACC_0_inv_stdev ) # [(m^2/s)/(kg/m^3)] else: logger.error("R_ACC_0_inv calculation failed; application interrupted") sys.exit("Error message") # unit change [(m^2/s)/(kg/m^3)] -> [(mm^2/year)/(kg/m^3)] final model input R_ACC_0_inv_final = 365 * 24 * 3600 * 1e6 * R_ACC_0_inv_temp return R_ACC_0_inv_final
# Test method factors
[docs]def k_t(): """Calculate test method regression parameter k_t[-] Notes ----- for R_ACC_0_inv[(mm^2/years)/(kg/m^3)]""" k_t = mh.normal_custom(1.25, 0.35) return k_t
[docs]def eps_t(): """Calculate error term, eps_t[(mm^2/years)/(kg/m^3)], considering inaccuracies which occur conditionally when using the ACC test method k_t[-] Notes ----- for R_ACC_0_inv[(mm^2/years)/(kg/m^3)]""" eps_t = mh.normal_custom(315.5, 48) return eps_t
# Environmental impact C_S
[docs]def C_S(C_S_emi=0): """Calculate CO2 density[kg/m^3] in the environment; it is about 350-380 ppm in the atm plus other source or sink Parameters ---------- C_S_emi : additional emission, positive or negative(sink), default is 0 Returns ------- float CO2 density in kg/m^3 """ C_S_atm = mh.normal_custom(0.00082, 0.0001) C_S = C_S_atm + C_S_emi return C_S
# weather function
[docs]def W_t(t, pars): """ Calculate weather parameter W, a parameter considering the meso-climatic conditions due to wetting events of concrete surface Parameters ---------- t : float Time [years] pars : object Instance of Param class containing the following attributes: pars.ToW : float Time of wetness [-], calculated as (days with rainfall h_Nd >= 2.5 mm per day)/365 pars.p_SR : float Probability of driving rain [-], 1.0 for vertical surface, 0.0 for horizontal or interior surfaces pars.b_w : float Exponent of regression [-], normally distributed with mean=0.446 and standard deviation=0.163 pars.t_0 : float Time of reference [years] [built-in param] Returns ------- numpy array Weather parameter array W """ ToW = pars.ToW p_SR = pars.p_SR t_0 = 0.0767 # [year] b_w = mh.normal_custom(0.446, 0.163) W = (t_0 / t) ** ((p_SR * ToW) ** b_w / 2.0) return W
# helper function: calibration function
[docs]def calibrate_f(model_raw, t, carb_depth_field, tol=1e-6, max_count=50, print_out=True): """Calibrate the carbonation model with field carbonation test data [mm] and return the new calibrated model. Optimization method: searching for the best accelerated test carbonation depth x_c[m] so the model matches field data on the mean value of the carbonation depth) Parameters ---------- model_raw : object Instance of the CarbonationModel class, mutable (a deepcopy will be used in this function). t : float or int Survey time, age of the concrete [years]. carb_depth_field : numpy array Field carbonation depths at time t [mm]. tol : float, optional Accelerated carbonation depth x_c optimization tolerance, default is 1e-5 [mm]. max_count : int, optional Maximum number of searching iterations, default is 50. print_out : bool, optional Flag to print out the results, default is True. Returns ------- object New calibrated model instance. """ model = model_raw.copy() # Define the initial search space for x_c x_c_min = 0.0 x_c_max = 0.1 # [m] unrealistically large safe ceiling # Optimization count = 0 while x_c_max - x_c_min > tol: # update guess for x_c x_c_guess = 0.5 * (x_c_min + x_c_max) model.pars.x_c = x_c_guess model.run(t) carb_depth_mean = mh.get_mean(model.xc_t) # Compare the mean carbonation depth with the field data if carb_depth_mean < carb_depth_field.mean(): # Narrow the search space x_c_min = max(x_c_guess, x_c_min) else: x_c_max = min(x_c_guess, x_c_max) logger.info("carb_depth_mean:{}".format(carb_depth_mean)) logger.info("x_c:{}".format(x_c_guess)) logger.debug("cap:[{}{}]".format(x_c_min, x_c_max)) count += 1 if count > max_count: logger.warning("Iteration exceeded the maximum count {}".format(count)) break if print_out: print("carb_depth:") print( "model: \nmean:{}\nstd:{}".format( mh.get_mean(model.xc_t), mh.get_std(model.xc_t) ) ) print( "field: \nmean:{}\nstd:{}".format( carb_depth_field.mean(), carb_depth_field.std() ) ) return model
[docs]def carb_year(model, year_lis, plot=True, amplify=80): """Run the model over time and plot the results.""" 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 distributions 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.cover_mean for M in M_lis], "--C0") ax3.plot(t_lis, [mh.get_mean(M.xc_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: cover", alpha=0.8) S_patch = mpatches.Patch(color="C1", label="S: carbonation", alpha=0.8) ax3.set_xlabel("Time[year]") ax3.set_ylabel("Cover/Carbonation Depth [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 CarbonationModel: def __init__(self, pars): self.pars = pars # pars with user-input, then updated with derived parameters logger.debug("\nRaw pars are {}\n".format(vars(pars)))
[docs] def run(self, t): """Run the model for the given time t [year].""" self.xc_t = carb_depth(t, self.pars) self.t = t logger.info("Carbonation depth, xc_t: {} mm".format(self.xc_t))
[docs] def postproc(self, plot=False): """Post-process the model results.""" sol = mh.pf_RS( (self.pars.cover_mean, self.pars.cover_std), self.xc_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.xc_t logger.info("pf{}\n beta_factor{}".format(self.pf, self.beta_factor))
[docs] def calibrate(self, t, carb_depth_field, print_out=False): """Calibrate the model with field data and return a new calibrated model instance.""" model_cal = calibrate_f(self, t, carb_depth_field, print_out=print_out) return model_cal
[docs] def copy(self): """Create a deep copy of the model.""" return deepcopy(self)
[docs] def carb_with_year(self, year_lis, plot=True, amplify=80): """Run the model over time and return lists of pf and beta values.""" pf_lis, beta_lis = carb_year(self, year_lis, plot=plot, amplify=amplify) return np.array(pf_lis), np.array(beta_lis)