diff --git a/docs/_example_code/usage_corrosion.py b/docs/_example_code/usage_corrosion.py new file mode 100644 index 00000000..40244393 --- /dev/null +++ b/docs/_example_code/usage_corrosion.py @@ -0,0 +1,29 @@ +"""Example code to calculate the velocity_of_corrosion and the area_after_corrosion of rebars.""" + +from structuralcodes.codes.mc2020._corrosion import calculate_velocity_of_corrosion,calculate_minimum_area_after_corrosion + +# Calculate the representative velocity of corrosion (defined Pcorr_rep according to MC2020) +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Unsheltered") +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered") +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="chloride_induced",exposure_class="Wet") +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="chloride_induced",exposure_class="Airborn_seawater") +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="chloride_induced",exposure_class="Submerged") +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="chloride_induced",exposure_class="Tidal_zone") +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="chloride_induced",exposure_class="Cyclic_dry_wet") +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="chloride_induced",exposure_class="Cyclic_dry_wet",fractile=0.5) #should give 30 +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="chloride_induced",exposure_class="Cyclic_dry_wet",fractile=0.8413) #should give 30+1*40=70 (1 stdev) +Pcorr_rep=calculate_velocity_of_corrosion(corrosion_type="chloride_induced",exposure_class="Cyclic_dry_wet",fractile=0.9772) #should give 30+2*40=110 (2 stdev) +print("Calculated velocity of corrosion Pcorr_rep = "+str(round(Pcorr_rep,2))+" μm/yr") + +# Calculate the minimum area after corrosion of a rebar of diameter 16mm. +InitialArea=8*8*3.141592 +print("Area before corrosion = "+str(round(InitialArea,2))+" mm2") +# MethodA: by indicating the mass_loss +Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=1.2,mass_loss=0.3) +# MethodB: by indicating the velocity_of_corrosion and the time_of_corrosion +Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=1,velocity_of_corrosion=100,time_of_corrosion=10) + +print("Calculated Minimum_area_after_corrosion = "+str(round(Minimum_area_after_corrosion,2))+" mm2") + + + diff --git a/structuralcodes/codes/mc2020/__init__.py b/structuralcodes/codes/mc2020/__init__.py index b0f0a88a..56fa1606 100644 --- a/structuralcodes/codes/mc2020/__init__.py +++ b/structuralcodes/codes/mc2020/__init__.py @@ -5,3 +5,7 @@ __title__: str = 'fib Model Code 2020' __year__: str = '2024' __materials__: t.Tuple[str] = ('concrete', 'reinforcement') + +from ._corrosion import kc, pcorr_fractile, pcorr_rep + +__all__ = ['pcorr_rep', 'pcorr_fractile', 'kc'] diff --git a/structuralcodes/codes/mc2020/_corrosion.py b/structuralcodes/codes/mc2020/_corrosion.py new file mode 100644 index 00000000..260680f1 --- /dev/null +++ b/structuralcodes/codes/mc2020/_corrosion.py @@ -0,0 +1,174 @@ +import typing as t + +import numpy as np +from scipy.stats import lognorm + +corrosion_type = t.Literal['carbonation', 'chloride'] +exposure_class = t.Literal[ + 'sheltered', + 'unsheltered', + 'wet', + 'cyclic_dry_wet', + 'airborn_seawater', + 'submerged', + 'tidal_zone', +] +exposure_class_corrosion_type = { + 'carbonation': ['sheltered', 'unsheltered'], + 'chloride': [ + 'wet', + 'cyclic_dry_wet', + 'airborn_seawater', + 'submerged', + 'tidal_zone', + ], +} + + +def _validate_corrosion_exposure( + corrosion_type: corrosion_type, exposure_class: exposure_class +) -> bool: + """Returns True if corrosion_type and exposure_class are valid.""" + corr_type = corrosion_type.lower() + exposure = exposure_class.lower() + if corr_type not in ['carbonation', 'chloride']: + return False + return exposure in exposure_class_corrosion_type[corr_type] + + +_table_30_1_6 = { + 'carbonation': { + 'sheltered': {'mean': 2, 'std': 3}, + # the table shows "t", but 7 is correct + 'unsheltered': {'mean': 5, 'std': 7}, + }, + 'chloride': { + 'wet': {'mean': 4, 'std': 6}, + 'cyclic_dry_wet': {'mean': 30, 'std': 40}, + 'airborn_seawater': {'mean': 30, 'std': 40}, + 'submerged': {'mean': 4, 'std': 7}, + 'tidal_zone': {'mean': 50, 'std': 100}, + }, +} + + +def pcorr_rep( + corrosion_type: corrosion_type, exposure_class: exposure_class +) -> dict: + """Returns corrosion rate properties in micrometer/year. + + This implements MC 2020 table 30.1-6a and 30.1-6b. + The return is a dictionary containing mean and standard deviation of pcorr + in micrometer/year. + + Note: + There is a typo in MC2020 Table 30.1-6a. In unshelterd conditions, the + pcorr standard deviation is indicated as "t" while it should be equal + to 7 micrometer/year according to fib Bulletin 111. + + Args: + corrosion_type (str): The type of corrosion. Can be either + "carbonation" or "chloride". + exposure_class (str): The exposure class. Can be one of the following + for "carbonation": "sheltered", "unsheltered". + Can be one of the following for "chloride": + "wet", "cyclic_dry_wet", "airborn_seawater", "submerged", + "tidal_zone". + + Returns: + dict: A dictionary containing the mean and standard deviation of the + corrosion rate in micrometer/year. + + Raises: + ValueError: if the corrosion type is not valid. + ValueError: if the exposure class is not valid for the relative + corrosion type. + """ + if not _validate_corrosion_exposure(corrosion_type, exposure_class): + raise ValueError( + 'Invalid corrosion_type or exposure_class.\n' + 'corrosion_type must be either "carbonation" or "chloride".\n' + 'exposure_class must be one of the following for "carbonation": ' + '"sheltered", "unsheltered".\n' + 'exposure_class must be one of the following for "chloride": ' + '"wet", "cyclic_dry_wet", "airborn_seawater", "submerged", ' + '"tidal_zone".' + ) + return _table_30_1_6[corrosion_type.lower()][exposure_class.lower()] + + +def pcorr_fractile( + corrosion_type: corrosion_type, + exposure_class: exposure_class, + fractile: float = 0.5, +) -> float: + """Returns the fractile of the corrosion rate in micrometer/year. + + This functions uses data from MC 2020 table 30.1-6a and 30.1-6b to + calculate the corrosion rate at a given fractile. The return is a float + representing the corrosion rate in micrometer/year. + + Note: according to fib Bulletin 111, the corrosion rate can be assumed to + follow a lognormal distribution. + + Args: + corrosion_type (str): The type of corrosion. Can be either + "carbonation" or "chloride". + exposure_class (str): The exposure class. Can be one of the following + for "carbonation": "sheltered", "unsheltered". + Can be one of the following for "chloride": + "wet", "cyclic_dry_wet", "airborn_seawater", "submerged", + "tidal_zone". + fractile (Optional[float]): The fractile for which to calculate the + corrosion rate. The default value is 0.5, which corresponds to the + median corrosion rate. The value should be in the range ]0,1[ + + Returns: + float: The corrosion rate at the specified fractile in micrometer/year. + + Raises: + ValueError: if the corrosion type is not valid. + ValueError: if the exposure class is not valid for the relative + corrosion type. + ValueError: if the fractile value is not valid. + """ + if fractile <= 0 or fractile >= 1: + raise ValueError( + 'The value of fractile is not valid, use a value between 0 and 1.' + ) + pcorr_dic = pcorr_rep(corrosion_type, exposure_class) + + # Lognormal distribution + s = np.log(1 + pcorr_dic['std'] ** 2 / pcorr_dic['mean'] ** 2) ** 0.5 + scale = ( + pcorr_dic['mean'] ** 2 + / (pcorr_dic['mean'] ** 2 + pcorr_dic['std'] ** 2) ** 0.5 + ) + lognorm_dist = lognorm(s=s, scale=scale) + return lognorm_dist.ppf(fractile) + + +def kc(fc: float) -> float: + """Compute kc reducing compressive strength factor. + + MC2020 30.1.10.3.2.1 + + It does depend only on fc and not on corrosion level, even if it is valid + for low to moderate corrosion values. + + Moderate and low corrosion levels have been defined to apply approximately + for medium bar diameters with 5% weight loss and 0.25 depth of corrosion. + + Args: + fc (float): Compressive strength of concrete in MPa. + + Returns: + float: The reducing compressive strength factor kc. + + Raises: + ValueError: if fc is not positive. + """ + if fc <= 0: + raise ValueError('fc must be a positive value.') + eta_fc = min((30 / fc) ** (1 / 3.0), 1) + return 0.75 * eta_fc diff --git a/tests/test_mc2020/test_corrosion.py b/tests/test_mc2020/test_corrosion.py new file mode 100644 index 00000000..8901a8a8 --- /dev/null +++ b/tests/test_mc2020/test_corrosion.py @@ -0,0 +1,188 @@ +"""Tests for the functions in corrosion.""" + +from math import isclose + +import numpy as np +import pytest +from scipy.stats import norm + +from structuralcodes.codes.mc2020._corrosion import ( + kc, + pcorr_fractile, + pcorr_rep, +) + + +@pytest.mark.parametrize( + 'corrosion_type, exposure_class, expected', + [ + ('carbonation', 'sheltered', (2, 3)), + ('carbonation', 'unsheltered', (5, 7)), + ('chloride', 'wet', (4, 6)), + ('chloride', 'cyclic_dry_wet', (30, 40)), + ('chloride', 'airborn_seawater', (30, 40)), + ('chloride', 'submerged', (4, 7)), + ('chloride', 'tidal_zone', (50, 100)), + ('Chloride', 'Tidal_zone', (50, 100)), + ], +) +def test_pcorr_dict(corrosion_type, exposure_class, expected): + """Test pcorr_rep with valid input.""" + pcorr_dic = pcorr_rep(corrosion_type, exposure_class) + assert pcorr_dic['mean'] == expected[0] + assert pcorr_dic['std'] == expected[1] + + +@pytest.mark.parametrize( + 'corrosion_type, exposure_class', + [ + ('chloride', 'sheltered'), + ('carbonatio', 'unsheltered'), + ('chloride', 'wett'), + ('chloride', 'sheltered'), + ('chloride', 'sunmerged'), + ], +) +def test_pcorr_dict_invalid(corrosion_type, exposure_class): + """Test pcorr_rep with valid input.""" + with pytest.raises(ValueError): + pcorr_rep(corrosion_type, exposure_class) + + +@pytest.mark.parametrize( + 'fractile', + [ + 0.5, + 0.05, + 0.95, + 0.16, + 0.84, + ], +) +@pytest.mark.parametrize( + 'corrosion_type, exposure_class', + [ + ('carbonation', 'sheltered'), + ('carbonation', 'unsheltered'), + ('chloride', 'wet'), + ('chloride', 'cyclic_dry_wet'), + ('chloride', 'airborn_seawater'), + ('chloride', 'submerged'), + ('chloride', 'tidal_zone'), + ], +) +def test_pcorr_fractile(corrosion_type, exposure_class, fractile): + """Test pcorr_fractile with valid input.""" + # Evaluate expected value + pcorr_dic = pcorr_rep( + corrosion_type=corrosion_type, exposure_class=exposure_class + ) + scale = ( + pcorr_dic['mean'] ** 2 + / (pcorr_dic['mean'] ** 2 + pcorr_dic['std'] ** 2) ** 0.5 + ) + median = scale + mu = np.log(median) # median = scale = exp(mu) + s = ( + np.log(1 + pcorr_dic['std'] ** 2 / pcorr_dic['mean'] ** 2) ** 0.5 + ) # sigma = s + z = norm.ppf(fractile) + expected = np.exp(mu + z * s) + # Act + assert isclose( + pcorr_fractile( + corrosion_type=corrosion_type, + exposure_class=exposure_class, + fractile=fractile, + ), + expected, + ) + + +@pytest.mark.parametrize( + 'corrosion_type, exposure_class', + [ + ('chloride', 'sheltered'), + ('carbonatio', 'unsheltered'), + ('chloride', 'wett'), + ('chloride', 'sheltered'), + ('chloride', 'sunmerged'), + ], +) +def test_pcorr_fractile_invalid(corrosion_type, exposure_class): + """Test pcorr_fractile with invalid corrosion type and/or exposure.""" + with pytest.raises(ValueError): + ( + pcorr_fractile( + corrosion_type=corrosion_type, + exposure_class=exposure_class, + fractile=0.5, + ), + ) + + +@pytest.mark.parametrize( + 'fractile', + [ + 0.0, + -0.1, + 50, + 1.0, + 1.2, + 95.0, + ], +) +@pytest.mark.parametrize( + 'corrosion_type, exposure_class', + [ + ('carbonation', 'sheltered'), + ('carbonation', 'unsheltered'), + ('chloride', 'wet'), + ('chloride', 'cyclic_dry_wet'), + ('chloride', 'airborn_seawater'), + ('chloride', 'submerged'), + ('chloride', 'tidal_zone'), + ], +) +def test_pcorr_fractile_invalid_fractile( + corrosion_type, exposure_class, fractile +): + """Test pcorr_fractile with invalid fractile.""" + with pytest.raises(ValueError): + ( + pcorr_fractile( + corrosion_type=corrosion_type, + exposure_class=exposure_class, + fractile=fractile, + ), + ) + + +@pytest.mark.parametrize( + 'fc, expected', + [ + (20, 0.75), + (25, 0.75), + (30, 0.75), + (35, 0.712435688694747), + (40, 0.681420222312052), + (45, 0.655185348552224), + (50, 0.632574498976312), + ], +) +def test_kc(fc, expected): + """Test kc with valid input.""" + assert isclose(kc(fc), expected) + + +@pytest.mark.parametrize( + 'fc', + [ + (0), + (-20), + ], +) +def test_kc_invalid(fc): + """Test kc with invalid input.""" + with pytest.raises(ValueError): + kc(fc)