diff --git a/structuralcodes/codes/ec2_2023/__init__.py b/structuralcodes/codes/ec2_2023/__init__.py index c8662148..8627f73b 100644 --- a/structuralcodes/codes/ec2_2023/__init__.py +++ b/structuralcodes/codes/ec2_2023/__init__.py @@ -38,6 +38,20 @@ weight_c, weight_s, ) +from ._section6_durability import ( + c_min, + c_min_dur_carb, + c_min_dur_chlo, + c_nom, + delta_c_min_30, + delta_c_min_exc, + delta_c_min_p, + delta_dur_abr, + delta_dur_red_1, + delta_dur_red_2, + get_exposure_class_description, + get_exposure_classes, +) from ._section9_sls import ( As_min_y, Ec_eff, @@ -52,6 +66,18 @@ ) __all__ = [ + 'c_min', + 'delta_c_min_30', + 'delta_c_min_exc', + 'delta_c_min_p', + 'delta_dur_abr', + 'delta_dur_red_1', + 'delta_dur_red_2', + 'c_nom', + 'c_min_dur_carb', + 'c_min_dur_chlo', + 'get_exposure_class_description', + 'get_exposure_classes', 'A_phi_correction_exp', 'alpha_c_th', 'alpha_s_th', diff --git a/structuralcodes/codes/ec2_2023/_section6_durability.py b/structuralcodes/codes/ec2_2023/_section6_durability.py new file mode 100644 index 00000000..aab5d863 --- /dev/null +++ b/structuralcodes/codes/ec2_2023/_section6_durability.py @@ -0,0 +1,592 @@ +"""Functions from Section 6 of EN 1992-1-1:2023.""" + +import math +from typing import Dict, List, Literal, Optional + +from scipy.interpolate import interp1d + + +def get_exposure_classes( + env_level: Optional[int] = None, + env_name: Optional[ + Literal[ + 'none', + 'carbonation', + 'chlorides', + 'sea', + 'freeze', + 'chemical', + 'abrasion', + ] + ] = None, +) -> List[str]: + """Returns a list with valid exposure classes. + + EN1992-1-1:2023 Table (6.1). + + Args: + env_level (int, optional): Filters the result by only a determined env + level. + env_name (str, optional): The name of the environment agent. + + Returns: + (List[str]): List with valid exposure classes. + """ + if env_level is not None and env_level not in range(1, 8): + raise ValueError( + f'env_level should be between 1 and 7. Got {env_level}' + ) + + exposure_classes = { + 1: ['X0'], + 2: ['XC1', 'XC2', 'XC3', 'XC4'], + 3: ['XD1', 'XD2', 'XD3'], + 4: ['XS1', 'XS2', 'XS3'], + 5: ['XF1', 'XF2', 'XF3', 'XF4'], + 6: ['XA1', 'XA2', 'XA3'], + 7: ['XM1', 'XM2', 'XM3'], + } + + env_name_map = { + 'none': 1, + 'carbonation': 2, + 'chlorides': 3, + 'sea': 4, + 'freeze': 5, + 'chemical': 6, + 'abrasion': 7, + } + + # Determine the corresponding environment level if env_name is provided + if env_name: + env_level = env_name_map.get(env_name) + + if env_level: + return exposure_classes.get(env_level, []) + + # Return all exposure classes if no specific level or name is provided + return sum(exposure_classes.values(), []) + + +def get_exposure_class_description(exp_class: str) -> Dict[str, str]: + """Returns the exposure class description. + + EN1992-1-1:2023 Table (6.1). + + Args: + exp_class (str): Exposure class code. + + Returns: + (Dict[str,str]): A dictinary containing the environment, the + description and the examples. + + Raise: + ValueError: If exposure class does not exist. + """ + data = { + 'X0': { + 'main_description': 'No risk of corrosion or attack.', + 'environment': 'All exposures except where there is freeze/thaw, abrasion or chemical attack.', # noqa: E501 + 'examples': 'Plain concrete members without any reinforcement.', + }, + 'XC1': { + 'main_description': 'Corrosion of embedded metal induced by carbonation.', # noqa: E501 + 'environment': 'Dry.', + 'examples': 'Concrete inside buildings with low air humidity, where the corrosion rate will be insignificant.', # noqa: E501 + }, + 'XC2': { + 'main_description': 'Corrosion of embedded metal induced by carbonation.', # noqa: E501 + 'environment': 'Wet or permanent high humidity, rarely dry.', + 'examples': 'Concrete surfaces subject to long-term water contact or permanently submerged in water or permanently exposed to high humidity; many foundations; water containments (not external).', # noqa: E501 + }, + 'XC3': { + 'main_description': 'Corrosion of embedded metal induced by carbonation.', # noqa: E501 + 'environment': 'Moderate humidity.', + 'examples': 'Concrete inside buildings with moderate humidity and not permanent high humidity; External concrete sheltered from rain.', # noqa: E501 + }, + 'XC4': { + 'main_description': 'Corrosion of embedded metal induced by carbonation.', # noqa: E501 + 'environment': 'Cyclic wet and dry.', + 'examples': 'Concrete surfaces subject to cyclic water contact (e.g. external concrete not sheltered from rain as walls and facades).', # noqa: E501 + }, + 'XD1': { + 'main_description': 'Corrosion of embedded metal induced by chlorides, excluding sea water.', # noqa: E501 + 'environment': 'Moderate humidity.', + 'examples': 'Concrete surfaces exposed to airborne chlorides.', + }, + 'XD2': { + 'main_description': 'Corrosion of embedded metal induced by chlorides, excluding sea water.', # noqa: E501 + 'environment': 'Wet, rarely dry.', + 'examples': 'Swimming pools; Concrete components exposed to industrial waters containing chlorides.', # noqa: E501 + }, + 'XD3': { + 'main_description': 'Corrosion of embedded metal induced by chlorides, excluding sea water.', # noqa: E501 + 'environment': 'Cyclic wet and dry.', + 'examples': 'Parts of bridges exposed to water containing chlorides; Concrete roads, pavements and car park slabs in areas where de-icing agents are frequently used.', # noqa: E501 + }, + 'XS1': { + 'main_description': 'Corrosion of embedded metal induced by chlorides from sea water.', # noqa: E501 + 'environment': 'Exposed to airborne salt but not in direct contact with sea water.', # noqa: E501 + 'examples': 'Structures near to or on the coast.', + }, + 'XS2': { + 'main_description': 'Corrosion of embedded metal induced by chlorides from sea water.', # noqa: E501 + 'environment': 'Permanently submerged.', + 'examples': 'Parts of marine structures and structures in seawater.', # noqa: E501 + }, + 'XS3': { + 'main_description': 'Corrosion of embedded metal induced by chlorides from sea water.', # noqa: E501 + 'environment': 'Tidal, splash and spray zones.', + 'examples': 'Parts of marine structures and structures temporarily or permanently directly over sea water.', # noqa: E501 + }, + 'XF1': { + 'main_description': 'Freeze/Thaw Attack.', + 'environment': 'Moderate water saturation, without de-icing agent.', # noqa: E501 + 'examples': 'Vertical concrete surfaces exposed to rain and freezing.', # noqa: E501 + }, + 'XF2': { + 'main_description': 'Freeze/Thaw Attack.', + 'environment': 'Moderate water saturation, with de-icing agent.', + 'examples': 'Vertical concrete surfaces of road structures exposed to freezing and airborne de-icing agents.', # noqa: E501 + }, + 'XF3': { + 'main_description': 'Freeze/Thaw Attack.', + 'environment': 'High water saturation, without de-icing agents.', + 'examples': 'Horizontal concrete surfaces exposed to rain and freezing.', # noqa: E501 + }, + 'XF4': { + 'main_description': 'Freeze/Thaw Attack.', + 'environment': 'High water saturation with de-icing agents or sea water.', # noqa: E501 + 'examples': 'Road and bridge decks exposed to de-icing agents and freezing; concrete surfaces exposed to direct spray containing de-icing agents and freezing; splash zone of marine structures exposed to freezing.', # noqa: E501 + }, + 'XA1': { + 'main_description': 'Chemical attack.', + 'environment': 'Slightly aggressive chemical environment.', + 'examples': 'Natural soils and ground water according to Table 6.2.', # noqa: E501 + }, + 'XA2': { + 'main_description': 'Chemical attack.', + 'environment': 'Moderately aggressive chemical environment.', + 'examples': 'Natural soils and ground water according to Table 6.2.', # noqa: E501 + }, + 'XA3': { + 'main_description': 'Chemical attack.', + 'environment': 'Highly aggressive chemical environment.', + 'examples': 'Natural soils and ground water according to Table 6.2.', # noqa: E501 + }, + 'XM1': { + 'main_description': 'Mechanical attack of concrete by abrasion.', + 'environment': 'Moderate abrasion.', + 'examples': 'Members of industrial sites frequented by vehicles with pneumatic tyres.', # noqa: E501 + }, + 'XM2': { + 'main_description': 'Mechanical attack of concrete by abrasion.', + 'environment': 'Heavy abrasion.', + 'examples': 'Members of industrial sites frequented by forklifts with pneumatic or solid rubber tyres.', # noqa: E501 + }, + 'XM3': { + 'main_description': 'Mechanical attack of concrete by abrasion.', + 'environment': 'Extreme abrasion.', + 'examples': 'Members of industrial sites frequented by forklifts with elastomer or steel tyres or track vehicles.', # noqa: E501 + }, + } + + exp_class_upper = exp_class.upper() + if exp_class_upper not in data: + raise ValueError(f"Exposure class '{exp_class}' does not exist.") + + return data[exp_class_upper] + + +def c_nom(c_min: float, delta_c_dev: float) -> float: + """Calculate the nominal cover. + + EN1992-1-1:2023 Eq. (6.1). + + Args: + c_min (float): Minimum cover in mm. + delta_c_dev (float): Allowance in design for deviation in mm. + + Returns: + float: Nominal cover in mm. + + Raises: + ValueError: If any of the inputs are negative. + """ + if c_min < 0: + raise ValueError(f'cmin must not be negative. Got {c_min}') + if delta_c_dev < 0: + raise ValueError(f'delta_cdev must not be negative. Got {delta_c_dev}') + + return c_min + delta_c_dev + + +def c_min( + c_min_dur: float, + sum_c: float, + c_min_b: float, + additional_cover: float = 0, +) -> float: + """Calculate the minimum cover. + + EN1992-1-1:2023 Eq. (6.2). + + Args: + cmin_dur (float): Minimum cover required for environmental conditions + in mm. + sum_c (float): Sum of applicable reductions and additions in mm. + c_min_b (float): Minimum cover for bond requirement in mm. + additional_cover (float, optional): Additional cover for casting + against soil or other special requirements in mm. Default is 0. + + Returns: + float: Minimum cover in mm. + + Raises: + ValueError: If any of the inputs are negative (except sum_c). + """ + if c_min_dur < 0: + raise ValueError(f'cmin_dur must not be negative. Got {c_min_dur}') + if c_min_b < 0: + raise ValueError(f'cmin_b must not be negative. Got {c_min_b}') + if additional_cover < 0: + raise ValueError( + f'additional_cover must not be negative. Got {additional_cover}' + ) + + return max(c_min_dur + sum_c, c_min_b, 10) + additional_cover + + +def c_min_dur_carb( + exposure_class: Literal['XC1', 'XC2', 'XC3', 'XC4'], + design_service_life: int, + exposure_resistance_class: Literal[ + 'XRC 0.5', + 'XRC 1', + 'XRC 2', + 'XRC 3', + 'XRC 4', + 'XRC 5', + 'XRC 6', + 'XRC 7', + ], + delta_c_dur_gamma: float = 0, +) -> float: + """Calculate the minimum concrete cover for durability against carbonation. + + EN1992-1-1:2023 Table. (6.3). + + Args: + exposure_class (Literal): Exposure class for carbonation (XC1, XC2, + XC3, XC4). + design_service_life (Literal): Design service life in years (50 or + 100). + exposure_resistance_class (Literal): Exposure resistance class (XRC + 0.5, XRC 1, XRC 2, XRC 3, XRC 4, XRC 5, XRC 6, XRC 7). + delta_c_dur_gamma (float): Increment in the concrete cover in mm + considering special requirements. + + Returns: + float: Minimum concrete cover in mm. + + Raise: + ValueError: If delta_c_dur_gamma is less than zero. + """ + if delta_c_dur_gamma < 0: + raise ValueError( + f'delta_c_dur_gamma must be positive. Got {delta_c_dur_gamma}' + ) + + # Clamp the design service life value + design_service_life = max(50, min(design_service_life, 100)) + + # Minimum covers from Table 6.3 + covers = { + 'XRC 0.5': { + 'XC1': [10, 10], + 'XC2': [10, 10], + 'XC3': [10, 10], + 'XC4': [10, 10], + }, + 'XRC 1': { + 'XC1': [10, 10], + 'XC2': [10, 10], + 'XC3': [10, 15], + 'XC4': [10, 15], + }, + 'XRC 2': { + 'XC1': [10, 15], + 'XC2': [10, 15], + 'XC3': [15, 25], + 'XC4': [15, 25], + }, + 'XRC 3': { + 'XC1': [10, 15], + 'XC2': [15, 20], + 'XC3': [20, 30], + 'XC4': [20, 30], + }, + 'XRC 4': { + 'XC1': [10, 20], + 'XC2': [15, 25], + 'XC3': [25, 35], + 'XC4': [25, 40], + }, + 'XRC 5': { + 'XC1': [15, 25], + 'XC2': [20, 30], + 'XC3': [25, 45], + 'XC4': [30, 45], + }, + 'XRC 6': { + 'XC1': [15, 25], + 'XC2': [25, 35], + 'XC3': [35, 55], + 'XC4': [40, 55], + }, + 'XRC 7': { + 'XC1': [15, 30], + 'XC2': [25, 40], + 'XC3': [40, 60], + 'XC4': [45, 60], + }, + } + + # Determine base cover + base_cover_v = covers[exposure_resistance_class][exposure_class] + base_cover = interp1d([50, 100], base_cover_v)(design_service_life) + + # Calculate minimum cover + c_min_dur = base_cover + delta_c_dur_gamma + return max(c_min_dur, 0) + + +def c_min_dur_chlo( + exposure_class: Literal['XS1', 'XS2', 'XS3', 'XD1', 'XD2', 'XD3'], + design_service_life: int, + exposure_resistance_class: Literal[ + 'XRDS 0.5', + 'XRDS 1', + 'XRDS 1.5', + 'XRDS 2', + 'XRDS 3', + 'XRDS 4', + 'XRDS 5', + 'XRDS 6', + 'XRDS 8', + 'XRDS 10', + ], + delta_c_dur_gamma: float = 0.0, +) -> float: + """Calculate the minimum concrete cover for durability against chlorides. + + EN1992-1-1:2023 Table. (6.4). + + Args: + exposure_class (Literal): Exposure class for chlorides (XS1, XS2, XS3, + XD1, XD2, XD3). + design_service_life (Literal): Design service life in years (50 or + 100). + exposure_resistance_class (Literal): Exposure resistance class (XRDS + 0.5, XRDS 1, XRDS 1.5, XRDS 2, XRDS 3, XRDS 4, XRDS 5, XRDS 6, XRDS + 8, XRDS 10). + delta_c_dur_gamma (float): Increment in the concrete cover in mm + considering special requirements. + + Returns: + float: Minimum concrete cover in mm. + + Raises: + ValueError: If input values are invalid or the combination of exposure + class, XDRS class and design service life is incompatible. + """ + if delta_c_dur_gamma < 0: + raise ValueError( + f'delta_c_dur_gamma must be positive. Got {delta_c_dur_gamma}' + ) + + # Validate input + design_service_life = max(50, min(design_service_life, 100)) + + # Minimum covers from Table 6.4 + covers = { + 'XRDS 0.5': { + 'XS1': [20, 20], + 'XS2': [20, 30], + 'XS3': [30, 40], + 'XD1': [20, 20], + 'XD2': [20, 30], + 'XD3': [30, 40], + }, + 'XRDS 1': { + 'XS1': [20, 25], + 'XS2': [25, 35], + 'XS3': [35, 45], + 'XD1': [20, 25], + 'XD2': [25, 35], + 'XD3': [35, 45], + }, + 'XRDS 1.5': { + 'XS1': [25, 30], + 'XS2': [30, 40], + 'XS3': [40, 50], + 'XD1': [25, 30], + 'XD2': [30, 40], + 'XD3': [40, 50], + }, + 'XRDS 2': { + 'XS1': [25, 30], + 'XS2': [35, 45], + 'XS3': [45, 55], + 'XD1': [25, 30], + 'XD2': [35, 45], + 'XD3': [45, 55], + }, + 'XRDS 3': { + 'XS1': [30, 35], + 'XS2': [40, 50], + 'XS3': [55, 65], + 'XD1': [30, 35], + 'XD2': [40, 50], + 'XD3': [55, 65], + }, + 'XRDS 4': { + 'XS1': [30, 40], + 'XS2': [50, 60], + 'XS3': [60, 80], + 'XD1': [30, 40], + 'XD2': [50, 60], + 'XD3': [60, 80], + }, + 'XRDS 5': { + 'XS1': [35, 45], + 'XS2': [60, 70], + 'XS3': [70, float('nan')], + 'XD1': [35, 45], + 'XD2': [60, 70], + 'XD3': [70, float('nan')], + }, + 'XRDS 6': { + 'XS1': [40, 50], + 'XS2': [65, 80], + 'XS3': [float('nan'), float('nan')], + 'XD1': [40, 50], + 'XD2': [65, 80], + 'XD3': [float('nan'), float('nan')], + }, + 'XRDS 8': { + 'XS1': [45, 55], + 'XS2': [75, float('nan')], + 'XS3': [float('nan'), float('nan')], + 'XD1': [45, 55], + 'XD2': [75, float('nan')], + 'XD3': [float('nan'), float('nan')], + }, + 'XRDS 10': { + 'XS1': [50, 65], + 'XS2': [80, float('nan')], + 'XS3': [float('nan'), float('nan')], + 'XD1': [50, 65], + 'XD2': [80, float('nan')], + 'XD3': [float('nan'), float('nan')], + }, + } + + # Determine base cover + base_cover_v = covers[exposure_resistance_class][exposure_class] + + if not base_cover_v: # Check for unavailable cover + raise ValueError( + 'No cover available for the selected exposure' + + ' resistance class and exposure class combination.' + ) + base_cover = interp1d( + [50, 100], + base_cover_v, + )(design_service_life) + if math.isnan(base_cover): + raise ValueError('Not a valid cover value found.') + + # Calculate minimum cover + c_min_dur = base_cover + delta_c_dur_gamma + return max(c_min_dur, 0) + + +def delta_c_min_30() -> float: + """Default reduction of minimum cover for structures with design life of 30 + years or less unless specified in National Annex. + + EN1992-1-1:2023 (6.5.2.2(2)). + + Returns: + float: Reduction distance in mm. + """ + return -5.0 + + +def delta_c_min_exc() -> float: + """Default reduction of minimum cover for superior compaction or improved + curing unless specified in National Annex. + + EN1992-1-1:2023 (6.5.2.2(3)). + + Returns: + float: Reduction distance in mm. + """ + return -5.0 + + +def delta_c_min_p() -> float: + """Default addition of minimum cover for prestressing tendons unless + specified in National Annex. + + EN1992-1-1:2023 Table. (6.5.2.2(4)). + + Returns: + float: Added distance in mm. + """ + return 10.0 + + +def delta_dur_red_1() -> float: + """Default reduction of minimum cover for use of special measures of + reinforcing steel unless specified in National Annex. + + EN1992-1-1:2023 (6.5.2.2(5)). + + Returns: + float: Reduction distance in mm. + """ + return -10.0 + + +def delta_dur_red_2() -> float: + """Default reduction of minimum cover for use of special measures of + reinforcing steel unless specified in National Annex. + + EN1992-1-1:2023 (6.5.2.2(9)). + + Returns: + float: Reduction distance in mm. + """ + return -0.0 + + +def delta_dur_abr(xm_class: Literal['XM1', 'XM2', 'XM3']) -> float: + """Default addition of minimum cover for abraion unless specified in + National Annex. + + EN1992-1-1:2023 (6.5.2.2(6)). + + Args: + xm_class (str): The XM class. + + Returns: + float: Added distance in mm. + """ + data = { + 'XM1': 5.0, + 'XM2': 10.0, + 'XM3': 15.0, + } + return data[xm_class.upper()] diff --git a/tests/test_ec2_2023/test_ec2_2023_section6_durability.py b/tests/test_ec2_2023/test_ec2_2023_section6_durability.py new file mode 100644 index 00000000..2c079e87 --- /dev/null +++ b/tests/test_ec2_2023/test_ec2_2023_section6_durability.py @@ -0,0 +1,245 @@ +"""Tests of functions from Section 6 of EN 1992-1-1:2023.""" + +import pytest + +from structuralcodes.codes.ec2_2023 import _section6_durability + + +@pytest.mark.parametrize( + 'env_level, env_name, expected_classes', + [ + ( + None, + None, + [ + 'X0', + 'XC1', + 'XC2', + 'XC3', + 'XC4', + 'XD1', + 'XD2', + 'XD3', + 'XS1', + 'XS2', + 'XS3', + 'XF1', + 'XF2', + 'XF3', + 'XF4', + 'XA1', + 'XA2', + 'XA3', + 'XM1', + 'XM2', + 'XM3', + ], + ), + (1, None, ['X0']), + (2, None, ['XC1', 'XC2', 'XC3', 'XC4']), + (3, None, ['XD1', 'XD2', 'XD3']), + (4, None, ['XS1', 'XS2', 'XS3']), + (5, None, ['XF1', 'XF2', 'XF3', 'XF4']), + (6, None, ['XA1', 'XA2', 'XA3']), + (7, None, ['XM1', 'XM2', 'XM3']), + (None, 'none', ['X0']), + (None, 'carbonation', ['XC1', 'XC2', 'XC3', 'XC4']), + (None, 'chlorides', ['XD1', 'XD2', 'XD3']), + (None, 'sea', ['XS1', 'XS2', 'XS3']), + (None, 'freeze', ['XF1', 'XF2', 'XF3', 'XF4']), + (None, 'chemical', ['XA1', 'XA2', 'XA3']), + (None, 'abrasion', ['XM1', 'XM2', 'XM3']), + ], +) +def test_get_exposure_classes(env_level, env_name, expected_classes): + """Test if the function returns the correct list of exposure classes.""" + assert ( + _section6_durability.get_exposure_classes(env_level, env_name) + == expected_classes + ) + + +@pytest.mark.parametrize( + 'exp_class, expected_main_description', + [ + ('X0', 'No risk of corrosion or attack.'), + ('XC1', 'Corrosion of embedded metal induced by carbonation.'), + ( + 'XD3', + 'Corrosion of embedded metal induced by chlorides, ' + + 'excluding sea water.', + ), + ], +) +def test_get_exposure_class_description_valid( + exp_class, expected_main_description +): + """Test valid exposure classes.""" + description = _section6_durability.get_exposure_class_description( + exp_class + ) + assert description['main_description'] == expected_main_description + + +def test_get_exporuse_class_description_invalid(): + """Test invalid exposure class.""" + with pytest.raises(ValueError): + _section6_durability.get_exposure_class_description('INVALID_CLASS') + + # Test another invalid input + with pytest.raises(ValueError): + _section6_durability.get_exposure_class_description('UNKNOWN') + + +def test_get_exporuse_class_description_case_insensitive(): + """Test if the function handles case-insensitive class codes.""" + assert ( + 'Corrosion of embedded metal induced by carbonation.' + in _section6_durability.get_exposure_class_description('xc1')[ + 'main_description' + ] + ) + assert ( + 'Corrosion of embedded metal induced by chlorides' + in _section6_durability.get_exposure_class_description('xd3')[ + 'main_description' + ] + ) + + +def test_cnom(): + """Test the nominal cover calculation.""" + assert _section6_durability.c_nom(25, 5) == 30 + assert _section6_durability.c_nom(20, 10) == 30 + assert _section6_durability.c_nom(0, 0) == 0 + + with pytest.raises(ValueError): + _section6_durability.c_nom(-1, 5) + + with pytest.raises(ValueError): + _section6_durability.c_nom(25, -5) + + +def test_cmin(): + """Test the minimum cover calculation.""" + assert _section6_durability.c_min(25, 5, 20) == 30 + assert _section6_durability.c_min(20, 10, 20) == 30 + assert _section6_durability.c_min(10, 0, 15) == 15 + assert _section6_durability.c_min(5, 0, 5, 5) == 15 + + with pytest.raises(ValueError): + _section6_durability.c_min(-1, 5, 20) + + with pytest.raises(ValueError): + _section6_durability.c_min(25, 5, -20) + + with pytest.raises(ValueError): + _section6_durability.c_min(25, 5, 20, -5) + + +@pytest.mark.parametrize( + 'exposure_class, design_service_life, exposure_resistance_class, expected', + [ + ('XC1', 50, 'XRC 0.5', 10), + ('XC3', 100, 'XRC 3', 30), + ('XC3', 50, 'XRC 2', 15), + ('XC4', 100, 'XRC 4', 40), + ('XC2', 75, 'XRC 7', 32.5), + ], +) +def test_minimum_cover_carbonation( + exposure_class, design_service_life, exposure_resistance_class, expected +): + """Test minimum concrete cover for carbonation.""" + assert ( + _section6_durability.c_min_dur_carb( + exposure_class, design_service_life, exposure_resistance_class + ) + == expected + ) + + +@pytest.mark.parametrize( + 'exposure_class, design_service_life, exposure_resistance_class, expected', + [ + ('XS1', 50, 'XRDS 0.5', 20), + ('XS3', 100, 'XRDS 3', 65), + ('XD1', 50, 'XRDS 2', 25), + ('XD3', 100, 'XRDS 1', 45), + ('XD3', 75, 'XRDS 1', 40), + ('XD2', 50, 'XRDS 10', 80), + ], +) +def test_minimum_cover_chlorides( + exposure_class, design_service_life, exposure_resistance_class, expected +): + """Test minimum concrete cover for chlorides.""" + assert ( + _section6_durability.c_min_dur_chlo( + exposure_class, design_service_life, exposure_resistance_class + ) + == expected + ) + + +@pytest.mark.parametrize( + 'exposure_class, design_service_life, exposure_resistance_class', + [ + ('XS3', 100, 'XRDS 10'), + ('XD3', 100, 'XRDS 8'), + ], +) +def test_minimum_cover_chlorides_with_invalid_values( + exposure_class, design_service_life, exposure_resistance_class +): + """Test minimum concrete cover for chlorides.""" + with pytest.raises(ValueError): + _section6_durability.c_min_dur_chlo( + exposure_class, design_service_life, exposure_resistance_class + ) + + +def test_delta_c_min_30(): + """Test reduction of minimum cover for design life of 30 years.""" + assert _section6_durability.delta_c_min_30() == -5.0 + + +def test_delta_c_min_exc(): + """Test reduction of minimum cover for superior + compaction or improved curing. + """ + assert _section6_durability.delta_c_min_exc() == -5.0 + + +def test_delta_c_min_p(): + """Test addition of minimum cover for prestressing tendons.""" + assert _section6_durability.delta_c_min_p() == 10.0 + + +def test_delta_dur_red_1(): + """Test addition of minimum cover for special + reinforcing steel measures. + """ + assert _section6_durability.delta_dur_red_1() == -10.0 + + +def test_delta_dur_red_2(): + """Test reduction of minimum cover for + special reinforcing steel measures. + """ + assert _section6_durability.delta_dur_red_2() == 0.0 + + +@pytest.mark.parametrize( + 'xm_class, expected', + [ + ('XM1', 5.0), + ('XM2', 10.0), + ('XM3', 15.0), + ], +) +def test_delta_dur_abr(xm_class, expected): + """Test addition of minimum cover for abrasion based on XM class.""" + assert ( + _section6_durability.delta_dur_abr(xm_class) == expected + ), f'Expected addition of {expected} mm for XM class {xm_class}.'