From 0359dd5acd3a1e42ee70cdf1e90293bc00244ea8 Mon Sep 17 00:00:00 2001 From: AndreaEste97 Date: Thu, 4 Dec 2025 13:27:33 +0100 Subject: [PATCH 1/7] First implementation of corrosion. --- docs/_example_code/usage_corrosion.py | 32 ++ structuralcodes/development/__init__.py | 0 structuralcodes/development/corrosion.py | 406 +++++++++++++++++++++++ 3 files changed, 438 insertions(+) create mode 100644 docs/_example_code/usage_corrosion.py create mode 100644 structuralcodes/development/__init__.py create mode 100644 structuralcodes/development/corrosion.py diff --git a/docs/_example_code/usage_corrosion.py b/docs/_example_code/usage_corrosion.py new file mode 100644 index 00000000..7abe00bf --- /dev/null +++ b/docs/_example_code/usage_corrosion.py @@ -0,0 +1,32 @@ +"""Example code to calculate the velocity_of_corrosion and the area_after_corrosion of rebars.""" + +from structuralcodes.development.corrosion import calculate_velocity_of_corrosion,calculate_minimum_area_after_corrosion +from structuralcodes import set_design_code + +# Set design code. Note: actually (04/12/2025) only formule valid for MC2020 are implemented. +set_design_code('ec2_2004') + +# 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.2,velocity_of_corrosion=110,time_of_corrosion=10) +print("Calculated Minimum_area_after_corrosion = "+str(round(Minimum_area_after_corrosion,2))+" mm2") + + + diff --git a/structuralcodes/development/__init__.py b/structuralcodes/development/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/structuralcodes/development/corrosion.py b/structuralcodes/development/corrosion.py new file mode 100644 index 00000000..bcf57f26 --- /dev/null +++ b/structuralcodes/development/corrosion.py @@ -0,0 +1,406 @@ +import typing as t +from structuralcodes.codes import _use_design_code + +def calculate_velocity_of_corrosion( + corrosion_type: t.Optional[str] = "chloride_induced", + exposure_class: t.Optional[str] = None, + fractile: t.Optional[float] = 0.5, + design_code: t.Optional[str] = None, +) -> float: + """A function to calculate the representative velocity of corrosion given certain environmental conditions. + Actually (02/12/2025) only MC2020 is implemented, see table 30.1-6a and 30.1-6b. + + Keyword Arguments: + corrosion_type (str): Corrosion type, which can be "carbonation_induced" or "chloride_induced" (default: "chloride_induced"). + exposure_class (str): Exposure class, which can be "Sheltered" or "Unsheltered" for corrosion_type=="carbonation_induced" + and "Wet", "Cyclic_dry_wet", "Airborn_seawater", "Submerged", "Tidal_zone" for corrosion_type=="chloride_induced" + (default: "Unsheltered" for corrosion_type=="carbonation_induced" and "Cyclic_dry_wet" for corrosion_type=="chloride_induced"). + fractile (float): A statistic parameter used to obtain the velocity_of_corrosion at a given number of standard deviation from the + average value. For example, the velocity of corrosion calculacted with fractile=0.5 can be interpreted as "50% of rebars + have a velocity of corrosion lower than Pcorr_rep (the returning value of this function). For example, the velocity of corrosion + calculacted with fractile=0.99 can be interpreted as "99% of rebars have a velocity of corrosion lower than Pcorr_rep, giving a + higher degree of safety of the returned value Pcorr_rep. (default: "0.5") + design_code (str): Optional string (default: None) indicating the + desired standard. If None (default) the globally used design + standard will be adopted. Otherwise the design standard specified + will be used for the instance of the material. + + Return: + Pcorr_rep (float): representative velocity of corrosion of the rebars, defined as the ratio (asbolute value) between the variation of the rebar's diameter + and the time of exposure. Unit of measurement: μm/yr. + + Raises: + ValueError: if the design code is not valid. + 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. + ValueError: if the fractile value used causes Pcorr_rep to obtain values lower than 0. + """ + # Get the code from the global variable + _code = _use_design_code(design_code) + + # Check if the code is a proper concrete code + code = None + if _code is not None: + code = _code + if code is None: + raise ValueError( + 'The design code is not set, either use ' + 'structuralcodes.code.set_designcode, or provide a valid ' + 'string in the function.' + ) + + # Check if corrosion_type is valid. + ValidCorrosionTypes=["carbonation_induced","chloride_induced"] + if corrosion_type not in ValidCorrosionTypes: + raise ValueError( + 'The variable corrosion_type is not valid, either use ' \ + '"carbonation_induced" or "chloride_induced"' + ) + + # Set exposure_class to default values if None. Then, check if exposure_class is valid. + ValidExposureClassesForCarbonationInducedCorrosion=["Sheltered","Unsheltered"] + ValidExposureClassesForChlorideInducedCorrosion=["Wet","Cyclic_dry_wet","Airborn_seawater","Submerged","Tidal_zone"] + if exposure_class is None: + if corrosion_type=="carbonation_induced": + exposure_class="Unsheltered" + elif corrosion_type=="chloride_induced": + exposure_class="Cyclic_dry_wet" + if corrosion_type=="carbonation_induced" and exposure_class not in ValidExposureClassesForCarbonationInducedCorrosion: + raise ValueError( + 'The variable exposure_class is not valid for the current corrosion_type, either use ' \ + '"Sheltered" or "Unsheltered"' + ) + if corrosion_type=="chloride_induced" and exposure_class not in ValidExposureClassesForChlorideInducedCorrosion: + raise ValueError( + 'The variable exposure_class is not valid for the current corrosion_type, either use ' \ + '"Wet", "Cyclic_dry_wet", "Airborn_seawater", "Submerged" or "Tidal_zone"' + ) + + # Check if fractile value is valid. + if fractile <=0 or fractile >=1: + raise ValueError( + 'The value of fractile is not valid, use a value between 0 and 1 (both excluded)' + ) + + # Calculate MeanValue and StandardDeviation + if corrosion_type == "carbonation_induced": + table_mean = { + "Sheltered": 2, + "Unsheltered": 5, + } + table_sd = { + "Sheltered": 3, + "Unsheltered": 1, # the table shows "t", but assuming 1 μm/yr (fix if needed) + } + elif corrosion_type == "chloride_induced": + table_mean = { + "Wet": 4, + "Cyclic_dry_wet": 30, + "Airborn_seawater": 30, + "Submerged": 4, + "Tidal_zone": 50, + } + table_sd = { + "Wet": 6, + "Cyclic_dry_wet": 40, + "Airborn_seawater": 40, + "Submerged": 7, + "Tidal_zone": 100, + } + mean_value = table_mean[exposure_class] + sd_value = table_sd[exposure_class] + + # Calculate Pcorr_rep for the correct fractile value (use Acklam's algorithm for the approximation of the inverse + # standard normal CDF in order to avoid to add other dependencies). + def inverse_normal_cdf(p: float) -> float: + """Approximation of the inverse standard normal CDF (Acklam's algorithm).""" + if p <= 0.0 or p >= 1.0: + raise ValueError("p must be in (0,1)") + + import math + from math import sqrt + + # Coefficients for the approximation + a = [ -3.969683028665376e+01, + 2.209460984245205e+02, + -2.759285104469687e+02, + 1.383577518672690e+02, + -3.066479806614716e+01, + 2.506628277459239e+00 ] + + b = [ -5.447609879822406e+01, + 1.615858368580409e+02, + -1.556989798598866e+02, + 6.680131188771972e+01, + -1.328068155288572e+01 ] + + c = [ -7.784894002430293e-03, + -3.223964580411365e-01, + -2.400758277161838e+00, + -2.549732539343734e+00, + 4.374664141464968e+00, + 2.938163982698783e+00 ] + + d = [ 7.784695709041462e-03, + 3.224671290700398e-01, + 2.445134137142996e+00, + 3.754408661907416e+00 ] + + # Define break-points + plow = 0.02425 + phigh = 1 - plow + + if p < plow: + q = sqrt(-2 * math.log(p)) + return (((((c[0]*q + c[1])*q + c[2])*q + c[3])*q + c[4])*q + c[5]) / \ + ((((d[0]*q + d[1])*q + d[2])*q + d[3])*q + 1) + elif p > phigh: + q = sqrt(-2 * math.log(1 - p)) + return -(((((c[0]*q + c[1])*q + c[2])*q + c[3])*q + c[4])*q + c[5]) / \ + ((((d[0]*q + d[1])*q + d[2])*q + d[3])*q + 1) + else: + q = p - 0.5 + r = q * q + return (((((a[0]*r + a[1])*r + a[2])*r + a[3])*r + a[4])*r + a[5]) * q / \ + (((((b[0]*r + b[1])*r + b[2])*r + b[3])*r + b[4])*r + 1) + + z = inverse_normal_cdf(fractile) + Pcorr_rep = mean_value + z * sd_value + + # Validate the result, which could be <=0 if fractile is too low. + if Pcorr_rep<=0: + raise ValueError( + 'The calculated value of Pcorr_rep is less or equal to 0 because the value of fractile is too low. '\ + 'Try using a higher value of fractile. Note that using a low value of fractile could lead to an '\ + 'underestimention of the effect of corrosion.' + ) + + return Pcorr_rep + +def calculate_minimum_area_after_corrosion( + uncorroded_area: float, + pitting_factor: t.Optional[float] = 1, + mass_loss: t.Optional[float] = None, + velocity_of_corrosion: t.Optional[float] = None, + time_of_corrosion: t.Optional[float] = None, + design_code: t.Optional[str] = None, +) -> float: + + """A function to calculate the minimum residual steel area after corrosion. + The function allows two alternative approaches: (i) using the total mass loss, + or (ii) using the velocity of corrosion together with the time of corrosion. + A pitting factor is used to relate the maximum pit depth to the average pit depth. + Actually (02/12/2025) only MC2020 is implemented, see chapter 30.1.11.3.5. + + Keyword Arguments: + uncorroded_area (float): Original (uncorroded) cross-sectional area of the rebar [mm²]. + pitting_factor (float): Ratio between maximum pit depth and average pit depth + (default: 1). Must be ≥ 1. + mass_loss (float): Fractional mass loss (0–1). If provided, velocity_of_corrosion + and time_of_corrosion must be None. (default: None) + velocity_of_corrosion (float): Corrosion rate [μm/yr]. Must be ≥ 0. Used together + with time_of_corrosion. If provided, mass_loss must be None. (default: None) + time_of_corrosion (float): Time of corrosion [years]. Must be ≥ 0. Used with + velocity_of_corrosion. (default: None) + design_code (str): Optional string indicating the desired standard. If None + (default) the globally used design standard will be adopted. Otherwise, + the design standard specified will be used. + + Return: + corroded_minimum_area (float): Minimum residual area of the corroded rebar [mm²], + computed assuming axisymmetric corrosion with maximum pit depth defined by + `pitting_factor * average_pit_depth`. + + Raises: + ValueError: if the design code is not set or invalid. + ValueError: if both mass_loss and (velocity_of_corrosion + time_of_corrosion) + are provided simultaneously. + ValueError: if only one between velocity_of_corrosion and time_of_corrosion is provided. + ValueError: if pitting_factor < 1. + ValueError: if uncorroded_area < 0. + ValueError: if mass_loss is not in the range [0, 1]. + ValueError: if velocity_of_corrosion < 0 or time_of_corrosion < 0. + ValueError: if the computed minimum residual radius becomes negative. + """ + + import math + from math import sqrt + uncorroded_diameter=sqrt(uncorroded_area/math.pi)*2 + + # Get the code from the global variable + _code = _use_design_code(design_code) + + # Check if the code is a proper concrete code + code = None + if _code is not None: + code = _code + if code is None: + raise ValueError( + 'The design code is not set, either use ' + 'structuralcodes.code.set_designcode, or provide a valid ' + 'string in the function.' + ) + + # Input data validation + if (mass_loss is not None) and (velocity_of_corrosion is not None or time_of_corrosion is not None): + raise ValueError( + 'Too many input arguments have been given to the function. ' + 'Either use mass_loss or velocity_of_corrosion + time_of_corrosion.' + ) + + if (velocity_of_corrosion is not None and time_of_corrosion is None) or \ + (velocity_of_corrosion is None and time_of_corrosion is not None): + raise ValueError( + 'velocity_of_corrosion or time_of_corrosion is missing.' + ) + + if pitting_factor < 1: + raise ValueError( + 'The pitting_factor must be greater than or equal to 1.' + ) + + if uncorroded_area < 0: + raise ValueError( + 'The uncorroded_area must be non-negative.' + ) + + if mass_loss is not None and (mass_loss < 0 or mass_loss > 1): + raise ValueError( + 'mass_loss must be between 0 and 1.' + ) + + if velocity_of_corrosion is not None and (velocity_of_corrosion < 0): + raise ValueError( + 'velocity_of_corrosion must be non-negative.' + ) + + if time_of_corrosion is not None and (time_of_corrosion < 0): + raise ValueError( + 'time_of_corrosion must be non-negative.' + ) + + # Calculate corroded area if mass_loss is given + if mass_loss is not None: + corroded_average_area=uncorroded_area*(1-mass_loss) + corroded_average_pit=uncorroded_diameter/2-sqrt(corroded_average_area/math.pi) + corroded_maximum_pit=pitting_factor*corroded_average_pit + corroded_minimum_area=(uncorroded_diameter/2-corroded_maximum_pit)**2*math.pi + if (uncorroded_diameter/2-corroded_maximum_pit) < 0: + raise ValueError( + 'The combination of mass_loss and pitting_factor gave a corroded_minimum_area' + 'lower than 0. Consider changing these values.' + ) + return corroded_minimum_area + + # Calculate corroded area if time_of_corrosion and velocity_of_corrosion are given + if velocity_of_corrosion is not None and time_of_corrosion is not None: + velocity_of_corrosion=velocity_of_corrosion/1000 #convert from micrometers/year to millimeters/year + corroded_average_pit=velocity_of_corrosion*time_of_corrosion + if corroded_average_pit>uncorroded_diameter: + raise ValueError( + 'The combination of velocity_of_corrosion and time_of_corrosion gave a corroded_minimum_area' + 'lower than 0. Consider changing these values.' + ) + corroded_maximum_pit=pitting_factor*corroded_average_pit + corroded_minimum_area=(uncorroded_diameter/2-corroded_maximum_pit)**2*math.pi + if uncorroded_diameter/2-corroded_maximum_pit < 0: + raise ValueError( + 'The combination of velocity_of_corrosion and time_of_corrosion and pitting_factor' + 'gave a corroded_minimum_area lower than 0. Consider changing these values.' + ) + return corroded_minimum_area + return + +""" +STILL IN PROGRESS + + def calculate_shear_resistance_of_members_without_shear_reinforcement_with_corroded_longitudinal_reinforcement( + fck: float, + gc: float, + z: float, + bw: float, + Es: float, + kdg:float, + As_det: float, + M_Ed: float, + V_Ed: float, + N_Ed: float, + De: float, + k_bond: float = 0.75, + design_code: t.Optional[str] = None, +) -> float: + "" + Compute the design shear resistance Vrdc for members without shear reinforcement + but with corroded longitudinal reinforcement, following MC2020 + Section 30.1.11.3.1 (eqs. 30.1-149 to 30.1-151). + + Parameters: + fck : float + Concrete compressive strength [MPa]. + g_c : float + Partial safety factor for concrete (-). + z : float + Internal lever arm [mm]. + bw : float + Effective web width [mm]. + Es : float + Modulus of elasticity of steel [MPa]. + As_det : float + Residual corroded tensile reinforcement area [mm²]. + M_Ed : float + Design bending moment [N·mm]. + V_Ed : float + Design shear force [N]. + N_Ed : float + Design axial force [N] (positive = tension). + De : float + Effective depth of tensile reinforcement [mm]. + k_bond : float, optional + Bond reduction factor for corrosion. Default: 0.75 (moderate corrosion). + design_code : str, optional + Required design code name (must be MC2020). If None → raise error. + + Returns + ------- + float + Shear resistance V_Rd,c [N]. + + Raises + ------ + ValueError + If input validation fails or code is invalid. + "" + + import math + + # Retrieve active design code + _code = _use_design_code(design_code) + + if _code is None: + raise ValueError( + "The design code is not set. Use structuralcodes.code.set_designcode " + "or pass a valid design_code string." + ) + if _code != "MC2020": + raise ValueError("Only MC2020 shear formulation is implemented (30.1.11.3.1).") + + # Input validation + if any(x < 0 for x in [f_c, g_c, z, bw, Es, As_det]): + raise ValueError("Material and geometric inputs must be non-negative.") + + if k_bond <= 0: + raise ValueError("k_bond must be positive.") + + + # Compute longitudinal strain ε_x,det (Equation 30.1-151) + eps_x_det = (1.0 / (2 * Es * As_det)* (k_bond * M_Ed / z+ V_Ed + N_Ed * (0.5 + De / z))) + + # Compute k_v (Equation 30.1-150) + kv = (0.4 / (1 + 1500 * eps_x_det)) + (1300 / (1000 + k_bond * z)) + + # Compute V_Rd,c (Equation 30.1-149) + Vrdc = kv * math.sqrt(f_c / g_c) * z * bw + + return Vrdc """ + From 66317ff2e86334109379f815596bcaa233adfa4c Mon Sep 17 00:00:00 2001 From: AndreaEste97 Date: Thu, 4 Dec 2025 17:51:27 +0100 Subject: [PATCH 2/7] Test the function calculate_velocity_of_corrosion --- tests/test_corrosion/__ini__.py | 1 + tests/test_corrosion/test_corrosion.py | 78 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 tests/test_corrosion/__ini__.py create mode 100644 tests/test_corrosion/test_corrosion.py diff --git a/tests/test_corrosion/__ini__.py b/tests/test_corrosion/__ini__.py new file mode 100644 index 00000000..8f258b55 --- /dev/null +++ b/tests/test_corrosion/__ini__.py @@ -0,0 +1 @@ +"""tests for corrosion functions""" \ No newline at end of file diff --git a/tests/test_corrosion/test_corrosion.py b/tests/test_corrosion/test_corrosion.py new file mode 100644 index 00000000..d6d079db --- /dev/null +++ b/tests/test_corrosion/test_corrosion.py @@ -0,0 +1,78 @@ +import pytest + +from structuralcodes.development.corrosion import calculate_velocity_of_corrosion,calculate_minimum_area_after_corrosion + +@pytest.mark.parametrize( + "corrosion_type, exposure_class, fractile, expected", + [ + ("carbonation_induced", "Sheltered", 0.5, 2), + ("carbonation_induced", "Unsheltered",0.5, 5), + ("chloride_induced", "Wet",0.5, 4), + ("chloride_induced", "Cyclic_dry_wet",0.5, 30), + ("chloride_induced", "Airborn_seawater",0.5, 30), + ("chloride_induced", "Submerged",0.5, 4), + ("chloride_induced", "Tidal_zone",0.5, 50), + ] +) +def test_calculate_velocity_of_corrosion_without_fractile(corrosion_type, exposure_class, fractile, expected): + from structuralcodes import set_design_code + set_design_code('ec2_2004') + Pcorr_rep = calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) + assert Pcorr_rep == expected + +@pytest.mark.parametrize( + "corrosion_type, exposure_class, fractile, expected", + [ + ("carbonation_induced", "Sheltered", 0.8413, 2+3), + ("carbonation_induced", "Unsheltered",0.8413, 5+1), + ("chloride_induced", "Wet",0.8413, 4+6), + ("chloride_induced", "Cyclic_dry_wet",0.8413, 30+40), + ("chloride_induced", "Airborn_seawater",0.8413, 30+40), + ("chloride_induced", "Submerged",0.8413, 4+7), + ("chloride_induced", "Tidal_zone",0.8413, 50+100), + ("carbonation_induced", "Sheltered", 0.5, 2), + ("carbonation_induced", "Unsheltered",0.5, 5), + ("chloride_induced", "Wet",0.5, 4), + ("chloride_induced", "Cyclic_dry_wet",0.5, 30), + ("chloride_induced", "Airborn_seawater",0.5, 30), + ("chloride_induced", "Submerged",0.5, 4), + ("chloride_induced", "Tidal_zone",0.5, 50), + ] +) +def test_calculate_velocity_of_corrosion_with_fractile(corrosion_type, exposure_class, fractile, expected): + from structuralcodes import set_design_code + set_design_code('ec2_2004') + Pcorr_rep = calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class,fractile=fractile) + assert abs(Pcorr_rep - expected) < expected*0.001 #<0.1% error + +@pytest.mark.parametrize( + "corrosion_type, exposure_class", + [ + ("carbonation_induced", "Wet"), + ("carbonation_induced", "Cyclic_dry_wet"), + ("carbonation_induced", "Airborn_seawater"), + ("carbonation_induced", "Submerged"), + ("carbonation_induced", "Tidal_zone"), + ("chloride_induced", "Sheltered"), + ("chloride_induced", "chloride_induced") + ] +) +def test_wrong_corrosion_type_and_exposure_class_combinations(corrosion_type, exposure_class): + from structuralcodes import set_design_code + with pytest.raises(Exception): + set_design_code('ec2_2004') + calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) + +def test_no_design_code_or_wring_design_code(): + from structuralcodes import set_design_code + with pytest.raises(Exception): + calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered") + with pytest.raises(Exception): + set_design_code('invaliddesigncode') + calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered") + +def test_low_values_of_fractile(): + from structuralcodes import set_design_code + with pytest.raises(Exception): + set_design_code('ec2_2004') + calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered",fractile=0.001) From 7e1d5181a7105f6680864bf303733efc064b74f5 Mon Sep 17 00:00:00 2001 From: AndreaEste97 Date: Fri, 5 Dec 2025 12:14:07 +0100 Subject: [PATCH 3/7] Updated tests for corrosion functions. --- docs/_example_code/usage_corrosion.py | 5 +- .../{__ini__.py => __init__.py} | 0 tests/test_corrosion/test_corrosion.py | 87 ++++++++++++++++++- 3 files changed, 89 insertions(+), 3 deletions(-) rename tests/test_corrosion/{__ini__.py => __init__.py} (100%) diff --git a/docs/_example_code/usage_corrosion.py b/docs/_example_code/usage_corrosion.py index 7abe00bf..98d8ac4c 100644 --- a/docs/_example_code/usage_corrosion.py +++ b/docs/_example_code/usage_corrosion.py @@ -25,8 +25,9 @@ # 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.2,velocity_of_corrosion=110,time_of_corrosion=10) +Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=1,velocity_of_corrosion=100,time_of_corrosion=10) +#Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=1,velocity_of_corrosion=0,time_of_corrosion=0) print("Calculated Minimum_area_after_corrosion = "+str(round(Minimum_area_after_corrosion,2))+" mm2") - +print(7**2/8**2*InitialArea) diff --git a/tests/test_corrosion/__ini__.py b/tests/test_corrosion/__init__.py similarity index 100% rename from tests/test_corrosion/__ini__.py rename to tests/test_corrosion/__init__.py diff --git a/tests/test_corrosion/test_corrosion.py b/tests/test_corrosion/test_corrosion.py index d6d079db..dcf0bcd0 100644 --- a/tests/test_corrosion/test_corrosion.py +++ b/tests/test_corrosion/test_corrosion.py @@ -63,7 +63,7 @@ def test_wrong_corrosion_type_and_exposure_class_combinations(corrosion_type, ex set_design_code('ec2_2004') calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) -def test_no_design_code_or_wring_design_code(): +def test_no_design_code_or_wrong_design_code(): from structuralcodes import set_design_code with pytest.raises(Exception): calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered") @@ -76,3 +76,88 @@ def test_low_values_of_fractile(): with pytest.raises(Exception): set_design_code('ec2_2004') calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered",fractile=0.001) + +from structuralcodes.development.corrosion import calculate_minimum_area_after_corrosion + +InitialArea=8*8*3.141592 + +@pytest.mark.parametrize( + "mass_loss, pitting_factor, expected", + [ + (0, 1, InitialArea), + (0, 2, InitialArea), + (0.5, 1, 0.5*InitialArea), + (0.5, 2, (2*0.70710678118-1)**2*InitialArea), + (0.75, 1, 0.25*InitialArea), + (1, 1, 0), + ] +) +def test_minimum_area_after_corrosion_given_mass_loss_and_pitting_factor(mass_loss, pitting_factor, expected): + from structuralcodes import set_design_code + set_design_code('ec2_2004') + InitialArea=8*8*3.141592 + Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=pitting_factor,mass_loss=mass_loss) + assert abs(Minimum_area_after_corrosion-expected) <= expected *0.00000001 + +@pytest.mark.parametrize( + "velocity_of_corrosion,time_of_corrosion, pitting_factor, expected", + [ + (0,0, 1, InitialArea), + (0,0, 2, InitialArea), + (100,10, 1, (7**2/8**2)*InitialArea), + (100,10, 2, (6**2/8**2)*InitialArea), + (100,40, 1, (4**2/8**2)*InitialArea), + ] +) +def test_minimum_area_after_corrosion_given_velocity_of_corrosion_and_time_of_corrosion(velocity_of_corrosion,time_of_corrosion, pitting_factor, expected): + from structuralcodes import set_design_code + set_design_code('ec2_2004') + InitialArea=8*8*3.141592 + Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( + uncorroded_area=InitialArea,pitting_factor=pitting_factor, + velocity_of_corrosion=velocity_of_corrosion, + time_of_corrosion=time_of_corrosion) + assert abs(Minimum_area_after_corrosion-expected) <= expected *0.0001 + +def test_no_design_code_or_wrong_design_code2(): + from structuralcodes import set_design_code + with pytest.raises(Exception): + InitialArea=8*8*3.141592 + Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( + uncorroded_area=InitialArea,pitting_factor=1, + velocity_of_corrosion=100, + time_of_corrosion=10) + with pytest.raises(Exception): + InitialArea=8*8*3.141592 + set_design_code('invaliddesigncode') + Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( + uncorroded_area=InitialArea,pitting_factor=1, + velocity_of_corrosion=100, + time_of_corrosion=10) + +@pytest.mark.parametrize( + "velocity_of_corrosion, time_of_corrosion, mass_loss, pitting_factor", + [ + (100,10, 0.5, 1), + (100,None, 0.5, 1), + (None,10, 0.5, 1), + (100,100, None, 1), + (100,50, None, 2), + (None,None, 1.5, 1), + (None,None, 0.9, 2), + ] +) +def test_wrong_combinations_or_negative_area_after_corrosion(velocity_of_corrosion,time_of_corrosion, pitting_factor, mass_loss): + from structuralcodes import set_design_code + set_design_code('ec2_2004') + InitialArea=8*8*3.141592 + with pytest.raises(Exception): + Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( + mass_loss=mass_loss, + uncorroded_area=InitialArea, + pitting_factor=pitting_factor, + velocity_of_corrosion=velocity_of_corrosion, + time_of_corrosion=time_of_corrosion) + + + From 2d35d6d0525f2a75de73328c45380ded371ef16e Mon Sep 17 00:00:00 2001 From: AndreaResente97 Date: Wed, 10 Dec 2025 16:28:05 +0100 Subject: [PATCH 4/7] Added comments before pull request. --- structuralcodes/development/corrosion.py | 1 + tests/test_corrosion/test_corrosion.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/structuralcodes/development/corrosion.py b/structuralcodes/development/corrosion.py index bcf57f26..ad11f8ab 100644 --- a/structuralcodes/development/corrosion.py +++ b/structuralcodes/development/corrosion.py @@ -192,6 +192,7 @@ def calculate_minimum_area_after_corrosion( or (ii) using the velocity of corrosion together with the time of corrosion. A pitting factor is used to relate the maximum pit depth to the average pit depth. Actually (02/12/2025) only MC2020 is implemented, see chapter 30.1.11.3.5. + Function is valid for round bars!. Keyword Arguments: uncorroded_area (float): Original (uncorroded) cross-sectional area of the rebar [mm²]. diff --git a/tests/test_corrosion/test_corrosion.py b/tests/test_corrosion/test_corrosion.py index dcf0bcd0..9c85a754 100644 --- a/tests/test_corrosion/test_corrosion.py +++ b/tests/test_corrosion/test_corrosion.py @@ -1,7 +1,10 @@ +"""tests for the functions in structuralcodes.development.corrosion""" + import pytest from structuralcodes.development.corrosion import calculate_velocity_of_corrosion,calculate_minimum_area_after_corrosion +# Should return the velocity of corrosion (average representative value). @pytest.mark.parametrize( "corrosion_type, exposure_class, fractile, expected", [ @@ -20,6 +23,7 @@ def test_calculate_velocity_of_corrosion_without_fractile(corrosion_type, exposu Pcorr_rep = calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) assert Pcorr_rep == expected +# Should return the velocity of corrosion for the relative fractile. @pytest.mark.parametrize( "corrosion_type, exposure_class, fractile, expected", [ @@ -45,6 +49,7 @@ def test_calculate_velocity_of_corrosion_with_fractile(corrosion_type, exposure_ Pcorr_rep = calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class,fractile=fractile) assert abs(Pcorr_rep - expected) < expected*0.001 #<0.1% error +# Should raise error @pytest.mark.parametrize( "corrosion_type, exposure_class", [ @@ -63,6 +68,7 @@ def test_wrong_corrosion_type_and_exposure_class_combinations(corrosion_type, ex set_design_code('ec2_2004') calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) +# Should raise error def test_no_design_code_or_wrong_design_code(): from structuralcodes import set_design_code with pytest.raises(Exception): @@ -71,6 +77,7 @@ def test_no_design_code_or_wrong_design_code(): set_design_code('invaliddesigncode') calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered") +# Should raise error because too low values of fractile gives negative velocity of corrosion. def test_low_values_of_fractile(): from structuralcodes import set_design_code with pytest.raises(Exception): @@ -79,8 +86,10 @@ def test_low_values_of_fractile(): from structuralcodes.development.corrosion import calculate_minimum_area_after_corrosion +# Round bar of diameter=16mm. InitialArea=8*8*3.141592 +# Should return the remaining area after corrosion. @pytest.mark.parametrize( "mass_loss, pitting_factor, expected", [ @@ -99,6 +108,7 @@ def test_minimum_area_after_corrosion_given_mass_loss_and_pitting_factor(mass_lo Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=pitting_factor,mass_loss=mass_loss) assert abs(Minimum_area_after_corrosion-expected) <= expected *0.00000001 +# Should return the remaining area after corrosion. @pytest.mark.parametrize( "velocity_of_corrosion,time_of_corrosion, pitting_factor, expected", [ @@ -119,6 +129,7 @@ def test_minimum_area_after_corrosion_given_velocity_of_corrosion_and_time_of_co time_of_corrosion=time_of_corrosion) assert abs(Minimum_area_after_corrosion-expected) <= expected *0.0001 +# Should raise error def test_no_design_code_or_wrong_design_code2(): from structuralcodes import set_design_code with pytest.raises(Exception): @@ -135,6 +146,7 @@ def test_no_design_code_or_wrong_design_code2(): velocity_of_corrosion=100, time_of_corrosion=10) +# Should raise error @pytest.mark.parametrize( "velocity_of_corrosion, time_of_corrosion, mass_loss, pitting_factor", [ From dae0175b5e01ca26d4e27c30bf79818d5fd08f4c Mon Sep 17 00:00:00 2001 From: AndreaResente97 Date: Mon, 15 Dec 2025 19:35:01 +0100 Subject: [PATCH 5/7] Changes to corrosion after first review. --- docs/_example_code/usage_corrosion.py | 10 +- .../mc2020/_corrosion.py} | 138 +----------------- structuralcodes/development/__init__.py | 0 tests/test_corrosion/test_corrosion.py | 48 +----- 4 files changed, 9 insertions(+), 187 deletions(-) rename structuralcodes/{development/corrosion.py => codes/mc2020/_corrosion.py} (72%) delete mode 100644 structuralcodes/development/__init__.py diff --git a/docs/_example_code/usage_corrosion.py b/docs/_example_code/usage_corrosion.py index 98d8ac4c..40244393 100644 --- a/docs/_example_code/usage_corrosion.py +++ b/docs/_example_code/usage_corrosion.py @@ -1,10 +1,6 @@ """Example code to calculate the velocity_of_corrosion and the area_after_corrosion of rebars.""" -from structuralcodes.development.corrosion import calculate_velocity_of_corrosion,calculate_minimum_area_after_corrosion -from structuralcodes import set_design_code - -# Set design code. Note: actually (04/12/2025) only formule valid for MC2020 are implemented. -set_design_code('ec2_2004') +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") @@ -26,8 +22,8 @@ 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) -#Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=1,velocity_of_corrosion=0,time_of_corrosion=0) + print("Calculated Minimum_area_after_corrosion = "+str(round(Minimum_area_after_corrosion,2))+" mm2") -print(7**2/8**2*InitialArea) + diff --git a/structuralcodes/development/corrosion.py b/structuralcodes/codes/mc2020/_corrosion.py similarity index 72% rename from structuralcodes/development/corrosion.py rename to structuralcodes/codes/mc2020/_corrosion.py index ad11f8ab..e01a2e98 100644 --- a/structuralcodes/development/corrosion.py +++ b/structuralcodes/codes/mc2020/_corrosion.py @@ -2,10 +2,9 @@ from structuralcodes.codes import _use_design_code def calculate_velocity_of_corrosion( - corrosion_type: t.Optional[str] = "chloride_induced", - exposure_class: t.Optional[str] = None, + corrosion_type: t.Literal["carbonation_induced","chloride_induced"], + exposure_class: t.Literal["Sheltered","Unsheltered","Wet","Cyclic_dry_wet","Airborn_seawater","Submerged","Tidal_zone"], fractile: t.Optional[float] = 0.5, - design_code: t.Optional[str] = None, ) -> float: """A function to calculate the representative velocity of corrosion given certain environmental conditions. Actually (02/12/2025) only MC2020 is implemented, see table 30.1-6a and 30.1-6b. @@ -20,35 +19,17 @@ def calculate_velocity_of_corrosion( have a velocity of corrosion lower than Pcorr_rep (the returning value of this function). For example, the velocity of corrosion calculacted with fractile=0.99 can be interpreted as "99% of rebars have a velocity of corrosion lower than Pcorr_rep, giving a higher degree of safety of the returned value Pcorr_rep. (default: "0.5") - design_code (str): Optional string (default: None) indicating the - desired standard. If None (default) the globally used design - standard will be adopted. Otherwise the design standard specified - will be used for the instance of the material. Return: Pcorr_rep (float): representative velocity of corrosion of the rebars, defined as the ratio (asbolute value) between the variation of the rebar's diameter and the time of exposure. Unit of measurement: μm/yr. Raises: - ValueError: if the design code is not valid. 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. ValueError: if the fractile value used causes Pcorr_rep to obtain values lower than 0. """ - # Get the code from the global variable - _code = _use_design_code(design_code) - - # Check if the code is a proper concrete code - code = None - if _code is not None: - code = _code - if code is None: - raise ValueError( - 'The design code is not set, either use ' - 'structuralcodes.code.set_designcode, or provide a valid ' - 'string in the function.' - ) # Check if corrosion_type is valid. ValidCorrosionTypes=["carbonation_induced","chloride_induced"] @@ -61,11 +42,6 @@ def calculate_velocity_of_corrosion( # Set exposure_class to default values if None. Then, check if exposure_class is valid. ValidExposureClassesForCarbonationInducedCorrosion=["Sheltered","Unsheltered"] ValidExposureClassesForChlorideInducedCorrosion=["Wet","Cyclic_dry_wet","Airborn_seawater","Submerged","Tidal_zone"] - if exposure_class is None: - if corrosion_type=="carbonation_induced": - exposure_class="Unsheltered" - elif corrosion_type=="chloride_induced": - exposure_class="Cyclic_dry_wet" if corrosion_type=="carbonation_induced" and exposure_class not in ValidExposureClassesForCarbonationInducedCorrosion: raise ValueError( 'The variable exposure_class is not valid for the current corrosion_type, either use ' \ @@ -184,7 +160,6 @@ def calculate_minimum_area_after_corrosion( mass_loss: t.Optional[float] = None, velocity_of_corrosion: t.Optional[float] = None, time_of_corrosion: t.Optional[float] = None, - design_code: t.Optional[str] = None, ) -> float: """A function to calculate the minimum residual steel area after corrosion. @@ -204,9 +179,6 @@ def calculate_minimum_area_after_corrosion( with time_of_corrosion. If provided, mass_loss must be None. (default: None) time_of_corrosion (float): Time of corrosion [years]. Must be ≥ 0. Used with velocity_of_corrosion. (default: None) - design_code (str): Optional string indicating the desired standard. If None - (default) the globally used design standard will be adopted. Otherwise, - the design standard specified will be used. Return: corroded_minimum_area (float): Minimum residual area of the corroded rebar [mm²], @@ -214,7 +186,6 @@ def calculate_minimum_area_after_corrosion( `pitting_factor * average_pit_depth`. Raises: - ValueError: if the design code is not set or invalid. ValueError: if both mass_loss and (velocity_of_corrosion + time_of_corrosion) are provided simultaneously. ValueError: if only one between velocity_of_corrosion and time_of_corrosion is provided. @@ -228,20 +199,6 @@ def calculate_minimum_area_after_corrosion( import math from math import sqrt uncorroded_diameter=sqrt(uncorroded_area/math.pi)*2 - - # Get the code from the global variable - _code = _use_design_code(design_code) - - # Check if the code is a proper concrete code - code = None - if _code is not None: - code = _code - if code is None: - raise ValueError( - 'The design code is not set, either use ' - 'structuralcodes.code.set_designcode, or provide a valid ' - 'string in the function.' - ) # Input data validation if (mass_loss is not None) and (velocity_of_corrosion is not None or time_of_corrosion is not None): @@ -313,95 +270,4 @@ def calculate_minimum_area_after_corrosion( return corroded_minimum_area return -""" -STILL IN PROGRESS - - def calculate_shear_resistance_of_members_without_shear_reinforcement_with_corroded_longitudinal_reinforcement( - fck: float, - gc: float, - z: float, - bw: float, - Es: float, - kdg:float, - As_det: float, - M_Ed: float, - V_Ed: float, - N_Ed: float, - De: float, - k_bond: float = 0.75, - design_code: t.Optional[str] = None, -) -> float: - "" - Compute the design shear resistance Vrdc for members without shear reinforcement - but with corroded longitudinal reinforcement, following MC2020 - Section 30.1.11.3.1 (eqs. 30.1-149 to 30.1-151). - - Parameters: - fck : float - Concrete compressive strength [MPa]. - g_c : float - Partial safety factor for concrete (-). - z : float - Internal lever arm [mm]. - bw : float - Effective web width [mm]. - Es : float - Modulus of elasticity of steel [MPa]. - As_det : float - Residual corroded tensile reinforcement area [mm²]. - M_Ed : float - Design bending moment [N·mm]. - V_Ed : float - Design shear force [N]. - N_Ed : float - Design axial force [N] (positive = tension). - De : float - Effective depth of tensile reinforcement [mm]. - k_bond : float, optional - Bond reduction factor for corrosion. Default: 0.75 (moderate corrosion). - design_code : str, optional - Required design code name (must be MC2020). If None → raise error. - - Returns - ------- - float - Shear resistance V_Rd,c [N]. - - Raises - ------ - ValueError - If input validation fails or code is invalid. - "" - - import math - - # Retrieve active design code - _code = _use_design_code(design_code) - - if _code is None: - raise ValueError( - "The design code is not set. Use structuralcodes.code.set_designcode " - "or pass a valid design_code string." - ) - if _code != "MC2020": - raise ValueError("Only MC2020 shear formulation is implemented (30.1.11.3.1).") - - # Input validation - if any(x < 0 for x in [f_c, g_c, z, bw, Es, As_det]): - raise ValueError("Material and geometric inputs must be non-negative.") - - if k_bond <= 0: - raise ValueError("k_bond must be positive.") - - - # Compute longitudinal strain ε_x,det (Equation 30.1-151) - eps_x_det = (1.0 / (2 * Es * As_det)* (k_bond * M_Ed / z+ V_Ed + N_Ed * (0.5 + De / z))) - - # Compute k_v (Equation 30.1-150) - kv = (0.4 / (1 + 1500 * eps_x_det)) + (1300 / (1000 + k_bond * z)) - - # Compute V_Rd,c (Equation 30.1-149) - Vrdc = kv * math.sqrt(f_c / g_c) * z * bw - - return Vrdc """ diff --git a/structuralcodes/development/__init__.py b/structuralcodes/development/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_corrosion/test_corrosion.py b/tests/test_corrosion/test_corrosion.py index 9c85a754..1e10846d 100644 --- a/tests/test_corrosion/test_corrosion.py +++ b/tests/test_corrosion/test_corrosion.py @@ -2,7 +2,7 @@ import pytest -from structuralcodes.development.corrosion import calculate_velocity_of_corrosion,calculate_minimum_area_after_corrosion +from structuralcodes.codes.mc2020._corrosion import calculate_velocity_of_corrosion # Should return the velocity of corrosion (average representative value). @pytest.mark.parametrize( @@ -18,8 +18,6 @@ ] ) def test_calculate_velocity_of_corrosion_without_fractile(corrosion_type, exposure_class, fractile, expected): - from structuralcodes import set_design_code - set_design_code('ec2_2004') Pcorr_rep = calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) assert Pcorr_rep == expected @@ -44,12 +42,10 @@ def test_calculate_velocity_of_corrosion_without_fractile(corrosion_type, exposu ] ) def test_calculate_velocity_of_corrosion_with_fractile(corrosion_type, exposure_class, fractile, expected): - from structuralcodes import set_design_code - set_design_code('ec2_2004') Pcorr_rep = calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class,fractile=fractile) assert abs(Pcorr_rep - expected) < expected*0.001 #<0.1% error -# Should raise error +# Should raise error because wrong combinations of corrosion_type and exposure_class are given as input. @pytest.mark.parametrize( "corrosion_type, exposure_class", [ @@ -63,28 +59,15 @@ def test_calculate_velocity_of_corrosion_with_fractile(corrosion_type, exposure_ ] ) def test_wrong_corrosion_type_and_exposure_class_combinations(corrosion_type, exposure_class): - from structuralcodes import set_design_code with pytest.raises(Exception): - set_design_code('ec2_2004') calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) -# Should raise error -def test_no_design_code_or_wrong_design_code(): - from structuralcodes import set_design_code - with pytest.raises(Exception): - calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered") - with pytest.raises(Exception): - set_design_code('invaliddesigncode') - calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered") - # Should raise error because too low values of fractile gives negative velocity of corrosion. def test_low_values_of_fractile(): - from structuralcodes import set_design_code with pytest.raises(Exception): - set_design_code('ec2_2004') calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered",fractile=0.001) -from structuralcodes.development.corrosion import calculate_minimum_area_after_corrosion +from structuralcodes.codes.mc2020._corrosion import calculate_minimum_area_after_corrosion # Round bar of diameter=16mm. InitialArea=8*8*3.141592 @@ -102,8 +85,6 @@ def test_low_values_of_fractile(): ] ) def test_minimum_area_after_corrosion_given_mass_loss_and_pitting_factor(mass_loss, pitting_factor, expected): - from structuralcodes import set_design_code - set_design_code('ec2_2004') InitialArea=8*8*3.141592 Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=pitting_factor,mass_loss=mass_loss) assert abs(Minimum_area_after_corrosion-expected) <= expected *0.00000001 @@ -120,8 +101,6 @@ def test_minimum_area_after_corrosion_given_mass_loss_and_pitting_factor(mass_lo ] ) def test_minimum_area_after_corrosion_given_velocity_of_corrosion_and_time_of_corrosion(velocity_of_corrosion,time_of_corrosion, pitting_factor, expected): - from structuralcodes import set_design_code - set_design_code('ec2_2004') InitialArea=8*8*3.141592 Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( uncorroded_area=InitialArea,pitting_factor=pitting_factor, @@ -129,24 +108,7 @@ def test_minimum_area_after_corrosion_given_velocity_of_corrosion_and_time_of_co time_of_corrosion=time_of_corrosion) assert abs(Minimum_area_after_corrosion-expected) <= expected *0.0001 -# Should raise error -def test_no_design_code_or_wrong_design_code2(): - from structuralcodes import set_design_code - with pytest.raises(Exception): - InitialArea=8*8*3.141592 - Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( - uncorroded_area=InitialArea,pitting_factor=1, - velocity_of_corrosion=100, - time_of_corrosion=10) - with pytest.raises(Exception): - InitialArea=8*8*3.141592 - set_design_code('invaliddesigncode') - Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( - uncorroded_area=InitialArea,pitting_factor=1, - velocity_of_corrosion=100, - time_of_corrosion=10) - -# Should raise error +# Should raise different kind of errors (wrong combinations of input values or negative area after corrosion). @pytest.mark.parametrize( "velocity_of_corrosion, time_of_corrosion, mass_loss, pitting_factor", [ @@ -160,8 +122,6 @@ def test_no_design_code_or_wrong_design_code2(): ] ) def test_wrong_combinations_or_negative_area_after_corrosion(velocity_of_corrosion,time_of_corrosion, pitting_factor, mass_loss): - from structuralcodes import set_design_code - set_design_code('ec2_2004') InitialArea=8*8*3.141592 with pytest.raises(Exception): Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( From c90281f2bde5a0cc51a0e0745097203f71a571f8 Mon Sep 17 00:00:00 2001 From: Diego Talledo Date: Fri, 8 May 2026 16:35:57 +0200 Subject: [PATCH 6/7] Refactor corrosion module and add comprehensive tests --- structuralcodes/codes/mc2020/__init__.py | 4 + structuralcodes/codes/mc2020/_corrosion.py | 516 +++++++++++---------- tests/test_corrosion/__init__.py | 1 - tests/test_corrosion/test_corrosion.py | 135 ------ tests/test_mc2020/test_corrosion.py | 265 +++++++++++ 5 files changed, 537 insertions(+), 384 deletions(-) delete mode 100644 tests/test_corrosion/__init__.py delete mode 100644 tests/test_corrosion/test_corrosion.py create mode 100644 tests/test_mc2020/test_corrosion.py 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 index e01a2e98..51cec739 100644 --- a/structuralcodes/codes/mc2020/_corrosion.py +++ b/structuralcodes/codes/mc2020/_corrosion.py @@ -1,273 +1,293 @@ import typing as t -from structuralcodes.codes import _use_design_code -def calculate_velocity_of_corrosion( - corrosion_type: t.Literal["carbonation_induced","chloride_induced"], - exposure_class: t.Literal["Sheltered","Unsheltered","Wet","Cyclic_dry_wet","Airborn_seawater","Submerged","Tidal_zone"], - fractile: t.Optional[float] = 0.5, -) -> float: - """A function to calculate the representative velocity of corrosion given certain environmental conditions. - Actually (02/12/2025) only MC2020 is implemented, see table 30.1-6a and 30.1-6b. - - Keyword Arguments: - corrosion_type (str): Corrosion type, which can be "carbonation_induced" or "chloride_induced" (default: "chloride_induced"). - exposure_class (str): Exposure class, which can be "Sheltered" or "Unsheltered" for corrosion_type=="carbonation_induced" - and "Wet", "Cyclic_dry_wet", "Airborn_seawater", "Submerged", "Tidal_zone" for corrosion_type=="chloride_induced" - (default: "Unsheltered" for corrosion_type=="carbonation_induced" and "Cyclic_dry_wet" for corrosion_type=="chloride_induced"). - fractile (float): A statistic parameter used to obtain the velocity_of_corrosion at a given number of standard deviation from the - average value. For example, the velocity of corrosion calculacted with fractile=0.5 can be interpreted as "50% of rebars - have a velocity of corrosion lower than Pcorr_rep (the returning value of this function). For example, the velocity of corrosion - calculacted with fractile=0.99 can be interpreted as "99% of rebars have a velocity of corrosion lower than Pcorr_rep, giving a - higher degree of safety of the returned value Pcorr_rep. (default: "0.5") - - Return: - Pcorr_rep (float): representative velocity of corrosion of the rebars, defined as the ratio (asbolute value) between the variation of the rebar's diameter - and the time of exposure. Unit of measurement: μm/yr. +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. - ValueError: if the fractile value is not valid. - ValueError: if the fractile value used causes Pcorr_rep to obtain values lower than 0. + ValueError: if the exposure class is not valid for the relative + corrosion type. """ - - # Check if corrosion_type is valid. - ValidCorrosionTypes=["carbonation_induced","chloride_induced"] - if corrosion_type not in ValidCorrosionTypes: + if not _validate_corrosion_exposure(corrosion_type, exposure_class): raise ValueError( - 'The variable corrosion_type is not valid, either use ' \ - '"carbonation_induced" or "chloride_induced"' - ) - - # Set exposure_class to default values if None. Then, check if exposure_class is valid. - ValidExposureClassesForCarbonationInducedCorrosion=["Sheltered","Unsheltered"] - ValidExposureClassesForChlorideInducedCorrosion=["Wet","Cyclic_dry_wet","Airborn_seawater","Submerged","Tidal_zone"] - if corrosion_type=="carbonation_induced" and exposure_class not in ValidExposureClassesForCarbonationInducedCorrosion: - raise ValueError( - 'The variable exposure_class is not valid for the current corrosion_type, either use ' \ - '"Sheltered" or "Unsheltered"' - ) - if corrosion_type=="chloride_induced" and exposure_class not in ValidExposureClassesForChlorideInducedCorrosion: - raise ValueError( - 'The variable exposure_class is not valid for the current corrosion_type, either use ' \ - '"Wet", "Cyclic_dry_wet", "Airborn_seawater", "Submerged" or "Tidal_zone"' - ) - - # Check if fractile value is valid. - if fractile <=0 or fractile >=1: - raise ValueError( - 'The value of fractile is not valid, use a value between 0 and 1 (both excluded)' + '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[ - # Calculate MeanValue and StandardDeviation - if corrosion_type == "carbonation_induced": - table_mean = { - "Sheltered": 2, - "Unsheltered": 5, - } - table_sd = { - "Sheltered": 3, - "Unsheltered": 1, # the table shows "t", but assuming 1 μm/yr (fix if needed) - } - elif corrosion_type == "chloride_induced": - table_mean = { - "Wet": 4, - "Cyclic_dry_wet": 30, - "Airborn_seawater": 30, - "Submerged": 4, - "Tidal_zone": 50, - } - table_sd = { - "Wet": 6, - "Cyclic_dry_wet": 40, - "Airborn_seawater": 40, - "Submerged": 7, - "Tidal_zone": 100, - } - mean_value = table_mean[exposure_class] - sd_value = table_sd[exposure_class] - - # Calculate Pcorr_rep for the correct fractile value (use Acklam's algorithm for the approximation of the inverse - # standard normal CDF in order to avoid to add other dependencies). - def inverse_normal_cdf(p: float) -> float: - """Approximation of the inverse standard normal CDF (Acklam's algorithm).""" - if p <= 0.0 or p >= 1.0: - raise ValueError("p must be in (0,1)") - - import math - from math import sqrt - - # Coefficients for the approximation - a = [ -3.969683028665376e+01, - 2.209460984245205e+02, - -2.759285104469687e+02, - 1.383577518672690e+02, - -3.066479806614716e+01, - 2.506628277459239e+00 ] - - b = [ -5.447609879822406e+01, - 1.615858368580409e+02, - -1.556989798598866e+02, - 6.680131188771972e+01, - -1.328068155288572e+01 ] - - c = [ -7.784894002430293e-03, - -3.223964580411365e-01, - -2.400758277161838e+00, - -2.549732539343734e+00, - 4.374664141464968e+00, - 2.938163982698783e+00 ] - - d = [ 7.784695709041462e-03, - 3.224671290700398e-01, - 2.445134137142996e+00, - 3.754408661907416e+00 ] - - # Define break-points - plow = 0.02425 - phigh = 1 - plow - - if p < plow: - q = sqrt(-2 * math.log(p)) - return (((((c[0]*q + c[1])*q + c[2])*q + c[3])*q + c[4])*q + c[5]) / \ - ((((d[0]*q + d[1])*q + d[2])*q + d[3])*q + 1) - elif p > phigh: - q = sqrt(-2 * math.log(1 - p)) - return -(((((c[0]*q + c[1])*q + c[2])*q + c[3])*q + c[4])*q + c[5]) / \ - ((((d[0]*q + d[1])*q + d[2])*q + d[3])*q + 1) - else: - q = p - 0.5 - r = q * q - return (((((a[0]*r + a[1])*r + a[2])*r + a[3])*r + a[4])*r + a[5]) * q / \ - (((((b[0]*r + b[1])*r + b[2])*r + b[3])*r + b[4])*r + 1) - - z = inverse_normal_cdf(fractile) - Pcorr_rep = mean_value + z * sd_value - - # Validate the result, which could be <=0 if fractile is too low. - if Pcorr_rep<=0: + 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 calculated value of Pcorr_rep is less or equal to 0 because the value of fractile is too low. '\ - 'Try using a higher value of fractile. Note that using a low value of fractile could lead to an '\ - 'underestimention of the effect of corrosion.' + 'The value of fractile is not valid, use a value between 0 and 1.' ) + pcorr_dic = pcorr_rep(corrosion_type, exposure_class) - return Pcorr_rep + # 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 calculate_minimum_area_after_corrosion( - uncorroded_area: float, - pitting_factor: t.Optional[float] = 1, - mass_loss: t.Optional[float] = None, - velocity_of_corrosion: t.Optional[float] = None, - time_of_corrosion: t.Optional[float] = None, -) -> float: - - """A function to calculate the minimum residual steel area after corrosion. - The function allows two alternative approaches: (i) using the total mass loss, - or (ii) using the velocity of corrosion together with the time of corrosion. - A pitting factor is used to relate the maximum pit depth to the average pit depth. - Actually (02/12/2025) only MC2020 is implemented, see chapter 30.1.11.3.5. - Function is valid for round bars!. - - Keyword Arguments: - uncorroded_area (float): Original (uncorroded) cross-sectional area of the rebar [mm²]. - pitting_factor (float): Ratio between maximum pit depth and average pit depth - (default: 1). Must be ≥ 1. - mass_loss (float): Fractional mass loss (0–1). If provided, velocity_of_corrosion - and time_of_corrosion must be None. (default: None) - velocity_of_corrosion (float): Corrosion rate [μm/yr]. Must be ≥ 0. Used together - with time_of_corrosion. If provided, mass_loss must be None. (default: None) - time_of_corrosion (float): Time of corrosion [years]. Must be ≥ 0. Used with - velocity_of_corrosion. (default: None) - - Return: - corroded_minimum_area (float): Minimum residual area of the corroded rebar [mm²], - computed assuming axisymmetric corrosion with maximum pit depth defined by - `pitting_factor * average_pit_depth`. + +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 both mass_loss and (velocity_of_corrosion + time_of_corrosion) - are provided simultaneously. - ValueError: if only one between velocity_of_corrosion and time_of_corrosion is provided. - ValueError: if pitting_factor < 1. - ValueError: if uncorroded_area < 0. - ValueError: if mass_loss is not in the range [0, 1]. - ValueError: if velocity_of_corrosion < 0 or time_of_corrosion < 0. - ValueError: if the computed minimum residual radius becomes negative. + 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 - import math - from math import sqrt - uncorroded_diameter=sqrt(uncorroded_area/math.pi)*2 - - # Input data validation - if (mass_loss is not None) and (velocity_of_corrosion is not None or time_of_corrosion is not None): - raise ValueError( - 'Too many input arguments have been given to the function. ' - 'Either use mass_loss or velocity_of_corrosion + time_of_corrosion.' - ) - if (velocity_of_corrosion is not None and time_of_corrosion is None) or \ - (velocity_of_corrosion is None and time_of_corrosion is not None): - raise ValueError( - 'velocity_of_corrosion or time_of_corrosion is missing.' - ) +# def calculate_minimum_area_after_corrosion( +# uncorroded_area: float, +# pitting_factor: t.Optional[float] = 1, +# mass_loss: t.Optional[float] = None, +# velocity_of_corrosion: t.Optional[float] = None, +# time_of_corrosion: t.Optional[float] = None, +# ) -> float: +# """A function to calculate the minimum residual steel area after corrosion. +# The function allows two alternative approaches: (i) using the total mass loss, +# or (ii) using the velocity of corrosion together with the time of corrosion. +# A pitting factor is used to relate the maximum pit depth to the average pit depth. +# Actually (02/12/2025) only MC2020 is implemented, see chapter 30.1.11.3.5. +# Function is valid for round bars!. - if pitting_factor < 1: - raise ValueError( - 'The pitting_factor must be greater than or equal to 1.' - ) +# Keyword Arguments: +# uncorroded_area (float): Original (uncorroded) cross-sectional area of the rebar [mm²]. +# pitting_factor (float): Ratio between maximum pit depth and average pit depth +# (default: 1). Must be ≥ 1. +# mass_loss (float): Fractional mass loss (0–1). If provided, velocity_of_corrosion +# and time_of_corrosion must be None. (default: None) +# velocity_of_corrosion (float): Corrosion rate [μm/yr]. Must be ≥ 0. Used together +# with time_of_corrosion. If provided, mass_loss must be None. (default: None) +# time_of_corrosion (float): Time of corrosion [years]. Must be ≥ 0. Used with +# velocity_of_corrosion. (default: None) - if uncorroded_area < 0: - raise ValueError( - 'The uncorroded_area must be non-negative.' - ) +# Return: +# corroded_minimum_area (float): Minimum residual area of the corroded rebar [mm²], +# computed assuming axisymmetric corrosion with maximum pit depth defined by +# `pitting_factor * average_pit_depth`. - if mass_loss is not None and (mass_loss < 0 or mass_loss > 1): - raise ValueError( - 'mass_loss must be between 0 and 1.' - ) +# Raises: +# ValueError: if both mass_loss and (velocity_of_corrosion + time_of_corrosion) +# are provided simultaneously. +# ValueError: if only one between velocity_of_corrosion and time_of_corrosion is provided. +# ValueError: if pitting_factor < 1. +# ValueError: if uncorroded_area < 0. +# ValueError: if mass_loss is not in the range [0, 1]. +# ValueError: if velocity_of_corrosion < 0 or time_of_corrosion < 0. +# ValueError: if the computed minimum residual radius becomes negative. +# """ +# import math +# from math import sqrt - if velocity_of_corrosion is not None and (velocity_of_corrosion < 0): - raise ValueError( - 'velocity_of_corrosion must be non-negative.' - ) +# uncorroded_diameter = sqrt(uncorroded_area / math.pi) * 2 - if time_of_corrosion is not None and (time_of_corrosion < 0): - raise ValueError( - 'time_of_corrosion must be non-negative.' - ) - - # Calculate corroded area if mass_loss is given - if mass_loss is not None: - corroded_average_area=uncorroded_area*(1-mass_loss) - corroded_average_pit=uncorroded_diameter/2-sqrt(corroded_average_area/math.pi) - corroded_maximum_pit=pitting_factor*corroded_average_pit - corroded_minimum_area=(uncorroded_diameter/2-corroded_maximum_pit)**2*math.pi - if (uncorroded_diameter/2-corroded_maximum_pit) < 0: - raise ValueError( - 'The combination of mass_loss and pitting_factor gave a corroded_minimum_area' - 'lower than 0. Consider changing these values.' - ) - return corroded_minimum_area - - # Calculate corroded area if time_of_corrosion and velocity_of_corrosion are given - if velocity_of_corrosion is not None and time_of_corrosion is not None: - velocity_of_corrosion=velocity_of_corrosion/1000 #convert from micrometers/year to millimeters/year - corroded_average_pit=velocity_of_corrosion*time_of_corrosion - if corroded_average_pit>uncorroded_diameter: - raise ValueError( - 'The combination of velocity_of_corrosion and time_of_corrosion gave a corroded_minimum_area' - 'lower than 0. Consider changing these values.' - ) - corroded_maximum_pit=pitting_factor*corroded_average_pit - corroded_minimum_area=(uncorroded_diameter/2-corroded_maximum_pit)**2*math.pi - if uncorroded_diameter/2-corroded_maximum_pit < 0: - raise ValueError( - 'The combination of velocity_of_corrosion and time_of_corrosion and pitting_factor' - 'gave a corroded_minimum_area lower than 0. Consider changing these values.' - ) - return corroded_minimum_area - return +# # Input data validation +# if (mass_loss is not None) and ( +# velocity_of_corrosion is not None or time_of_corrosion is not None +# ): +# raise ValueError( +# 'Too many input arguments have been given to the function. ' +# 'Either use mass_loss or velocity_of_corrosion + time_of_corrosion.' +# ) + +# if (velocity_of_corrosion is not None and time_of_corrosion is None) or ( +# velocity_of_corrosion is None and time_of_corrosion is not None +# ): +# raise ValueError( +# 'velocity_of_corrosion or time_of_corrosion is missing.' +# ) + +# if pitting_factor < 1: +# raise ValueError( +# 'The pitting_factor must be greater than or equal to 1.' +# ) + +# if uncorroded_area < 0: +# raise ValueError('The uncorroded_area must be non-negative.') + +# if mass_loss is not None and (mass_loss < 0 or mass_loss > 1): +# raise ValueError('mass_loss must be between 0 and 1.') + +# if velocity_of_corrosion is not None and (velocity_of_corrosion < 0): +# raise ValueError('velocity_of_corrosion must be non-negative.') + +# if time_of_corrosion is not None and (time_of_corrosion < 0): +# raise ValueError('time_of_corrosion must be non-negative.') +# # Calculate corroded area if mass_loss is given +# if mass_loss is not None: +# corroded_average_area = uncorroded_area * (1 - mass_loss) +# corroded_average_pit = uncorroded_diameter / 2 - sqrt( +# corroded_average_area / math.pi +# ) +# corroded_maximum_pit = pitting_factor * corroded_average_pit +# corroded_minimum_area = ( +# uncorroded_diameter / 2 - corroded_maximum_pit +# ) ** 2 * math.pi +# if (uncorroded_diameter / 2 - corroded_maximum_pit) < 0: +# raise ValueError( +# 'The combination of mass_loss and pitting_factor gave a corroded_minimum_area' +# 'lower than 0. Consider changing these values.' +# ) +# return corroded_minimum_area +# # Calculate corroded area if time_of_corrosion and velocity_of_corrosion are given +# if velocity_of_corrosion is not None and time_of_corrosion is not None: +# velocity_of_corrosion = ( +# velocity_of_corrosion / 1000 +# ) # convert from micrometers/year to millimeters/year +# corroded_average_pit = velocity_of_corrosion * time_of_corrosion +# if corroded_average_pit > uncorroded_diameter: +# raise ValueError( +# 'The combination of velocity_of_corrosion and time_of_corrosion gave a corroded_minimum_area' +# 'lower than 0. Consider changing these values.' +# ) +# corroded_maximum_pit = pitting_factor * corroded_average_pit +# corroded_minimum_area = ( +# uncorroded_diameter / 2 - corroded_maximum_pit +# ) ** 2 * math.pi +# if uncorroded_diameter / 2 - corroded_maximum_pit < 0: +# raise ValueError( +# 'The combination of velocity_of_corrosion and time_of_corrosion and pitting_factor' +# 'gave a corroded_minimum_area lower than 0. Consider changing these values.' +# ) +# return corroded_minimum_area +# return None diff --git a/tests/test_corrosion/__init__.py b/tests/test_corrosion/__init__.py deleted file mode 100644 index 8f258b55..00000000 --- a/tests/test_corrosion/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""tests for corrosion functions""" \ No newline at end of file diff --git a/tests/test_corrosion/test_corrosion.py b/tests/test_corrosion/test_corrosion.py deleted file mode 100644 index 1e10846d..00000000 --- a/tests/test_corrosion/test_corrosion.py +++ /dev/null @@ -1,135 +0,0 @@ -"""tests for the functions in structuralcodes.development.corrosion""" - -import pytest - -from structuralcodes.codes.mc2020._corrosion import calculate_velocity_of_corrosion - -# Should return the velocity of corrosion (average representative value). -@pytest.mark.parametrize( - "corrosion_type, exposure_class, fractile, expected", - [ - ("carbonation_induced", "Sheltered", 0.5, 2), - ("carbonation_induced", "Unsheltered",0.5, 5), - ("chloride_induced", "Wet",0.5, 4), - ("chloride_induced", "Cyclic_dry_wet",0.5, 30), - ("chloride_induced", "Airborn_seawater",0.5, 30), - ("chloride_induced", "Submerged",0.5, 4), - ("chloride_induced", "Tidal_zone",0.5, 50), - ] -) -def test_calculate_velocity_of_corrosion_without_fractile(corrosion_type, exposure_class, fractile, expected): - Pcorr_rep = calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) - assert Pcorr_rep == expected - -# Should return the velocity of corrosion for the relative fractile. -@pytest.mark.parametrize( - "corrosion_type, exposure_class, fractile, expected", - [ - ("carbonation_induced", "Sheltered", 0.8413, 2+3), - ("carbonation_induced", "Unsheltered",0.8413, 5+1), - ("chloride_induced", "Wet",0.8413, 4+6), - ("chloride_induced", "Cyclic_dry_wet",0.8413, 30+40), - ("chloride_induced", "Airborn_seawater",0.8413, 30+40), - ("chloride_induced", "Submerged",0.8413, 4+7), - ("chloride_induced", "Tidal_zone",0.8413, 50+100), - ("carbonation_induced", "Sheltered", 0.5, 2), - ("carbonation_induced", "Unsheltered",0.5, 5), - ("chloride_induced", "Wet",0.5, 4), - ("chloride_induced", "Cyclic_dry_wet",0.5, 30), - ("chloride_induced", "Airborn_seawater",0.5, 30), - ("chloride_induced", "Submerged",0.5, 4), - ("chloride_induced", "Tidal_zone",0.5, 50), - ] -) -def test_calculate_velocity_of_corrosion_with_fractile(corrosion_type, exposure_class, fractile, expected): - Pcorr_rep = calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class,fractile=fractile) - assert abs(Pcorr_rep - expected) < expected*0.001 #<0.1% error - -# Should raise error because wrong combinations of corrosion_type and exposure_class are given as input. -@pytest.mark.parametrize( - "corrosion_type, exposure_class", - [ - ("carbonation_induced", "Wet"), - ("carbonation_induced", "Cyclic_dry_wet"), - ("carbonation_induced", "Airborn_seawater"), - ("carbonation_induced", "Submerged"), - ("carbonation_induced", "Tidal_zone"), - ("chloride_induced", "Sheltered"), - ("chloride_induced", "chloride_induced") - ] -) -def test_wrong_corrosion_type_and_exposure_class_combinations(corrosion_type, exposure_class): - with pytest.raises(Exception): - calculate_velocity_of_corrosion(corrosion_type=corrosion_type,exposure_class=exposure_class) - -# Should raise error because too low values of fractile gives negative velocity of corrosion. -def test_low_values_of_fractile(): - with pytest.raises(Exception): - calculate_velocity_of_corrosion(corrosion_type="carbonation_induced",exposure_class="Sheltered",fractile=0.001) - -from structuralcodes.codes.mc2020._corrosion import calculate_minimum_area_after_corrosion - -# Round bar of diameter=16mm. -InitialArea=8*8*3.141592 - -# Should return the remaining area after corrosion. -@pytest.mark.parametrize( - "mass_loss, pitting_factor, expected", - [ - (0, 1, InitialArea), - (0, 2, InitialArea), - (0.5, 1, 0.5*InitialArea), - (0.5, 2, (2*0.70710678118-1)**2*InitialArea), - (0.75, 1, 0.25*InitialArea), - (1, 1, 0), - ] -) -def test_minimum_area_after_corrosion_given_mass_loss_and_pitting_factor(mass_loss, pitting_factor, expected): - InitialArea=8*8*3.141592 - Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion(uncorroded_area=InitialArea,pitting_factor=pitting_factor,mass_loss=mass_loss) - assert abs(Minimum_area_after_corrosion-expected) <= expected *0.00000001 - -# Should return the remaining area after corrosion. -@pytest.mark.parametrize( - "velocity_of_corrosion,time_of_corrosion, pitting_factor, expected", - [ - (0,0, 1, InitialArea), - (0,0, 2, InitialArea), - (100,10, 1, (7**2/8**2)*InitialArea), - (100,10, 2, (6**2/8**2)*InitialArea), - (100,40, 1, (4**2/8**2)*InitialArea), - ] -) -def test_minimum_area_after_corrosion_given_velocity_of_corrosion_and_time_of_corrosion(velocity_of_corrosion,time_of_corrosion, pitting_factor, expected): - InitialArea=8*8*3.141592 - Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( - uncorroded_area=InitialArea,pitting_factor=pitting_factor, - velocity_of_corrosion=velocity_of_corrosion, - time_of_corrosion=time_of_corrosion) - assert abs(Minimum_area_after_corrosion-expected) <= expected *0.0001 - -# Should raise different kind of errors (wrong combinations of input values or negative area after corrosion). -@pytest.mark.parametrize( - "velocity_of_corrosion, time_of_corrosion, mass_loss, pitting_factor", - [ - (100,10, 0.5, 1), - (100,None, 0.5, 1), - (None,10, 0.5, 1), - (100,100, None, 1), - (100,50, None, 2), - (None,None, 1.5, 1), - (None,None, 0.9, 2), - ] -) -def test_wrong_combinations_or_negative_area_after_corrosion(velocity_of_corrosion,time_of_corrosion, pitting_factor, mass_loss): - InitialArea=8*8*3.141592 - with pytest.raises(Exception): - Minimum_area_after_corrosion=calculate_minimum_area_after_corrosion( - mass_loss=mass_loss, - uncorroded_area=InitialArea, - pitting_factor=pitting_factor, - velocity_of_corrosion=velocity_of_corrosion, - time_of_corrosion=time_of_corrosion) - - - diff --git a/tests/test_mc2020/test_corrosion.py b/tests/test_mc2020/test_corrosion.py new file mode 100644 index 00000000..0fa50310 --- /dev/null +++ b/tests/test_mc2020/test_corrosion.py @@ -0,0 +1,265 @@ +"""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) + + +# # Should return the remaining area after corrosion. +# @pytest.mark.parametrize( +# 'mass_loss, pitting_factor, expected', +# [ +# (0, 1, InitialArea), +# (0, 2, InitialArea), +# (0.5, 1, 0.5 * InitialArea), +# (0.5, 2, (2 * 0.70710678118 - 1) ** 2 * InitialArea), +# (0.75, 1, 0.25 * InitialArea), +# (1, 1, 0), +# ], +# ) +# def test_minimum_area_after_corrosion_given_mass_loss_and_pitting_factor( +# mass_loss, pitting_factor, expected +# ): +# InitialArea = 8 * 8 * 3.141592 +# Minimum_area_after_corrosion = calculate_minimum_area_after_corrosion( +# uncorroded_area=InitialArea, +# pitting_factor=pitting_factor, +# mass_loss=mass_loss, +# ) +# assert ( +# abs(Minimum_area_after_corrosion - expected) <= expected * 0.00000001 +# ) + + +# # Should return the remaining area after corrosion. +# @pytest.mark.parametrize( +# 'velocity_of_corrosion,time_of_corrosion, pitting_factor, expected', +# [ +# (0, 0, 1, InitialArea), +# (0, 0, 2, InitialArea), +# (100, 10, 1, (7**2 / 8**2) * InitialArea), +# (100, 10, 2, (6**2 / 8**2) * InitialArea), +# (100, 40, 1, (4**2 / 8**2) * InitialArea), +# ], +# ) +# def test_minimum_area_after_corrosion_given_velocity_of_corrosion_and_time_of_corrosion( +# velocity_of_corrosion, time_of_corrosion, pitting_factor, expected +# ): +# InitialArea = 8 * 8 * 3.141592 +# Minimum_area_after_corrosion = calculate_minimum_area_after_corrosion( +# uncorroded_area=InitialArea, +# pitting_factor=pitting_factor, +# velocity_of_corrosion=velocity_of_corrosion, +# time_of_corrosion=time_of_corrosion, +# ) +# assert abs(Minimum_area_after_corrosion - expected) <= expected * 0.0001 + + +# # Should raise different kind of errors (wrong combinations of input values or negative area after corrosion). +# @pytest.mark.parametrize( +# 'velocity_of_corrosion, time_of_corrosion, mass_loss, pitting_factor', +# [ +# (100, 10, 0.5, 1), +# (100, None, 0.5, 1), +# (None, 10, 0.5, 1), +# (100, 100, None, 1), +# (100, 50, None, 2), +# (None, None, 1.5, 1), +# (None, None, 0.9, 2), +# ], +# ) +# def test_wrong_combinations_or_negative_area_after_corrosion( +# velocity_of_corrosion, time_of_corrosion, pitting_factor, mass_loss +# ): +# InitialArea = 8 * 8 * 3.141592 +# with pytest.raises(Exception): +# Minimum_area_after_corrosion = calculate_minimum_area_after_corrosion( +# mass_loss=mass_loss, +# uncorroded_area=InitialArea, +# pitting_factor=pitting_factor, +# velocity_of_corrosion=velocity_of_corrosion, +# time_of_corrosion=time_of_corrosion, +# ) From 920ce72a3decb0aca47c23e72246b5c74856509e Mon Sep 17 00:00:00 2001 From: Diego Talledo Date: Fri, 8 May 2026 16:49:32 +0200 Subject: [PATCH 7/7] remove commented-out code for minimum area calculation in corrosion tests --- structuralcodes/codes/mc2020/_corrosion.py | 119 --------------------- tests/test_mc2020/test_corrosion.py | 77 ------------- 2 files changed, 196 deletions(-) diff --git a/structuralcodes/codes/mc2020/_corrosion.py b/structuralcodes/codes/mc2020/_corrosion.py index 51cec739..260680f1 100644 --- a/structuralcodes/codes/mc2020/_corrosion.py +++ b/structuralcodes/codes/mc2020/_corrosion.py @@ -172,122 +172,3 @@ def kc(fc: float) -> float: raise ValueError('fc must be a positive value.') eta_fc = min((30 / fc) ** (1 / 3.0), 1) return 0.75 * eta_fc - - -# def calculate_minimum_area_after_corrosion( -# uncorroded_area: float, -# pitting_factor: t.Optional[float] = 1, -# mass_loss: t.Optional[float] = None, -# velocity_of_corrosion: t.Optional[float] = None, -# time_of_corrosion: t.Optional[float] = None, -# ) -> float: -# """A function to calculate the minimum residual steel area after corrosion. -# The function allows two alternative approaches: (i) using the total mass loss, -# or (ii) using the velocity of corrosion together with the time of corrosion. -# A pitting factor is used to relate the maximum pit depth to the average pit depth. -# Actually (02/12/2025) only MC2020 is implemented, see chapter 30.1.11.3.5. -# Function is valid for round bars!. - -# Keyword Arguments: -# uncorroded_area (float): Original (uncorroded) cross-sectional area of the rebar [mm²]. -# pitting_factor (float): Ratio between maximum pit depth and average pit depth -# (default: 1). Must be ≥ 1. -# mass_loss (float): Fractional mass loss (0–1). If provided, velocity_of_corrosion -# and time_of_corrosion must be None. (default: None) -# velocity_of_corrosion (float): Corrosion rate [μm/yr]. Must be ≥ 0. Used together -# with time_of_corrosion. If provided, mass_loss must be None. (default: None) -# time_of_corrosion (float): Time of corrosion [years]. Must be ≥ 0. Used with -# velocity_of_corrosion. (default: None) - -# Return: -# corroded_minimum_area (float): Minimum residual area of the corroded rebar [mm²], -# computed assuming axisymmetric corrosion with maximum pit depth defined by -# `pitting_factor * average_pit_depth`. - -# Raises: -# ValueError: if both mass_loss and (velocity_of_corrosion + time_of_corrosion) -# are provided simultaneously. -# ValueError: if only one between velocity_of_corrosion and time_of_corrosion is provided. -# ValueError: if pitting_factor < 1. -# ValueError: if uncorroded_area < 0. -# ValueError: if mass_loss is not in the range [0, 1]. -# ValueError: if velocity_of_corrosion < 0 or time_of_corrosion < 0. -# ValueError: if the computed minimum residual radius becomes negative. -# """ -# import math -# from math import sqrt - -# uncorroded_diameter = sqrt(uncorroded_area / math.pi) * 2 - -# # Input data validation -# if (mass_loss is not None) and ( -# velocity_of_corrosion is not None or time_of_corrosion is not None -# ): -# raise ValueError( -# 'Too many input arguments have been given to the function. ' -# 'Either use mass_loss or velocity_of_corrosion + time_of_corrosion.' -# ) - -# if (velocity_of_corrosion is not None and time_of_corrosion is None) or ( -# velocity_of_corrosion is None and time_of_corrosion is not None -# ): -# raise ValueError( -# 'velocity_of_corrosion or time_of_corrosion is missing.' -# ) - -# if pitting_factor < 1: -# raise ValueError( -# 'The pitting_factor must be greater than or equal to 1.' -# ) - -# if uncorroded_area < 0: -# raise ValueError('The uncorroded_area must be non-negative.') - -# if mass_loss is not None and (mass_loss < 0 or mass_loss > 1): -# raise ValueError('mass_loss must be between 0 and 1.') - -# if velocity_of_corrosion is not None and (velocity_of_corrosion < 0): -# raise ValueError('velocity_of_corrosion must be non-negative.') - -# if time_of_corrosion is not None and (time_of_corrosion < 0): -# raise ValueError('time_of_corrosion must be non-negative.') - -# # Calculate corroded area if mass_loss is given -# if mass_loss is not None: -# corroded_average_area = uncorroded_area * (1 - mass_loss) -# corroded_average_pit = uncorroded_diameter / 2 - sqrt( -# corroded_average_area / math.pi -# ) -# corroded_maximum_pit = pitting_factor * corroded_average_pit -# corroded_minimum_area = ( -# uncorroded_diameter / 2 - corroded_maximum_pit -# ) ** 2 * math.pi -# if (uncorroded_diameter / 2 - corroded_maximum_pit) < 0: -# raise ValueError( -# 'The combination of mass_loss and pitting_factor gave a corroded_minimum_area' -# 'lower than 0. Consider changing these values.' -# ) -# return corroded_minimum_area - -# # Calculate corroded area if time_of_corrosion and velocity_of_corrosion are given -# if velocity_of_corrosion is not None and time_of_corrosion is not None: -# velocity_of_corrosion = ( -# velocity_of_corrosion / 1000 -# ) # convert from micrometers/year to millimeters/year -# corroded_average_pit = velocity_of_corrosion * time_of_corrosion -# if corroded_average_pit > uncorroded_diameter: -# raise ValueError( -# 'The combination of velocity_of_corrosion and time_of_corrosion gave a corroded_minimum_area' -# 'lower than 0. Consider changing these values.' -# ) -# corroded_maximum_pit = pitting_factor * corroded_average_pit -# corroded_minimum_area = ( -# uncorroded_diameter / 2 - corroded_maximum_pit -# ) ** 2 * math.pi -# if uncorroded_diameter / 2 - corroded_maximum_pit < 0: -# raise ValueError( -# 'The combination of velocity_of_corrosion and time_of_corrosion and pitting_factor' -# 'gave a corroded_minimum_area lower than 0. Consider changing these values.' -# ) -# return corroded_minimum_area -# return None diff --git a/tests/test_mc2020/test_corrosion.py b/tests/test_mc2020/test_corrosion.py index 0fa50310..8901a8a8 100644 --- a/tests/test_mc2020/test_corrosion.py +++ b/tests/test_mc2020/test_corrosion.py @@ -186,80 +186,3 @@ def test_kc_invalid(fc): """Test kc with invalid input.""" with pytest.raises(ValueError): kc(fc) - - -# # Should return the remaining area after corrosion. -# @pytest.mark.parametrize( -# 'mass_loss, pitting_factor, expected', -# [ -# (0, 1, InitialArea), -# (0, 2, InitialArea), -# (0.5, 1, 0.5 * InitialArea), -# (0.5, 2, (2 * 0.70710678118 - 1) ** 2 * InitialArea), -# (0.75, 1, 0.25 * InitialArea), -# (1, 1, 0), -# ], -# ) -# def test_minimum_area_after_corrosion_given_mass_loss_and_pitting_factor( -# mass_loss, pitting_factor, expected -# ): -# InitialArea = 8 * 8 * 3.141592 -# Minimum_area_after_corrosion = calculate_minimum_area_after_corrosion( -# uncorroded_area=InitialArea, -# pitting_factor=pitting_factor, -# mass_loss=mass_loss, -# ) -# assert ( -# abs(Minimum_area_after_corrosion - expected) <= expected * 0.00000001 -# ) - - -# # Should return the remaining area after corrosion. -# @pytest.mark.parametrize( -# 'velocity_of_corrosion,time_of_corrosion, pitting_factor, expected', -# [ -# (0, 0, 1, InitialArea), -# (0, 0, 2, InitialArea), -# (100, 10, 1, (7**2 / 8**2) * InitialArea), -# (100, 10, 2, (6**2 / 8**2) * InitialArea), -# (100, 40, 1, (4**2 / 8**2) * InitialArea), -# ], -# ) -# def test_minimum_area_after_corrosion_given_velocity_of_corrosion_and_time_of_corrosion( -# velocity_of_corrosion, time_of_corrosion, pitting_factor, expected -# ): -# InitialArea = 8 * 8 * 3.141592 -# Minimum_area_after_corrosion = calculate_minimum_area_after_corrosion( -# uncorroded_area=InitialArea, -# pitting_factor=pitting_factor, -# velocity_of_corrosion=velocity_of_corrosion, -# time_of_corrosion=time_of_corrosion, -# ) -# assert abs(Minimum_area_after_corrosion - expected) <= expected * 0.0001 - - -# # Should raise different kind of errors (wrong combinations of input values or negative area after corrosion). -# @pytest.mark.parametrize( -# 'velocity_of_corrosion, time_of_corrosion, mass_loss, pitting_factor', -# [ -# (100, 10, 0.5, 1), -# (100, None, 0.5, 1), -# (None, 10, 0.5, 1), -# (100, 100, None, 1), -# (100, 50, None, 2), -# (None, None, 1.5, 1), -# (None, None, 0.9, 2), -# ], -# ) -# def test_wrong_combinations_or_negative_area_after_corrosion( -# velocity_of_corrosion, time_of_corrosion, pitting_factor, mass_loss -# ): -# InitialArea = 8 * 8 * 3.141592 -# with pytest.raises(Exception): -# Minimum_area_after_corrosion = calculate_minimum_area_after_corrosion( -# mass_loss=mass_loss, -# uncorroded_area=InitialArea, -# pitting_factor=pitting_factor, -# velocity_of_corrosion=velocity_of_corrosion, -# time_of_corrosion=time_of_corrosion, -# )