diff --git a/docs/conf.py b/docs/conf.py index 519e3b67..e8cad954 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,7 +67,7 @@ - """, + """, # noqa: E501 'class': '', }, ], diff --git a/structuralcodes/codes/ec2_2023/__init__.py b/structuralcodes/codes/ec2_2023/__init__.py index c8662148..3905b97e 100644 --- a/structuralcodes/codes/ec2_2023/__init__.py +++ b/structuralcodes/codes/ec2_2023/__init__.py @@ -50,8 +50,136 @@ wk_cal, wk_cal2, ) +from ._section_8_2_shear import ( + Asf_flange, + Fcd, + Ftd, + Ftd_max, + Nvd, + NVds_inclined, + a_cs, + a_v, + as_min, + bw_nom, + check_tau_Ed_flange_verification, + cot_theta_inclined, + cot_theta_max_shear_constant_nu, + cot_theta_max_shear_variable_nu, + cot_theta_min, + cot_theta_simultaneous, + cv1, + cv2, + d_dg, + d_eff, + d_eff_angle, + d_eff_p, + delta_MEd, + eps_x_flang, + epsilon_x, + epsilon_xc_comp, + epsilon_xc_tens, + epsilon_xt, + k1, + k_duct, + k_vp, + kdowel, + kv, + mu_v, + nu, + rho_l, + rho_l_p, + rho_l_planar, + rho_w, + sigma_cd, + sigma_cd_flange, + sigma_cd_inclined, + sigma_swd, + sigma_swd_inclined, + tau_Ed, + tau_Ed_flange, + tau_Ed_planar, + tau_Edi, + tau_Edi_composite, + tau_Rd, + tau_rd, + tau_Rd_inclined, + tau_Rd_sy, + tau_Rd_sy_inclined, + tau_Rdc, + tau_Rdc_0, + tau_Rdc_comp, + tau_Rdc_max, + tau_rdc_min, + tau_Rdi, + tau_Rdi_no_yielding, + tau_Rdm, + v_Ed, +) __all__ = [ + 'as_min', + 'check_tau_Ed_flange_verification', + 'd_dg', + 'NVds_inclined', + 'cv1', + 'kdowel', + 'tau_Rdi_no_yielding', + 'cv2', + 'kv', + 'mu_v', + 'tau_Edi', + 'tau_Rdi', + 'tau_Edi_composite', + 'eps_x_flang', + 'tau_Ed_flange', + 'Asf_flange', + 'sigma_cd_flange', + 'tau_Rdm', + 'sigma_cd_s', + 'sigma_swd_inclined', + 'tau_Rd_sy_inclined', + 'tau_Rd_inclined', + 'delta_MEd', + 'tau_rd', + 'sigma_swd', + 'bw_nom', + 'Fcd', + 'Nvd', + 'Ftd', + 'Ftd_max', + 'd_eff_p', + 'sigma_cd', + 'sigma_cd_inclined', + 'rho_l_planar', + 'rho_l_p', + 'epsilon_x', + 'epsilon_xc_comp', + 'epsilon_xc_tens', + 'epsilon_xt', + 'nu', + 'cot_theta_inclined', + 'cot_theta_max_shear_constant_nu', + 'cot_theta_max_shear_variable_nu', + 'cot_theta_min', + 'rho_w', + 'cot_theta_simultaneous', + 'tau_Rd', + 'tau_Rd_sy', + 'a_cs', + 'a_v', + 'k1', + 'rho_l', + 'tau_Rdc', + 'tau_Rdc_0', + 'tau_Rdc_comp', + 'tau_Rdc_max', + 'k_vp', + 'tau_Ed', + 'd_eff', + 'd_eff_angle', + 'v_Ed', + 'tau_Ed_planar', + 'tau_rdc_min', 'A_phi_correction_exp', 'alpha_c_th', 'alpha_s_th', @@ -97,6 +225,7 @@ 'srm_cal', 'wk_cal', 'wk_cal2', + 'k_duct', ] __title__: str = 'EUROCODE 2 1992-1-1:2023' diff --git a/structuralcodes/codes/ec2_2023/_section_8_2_shear.py b/structuralcodes/codes/ec2_2023/_section_8_2_shear.py new file mode 100644 index 00000000..f7c2990a --- /dev/null +++ b/structuralcodes/codes/ec2_2023/_section_8_2_shear.py @@ -0,0 +1,2521 @@ +"""Functions from Section 8.2 of EN 1992-1-1:2023.""" + +import math +from typing import Iterable, Literal + + +def tau_Ed(VEd: float, bw: float, d: float) -> float: + """Calculate the average shear stress over the cross-section for linear + members. + + EN1992-1-1:2923 Eq. (8.18). + + Args: + VEd (float): Design shear force at the control section in linear + members in kN. + bw (float): Width of the cross-section of linear members in mm. + d (float): Effective depth of the cross-section in mm. + + Returns: + float: Average shear stress over the cross-section for linear members + in Mpa. + """ + if bw < 0: + raise ValueError(f'bw must not be negative. Got {bw}') + if d < 0: + raise ValueError(f'd must not be negative. Got {d}') + + z = 0.9 * d + return VEd * 1000 / (bw * z) + + +def tau_Ed_planar(vEd: float, d: float) -> float: + """Calculate the average shear stress over the cross-section for planar + members. + + EN1992-1-1:2923 Eq. (8.19). + + Args: + vEd (float): Design shear force per unit width in planar members in + kN/m. + d (float): Effective depth of the cross-section in mm. + + Returns: + float: Average shear stress over the cross-section for planar members + in MPa. + """ + if d < 0: + raise ValueError(f'd must not be negative. Got {d}') + + z = 0.9 * d + return vEd / z + + +def d_dg(f_ck: float, d_lower: float) -> float: + """Calculate the size parameter describing the failure zone roughness. + + EN1992-1-1:2023 Note 2 for Eq. (8.20). + + Args: + f_ck (float): Characteristic compressive strength of concrete in MPa + (must be positive). + d_lower (float): Smallest value of the upper sieve size D in an + aggregate for the coarsest fraction of aggregates in mm (must be + positive). + + Returns: + float: Size parameter ddg in mm. + + Raises: + ValueError: If any input value is non-positive. + """ + if f_ck < 0: + raise ValueError(f'f_ck must not be negative. Got {f_ck}') + if d_lower < 0: + raise ValueError(f'd_lower must not be negative. Got {d_lower}') + + if f_ck <= 60: + return min(16 + d_lower, 40) + + return min(16 + d_lower * (60 / f_ck) ** 2, 40) + + +def tau_rdc_min( + gamma_v: float, + f_ck: float, + f_yd: float, + d: float, + d_dg: float, +) -> float: + """Calculate the minimum shear stress resistance. + + EN1992-1-1:2023 Eq. (8.20). + + Args: + gamma_v (float): Partial factor for shear design. + f_ck (float): Characteristic compressive strength of concrete in MPa + (must be positive). + f_yd (float): Design value of the yield strength in MPa (must be + positive). + d (float): Effective depth of the flexural reinforcement in mm (must be + positive). + d_dg (float): Size parameter describing the failure zone roughness in + mm (must be positive). + + Returns: + float: Minimum shear stress resistance in MPa. + + Raises: + ValueError: If any input value is non-positive. + """ + if gamma_v < 0: + raise ValueError(f'gamma_v must not be negative. Got {gamma_v}') + if f_ck < 0: + raise ValueError(f'f_ck must not be negative. Got {f_ck}') + if f_yd < 0: + raise ValueError(f'f_yd must not be negative. Got {f_yd}') + if d < 0: + raise ValueError(f'd must not be negative. Got {d}') + if d_dg < 0: + raise ValueError(f'd_dg must not be negative. Got {d_dg}') + + return 11 / gamma_v * math.sqrt(f_ck / f_yd * d_dg / d) + + +def v_Ed(vEd_x: float, vEd_y: float) -> float: + """Calculate the design shear force per unit width (vEd). + + EN1992-1-1:2023 Eq. (8.21). + + Args: + vEd_x (float): Shear force in x-direction in kN/m. + vEd_y (float): Shear force in y-direction in kN/m. + + Returns: + float: Design shear force per unit width (vEd) kN/m. + """ + return math.sqrt(vEd_x**2 + vEd_y**2) + + +def d_eff(dx: float, dy: float, vEd_x: float, vEd_y: float) -> float: + """Calculate the effective depth (d) based on the ratio of shear forces. + + EN1992-1-1:2023 Eq. (8.22), (8.23), (8.24). + + Args: + dx (float): Effective depth in x-direction in mm (must be positive). + dy (float): Effective depth in y-direction in mm (must be positive). + vEd_x (float): Shear force in x-direction in kN/m. + vEd_y (float): Shear force in y-direction in kN/m. + + Returns: + float: Effective depth (d) in mm. + + Raises: + ValueError: If dx or dy is negative. + """ + if dx < 0: + raise ValueError(f'dx must not be negative. Got {dx}') + if dy < 0: + raise ValueError(f'dy must not be negative. Got {dy}') + + vEd_x = abs(vEd_x) + vEd_y = abs(vEd_y) + + ratio = vEd_y / vEd_x if vEd_x != 0 else float('inf') + + if ratio <= 0.5: + return dx + if 0.5 < ratio < 2: + return 0.5 * (dx + dy) + return dy + + +def d_eff_angle(dx: float, dy: float, vEd_x: float, vEd_y: float) -> float: + """Calculate the effective depth (d) based on the angle alpha_v. + + EN1992-1-1:2023 Eq. (8.25), (8.26). + + Args: + dx (float): Effective depth in x-direction in mm. + dy (float): Effective depth in y-direction in mm. + vEd_x (float): Shear force in x-direction in kN/m. + vEd_y (float): Shear force in y-direction kN/m. + + Returns: + float: Effective depth (d) mm. + + Raises: + ValueError: If dx or dy is negative. + """ + if dx < 0: + raise ValueError(f'dx must not be negative. Got {dx}') + if dy < 0: + raise ValueError(f'dy must not be negative. Got {dy}') + + vEd_x = abs(vEd_x) + vEd_y = abs(vEd_y) + + if vEd_x == 0: + # When vEd_x = 0, alpha_v = 90 degrees, return dy + return dy + + alpha_v = math.atan(vEd_y / vEd_x) + + return dx * math.cos(alpha_v) ** 2 + dy * math.sin(alpha_v) ** 2 + + +def tau_Rdc( + gamma_v: float, + rho_l: float, + f_ck: float, + d: float, + d_dg: float, + tau_rdc_min: float, +) -> float: + """Calculate the design value of the shear stress resistance. + + EN1992-1-1:2023 Eq. (8.27). + + Args: + gamma_v (float): Partial factor for shear (unitless). + rho_l (float): Reinforcement ratio (unitless). + f_ck (float): Characteristic compressive strength of concrete in MPa. + d (float): Effective depth in mm. + d_dg (float): Size parameter describing the failure zone roughness in + mm. + tau_rdc_min (float): Minimum resistance neede in MPa. + + Returns: + float: The design value of the shear stress resistance MPa. + + Raises: + ValueError: If any input values are negative + or if gamma_v or d are non-positive. + """ + if gamma_v <= 0 or rho_l < 0 or f_ck < 0 or d <= 0 or d_dg < 0: + raise ValueError( + 'gamma_v and d must be positive other values must be non-negative.' + + f'Got gamma_v={gamma_v}, rho_l={rho_l}, ' + + f' f_ck={f_ck}, d={d}, d_dg={d_dg}' + ) + + return max( + 0.66 / gamma_v * (100 * rho_l * f_ck * d_dg / d) ** (1 / 3), + abs(tau_rdc_min), + ) + + +def rho_l(A_sl: float, b_w: float, d: float) -> float: + """Calculate the reinforcement ratio. + + EN1992-1-1:2023 Eq. (8.28). + + Args: + A_sl (float): Effective area of tensile reinforcement in mm2. + b_w (float): Width of the cross-section in mm. + d (float): Effective depth in mm. + + Returns: + float: The reinforcement ratio (unitless). + + Raises: + ValueError: If any of the input values are negative. + """ + if A_sl <= 0 or b_w <= 0 or d <= 0: + raise ValueError( + 'All input values must be positive.' + + f'Got A_sl={A_sl}, b_w={b_w}, d={d}' + ) + + return A_sl / (b_w * d) + + +def a_v(a_cs: float, d: float) -> float: + """Calculate the mechanical shear span. + + EN1992-1-1:2023 Eq. (8.29). + + Args: + a_cs (float): Effective shear span in mm. + d (float): Effective depth in mm. + + Returns: + float: The mechanical shear span av in mm. + + Raises: + ValueError: If any of the input values are non-positive. + """ + if a_cs <= 0 or d <= 0: + raise ValueError( + 'All input values must be positive.' + f'Got a_cs={a_cs}, d={d}' + ) + return math.sqrt(a_cs / 4 * d) + + +def a_cs(M_Ed: float, V_Ed: float, d: float) -> float: + """Calculate the effective shear span. + + EN1992-1-1:2023 Eq. (8.30). + + Args: + M_Ed (float): Bending moment kN·m. + V_Ed (float): Shear force kN. + d (float): Effective depth mm. + + Returns: + float: The effective shear span in mm. + + Raises: + ValueError: If any of the input values are negative. + """ + if d <= 0: + raise ValueError('All input values must be positive.' + f'Got d={d}') + + return max(abs(M_Ed * 1000 / V_Ed), d) + + +def k_vp(N_Ed: float, V_Ed: float, d: float, a_cs: float) -> float: + """Calculate the coefficient k_vp. + + EN1992-1-1:2023 Eq. (8.31). + + Args: + N_Ed (float): Axial force in kN + (compression negative, tension positive). + V_Ed (float): Shear force in kN. + d (float): Effective depth in mm. + a_cs (float): Effective shear span in mm. + + Returns: + float: The coefficient k_vp (unitless). + + Raises: + ValueError: If d or a_cs are non-positive. + + """ + if d <= 0 or a_cs <= 0: + raise ValueError( + 'd and a_cs must be positive. ' + f'Got d={d}, a_cs={a_cs}' + ) + + k_vp = 1 + (N_Ed / abs(V_Ed)) * (d / (3 * a_cs)) + return max(k_vp, 0.1) + + +def tau_Rdc_0( + gamma_v: float, rho_l: float, f_ck: float, d: float, d_dg: float +) -> float: + """Calculate the design value of the shear stress resistance without axial + force effects. + + EN1992-1-1:2023 Eq. (8.33). + + Args: + gamma_v (float): Partial factor for shear (unitless). + rho_l (float): Reinforcement ratio (unitless). + f_ck (float): Characteristic compressive strength of concrete in MPa. + d (float): Effective depth in mm. + d_dg (float): Size parameter describing the failure zone roughness in + mm. + + Returns: + float: The design value of the shear stress resistance in MPa. + + Raises: + ValueError: If any of the input values are negative. + """ + if gamma_v <= 0 or rho_l <= 0 or f_ck <= 0 or d <= 0 or d_dg <= 0: + raise ValueError( + 'All input values must be positive. ' + + f'Got gamma_v={gamma_v}, rho_l={rho_l}, ' + + f'f_ck={f_ck}, d={d}, d_dg={d_dg}' + ) + + return 0.66 / gamma_v * (100 * rho_l * f_ck * d_dg / d) ** (1 / 3) + + +def tau_Rdc_comp( + tau_Rdc_0: float, + k1: float, + sigma_cp: float, + tau_Rdc_max: float, + tau_rdc_min: float, +) -> float: + """Calculate the design value of the shear stress resistance considering + compressive normal forces. + + EN1992-1-1:2023 Eq. (8.32). + + Args: + tau_Rdc_0 (float): Design value of the shear stress resistance without + axial force effects in MPa. + k1 (float): Factor considering the effect of compressive normal forces + (unitless). + sigma_cp (float): Compressive stress due to axial force in MPa. + tau_Rdc_max (float): Maximum design value of the shear stress + resistance in MPa. + tau_rdc_min (float): Minimum design value of the shear stress + resistance in MPa. + + Returns: + float: The design value of the shear stress resistance in MPa. + + Raises: + ValueError: If any of the input values are negative. + """ + if ( + tau_Rdc_0 <= 0 + or k1 <= 0 + or sigma_cp < 0 + or tau_Rdc_max <= 0 + or tau_rdc_min <= 0 + ): + raise ValueError( + 'All input values must be positive. ' + + f'Got tau_Rdc_0={tau_Rdc_0}, k1={k1}, ' + + f'sigma_cp={sigma_cp}, tau_Rdc_max={tau_Rdc_max}, ' + + f'tau_rdc_min={tau_rdc_min}' + ) + + tau_Rdc = tau_Rdc_0 - k1 * sigma_cp + return max(min(tau_Rdc, tau_Rdc_max), tau_rdc_min) + + +def k1( + a_cs_0: float, + e_p: float, + A_c: float, + b_w: float, + z: float, + d: float, +) -> float: + """Calculate the factor k1 considering the effect of compressive normal + forces. + + EN1992-1-1:2023 Eq. (8.34). + + Args: + a_cs_0 (float): Effective shear span without considering prestressing + effects in mm. + e_p (float): Eccentricity of the prestressing force or external load in + mm. + A_c (float): Area of concrete cross-section in mm2. + b_w (float): Width of the cross-section in mm. + z (float): Lever arm in mm. + d (float): Effective depth in mm. + + Returns: + float: The factor k1 (unitless). + + Raises: + ValueError: If any of the input values are negative. + """ + if a_cs_0 <= 0 or e_p < 0 or A_c <= 0 or b_w <= 0 or z <= 0 or d <= 0: + raise ValueError( + 'All input values must be positive.' + + f' Got a_cs_0={a_cs_0}, e_p={e_p}, A_c={A_c}, ' + + f'b_w={b_w}, z={z}, d={d}' + ) + + k1 = 0.5 / a_cs_0 / (e_p + d / 3) * (A_c / (b_w * z)) + return min(k1, A_c * 0.18 / (b_w * z)) + + +def tau_Rdc_max(tau_Rdc_0: float, a_cs_0: float, d: float) -> float: + """Calculate the maximum design value of the shear stress resistance. + + EN1992-1-1:2023 Eq. (8.35). + + Args: + tau_Rdc_0 (float): Design value of the shear stress resistance without + axial force effects in MPa. + a_cs_0 (float): Effective shear span without considering prestressing + effects in mm. + d (float): Effective depth in mm. + + Returns: + float: The maximum design value of the shear stress resistance in MPa. + + Raises: + ValueError: If any of the input values are negative. + """ + if tau_Rdc_0 <= 0 or a_cs_0 <= 0 or d <= 0: + raise ValueError( + 'All input values must be positive. ' + + f'Got tau_Rdc_0={tau_Rdc_0}, a_cs_0={a_cs_0}, d={d}' + ) + + tau_Rdc_max = 2.15 * tau_Rdc_0 * (a_cs_0 / d) ** (1 / 6) + return min(tau_Rdc_max, 2.7 * tau_Rdc_0) + + +def d_eff_p(ds: float, As: float, dp: float, Ap: float) -> float: + """Calculate the effective depth for prestressed members with bonded + tendons. + + EN1992-1-1:2023 Eq. (8.36). + + Args: + ds (float): Depth of the tension reinforcement in mm. + As (float): Area of the tension reinforcement in mm2. + dp (float): Depth of the prestressed reinforcement in mm. + Ap (float): Area of the prestressed reinforcement in mm2. + + Returns: + float: Effective depth in mm. + + Raises: + ValueError: If any of the input values are negative or if + ds*As + dp*Ap equals zero (division by zero). + """ + if ds < 0: + raise ValueError(f'ds must not be negative. Got {ds}') + if As < 0: + raise ValueError(f'As must not be negative. Got {As}') + if dp < 0: + raise ValueError(f'dp must not be negative. Got {dp}') + if Ap < 0: + raise ValueError(f'Ap must not be negative. Got {Ap}') + + denominator = ds * As + dp * Ap + if denominator == 0: + raise ValueError( + 'Division by zero: ds*As + dp*Ap cannot be zero. ' + + f'Got ds={ds}, As={As}, dp={dp}, Ap={Ap}' + ) + + return (ds**2 * As + dp**2 * Ap) / denominator + + +def rho_l_p( + ds: float, As: float, dp: float, Ap: float, bw: float, d: float +) -> float: + """Calculate the reinforcement ratio for prestressed members with bonded + tendons. + + EN1992-1-1:2023 Eq. (8.37). + + Args: + ds (float): Depth of the tension reinforcement in mm. + As (float): Area of the tension reinforcement in mm2. + dp (float): Depth of the prestressed reinforcement in mm. + Ap (float): Area of the prestressed reinforcement in mm2. + bw (float): Width of the member in mm. + d (float): Effective depth in mm. + + Returns: + float: Reinforcement ratio. + + Raises: + ValueError: If any of the input values are negative or if bw is not + positive. + """ + if ds < 0: + raise ValueError(f'ds must not be negative. Got {ds}') + if As < 0: + raise ValueError(f'As must not be negative. Got {As}') + if dp < 0: + raise ValueError(f'dp must not be negative. Got {dp}') + if Ap < 0: + raise ValueError(f'Ap must not be negative. Got {Ap}') + if bw <= 0: + raise ValueError(f'bw must be positive. Got {bw}') + if d < 0: + raise ValueError(f'd must not be negative. Got {d}') + + return (ds * As + dp * Ap) / (bw * d**2) + + +def rho_l_planar( + vEd_y: float, vEd_x: float, rho_l_x: float, rho_l_y: float +) -> float: + """Calculate the reinforcement ratio for planar members with different + reinforcement ratios in both directions. + + EN1992-1-1:2023 Eq. (8.38), (8.39), (8.40). + + Args: + vEd_y (float): Shear force in y-direction (kN). + vEd_x (float): Shear force in x-direction (kN). + rho_l_x (float): Reinforcement ratio in x-direction. + rho_l_y (float): Reinforcement ratio in y-direction. + + Returns: + float: Reinforcement ratio. + + Raises: + ValueError: If any of the input values are negative. + """ + if vEd_y < 0: + raise ValueError(f'vEd_y must not be negative. Got {vEd_y}') + if vEd_x < 0: + raise ValueError(f'vEd_x must not be negative. Got {vEd_x}') + if rho_l_x < 0: + raise ValueError(f'rho_l_x must not be negative. Got {rho_l_x}') + if rho_l_y < 0: + raise ValueError(f'rho_l_y must not be negative. Got {rho_l_y}') + + ratio = vEd_y / vEd_x if vEd_x != 0 else float('inf') + + if ratio <= 0.5: + rho_l = rho_l_x + elif ratio >= 2: + rho_l = rho_l_y + else: + alpha_v = math.atan(vEd_y / vEd_x) + rho_l = ( + rho_l_x * math.cos(alpha_v) ** 4 + rho_l_y * math.sin(alpha_v) ** 4 + ) + + return rho_l + + +def cot_theta_min( + NEd: float, + VEd: float, + x: float, + d: float, + Ac: float, + apply_ductility_class_a_reduction: bool = False, +) -> float: + """Calculate the minimum cotangent of the compression field inclination + angle, theta_min, according to the conditions provided. + + EN1992-1-1:2023 Eq. (8.41). + + Args: + NEd (float): Axial force in the member in kN (positive for tension, + negative for compression). + VEd (float): Shear force in the member in kN (can be positive or + negative). + x (float): Depth of the compression chord in mm (must be non-negative). + d (float): Effective depth of the member in mm (must be positive). + Ac (float): Area of concrete cross-section in mm2 (must be positive). + apply_ductility_class_a_reduction (bool, optional): If True, applies + 20% reduction for ductility class A reinforcement. Default is False + + Returns: + float: Minimum cotangent of the compression field inclination angle. + - For tension (NEd > 0): 2.5 - 0.1*NEd/|VEd| (min 1.0) + - For compression (NEd < 0) with significant stress and x < 0.25d: + 3.0 (with interpolation from 2.5 at 0 MPa to 3.0 at 3 MPa) + - For compression (NEd < 0) with x >= 0.25d: tension formula + - For no axial force (NEd = 0): 2.5 + - For ductility class A: values reduced by 20% (when flag is True) + + Raises: + ValueError: If d or Ac is not positive, or if x is negative, or if + VEd is zero when NEd is not zero (to avoid division by zero). + """ + if d <= 0 or x < 0: + raise ValueError( + 'd must be positive and x must not be negative. ' + + f'Got d={d}, x={x}' + ) + if Ac <= 0: + raise ValueError(f'Ac must be positive. Got {Ac}') + + # Check VEd is not zero when needed for division + if VEd == 0 and NEd != 0: + raise ValueError( + 'VEd must not be zero when NEd is not zero to avoid division ' + + f'by zero. Got VEd={VEd}, NEd={NEd}' + ) + + # Calculate base cot_theta_min + if NEd > 0: + # Tension case + cot_theta_min = 2.5 - 0.1 * NEd / abs(VEd) + cot_theta_min = max(cot_theta_min, 1.0) + elif NEd < 0: + # Compression case + sigma_c = abs(NEd) * 1000 / Ac # Convert to MPa + if sigma_c >= 0.0 and x < 0.25 * d: + # Significant compressive stress and x < 0.25d + # Interpolate: 2.5 at 0 MPa, 3.0 at 3 MPa and above + # Formula: 2.5 + (3.0-2.5) * sigma_c / (3.0-0.0) + cot_theta_min = 3.0 if sigma_c >= 3.0 else 2.5 + sigma_c / 6.0 + else: + # Use tension formula for other compression cases + cot_theta_min = 2.5 - 0.1 * NEd / abs(VEd) + cot_theta_min = max(cot_theta_min, 1.0) + else: + # NEd == 0 (no axial force) + cot_theta_min = 2.5 + + # Apply ductility class A reduction (20% reduction) + if apply_ductility_class_a_reduction: + cot_theta_min *= 0.8 + + return cot_theta_min + + +def tau_Rd_sy( + rho_w: float, fywd: float, cot_theta: float, cot_theta_min: float +) -> float: + """Calculate the shear stress resistance of yielding shear reinforcement. + + EN1992-1-1:2023 Eq. (8.42). + + Args: + rho_w (float): Shear reinforcement ratio (unitless). + fywd (float): Design yield strength of the shear reinforcement in MPa. + cot_theta (float): Cotangent of the angle of the compression field + (must be between 1 and cot_theta_min). + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + + Returns: + float: Shear stress resistance in MPa. + + Raises: + ValueError: If rho_w or fywd is not positive, if cot_theta_min is less + than 1, or if cot_theta is not within [1, cot_theta_min]. + """ + if rho_w <= 0 or fywd <= 0: + raise ValueError( + 'Shear reinforcement ratio and yield strength must be positive. ' + + f'Got rho_w={rho_w}, fywd={fywd}' + ) + + if cot_theta_min < 1.0: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + if cot_theta < 1.0 or cot_theta > cot_theta_min: + raise ValueError( + 'cot_theta must be between 1 and cot_theta_min. ' + + f'Got cot_theta={cot_theta}, cot_theta_min={cot_theta_min}' + ) + + return rho_w * fywd * cot_theta + + +def rho_w(Asw: float, bw: float, s: float) -> float: + """Calculate the shear reinforcement ratio rho_w. + + EN1992-1-1:2023 Eq. (8.43). + + Args: + Asw (float): Area of shear reinforcement in mm2. + bw (float): Width of the web in mm. + s (float): Spacing of the shear reinforcement in mm. + + Returns: + float: Shear reinforcement ratio, unitless. + + Raises: + ValueError: If Asw, bw, or s is negative or zero. + + """ + if Asw <= 0 or bw <= 0 or s <= 0: + raise ValueError('Asw, bw, and s must be positive and non-zero.') + + return Asw / (bw * s) + + +def sigma_cd( + tau_Ed: float, + cot_theta: float, + cot_theta_min: float, + nu: float, + f_cd: float, +) -> float: + """Calculate the stress in the compression field sigma_cd and verify it. + + EN1992-1-1:2023 Eq. (8.44). + + Args: + tau_Ed (float): Design value of the shear stress in MPa (must be + non-negative). + cot_theta (float): Cotangent of the angle of the compression field + (must be between 1 and cot_theta_min). + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + nu (float): Coefficient (usually 0.5 as per the note, must be + non-negative). + f_cd (float): Design value of the concrete compressive strength in MPa + (must be positive). + + Returns: + float: Stress in the compression field sigma_cd in MPa. + + Raises: + ValueError: If tau_Ed is negative, if nu is negative or f_cd is not + positive, if cot_theta_min < 1, or if cot_theta is not within + [1, cot_theta_min]. + """ + if tau_Ed < 0: + raise ValueError(f'tau_Ed must not be negative. Got {tau_Ed}') + + if nu < 0 or f_cd <= 0: + raise ValueError( + 'nu must be non-negative and f_cd must be positive. ' + + f'Got nu={nu}, f_cd={f_cd}' + ) + + if cot_theta_min < 1.0: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + if cot_theta < 1.0 or cot_theta > cot_theta_min: + raise ValueError( + 'cot_theta must be between 1 and cot_theta_min. ' + + f'Got cot_theta={cot_theta}, cot_theta_min={cot_theta_min}' + ) + + # Calculate tan_theta from cot_theta to ensure consistency + tan_theta = 1.0 / cot_theta + sigma_cd_value = tau_Ed * (cot_theta + tan_theta) + return min(sigma_cd_value, nu * f_cd) + + +def tau_Rd( + rho_w: float, + fywd: float, + cot_theta: float, + cot_theta_min: float, + nu: float, + f_cd: float, +) -> float: + """Calculate the shear stress resistance tau_Rd considering the + simultaneous yielding of the shear reinforcement and failure of the + compression field. + + EN1992-1-1:2023 Eq. (8.42) and (8.44), NOTE 1. + + Args: + rho_w (float): Shear reinforcement ratio, unitless. + fywd (float): Design yield strength of the shear reinforcement in MPa. + cot_theta (float): Cotangent of the angle of the compression field. + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + nu (float): Coefficient (usually 0.5 as per the note). + f_cd (float): Design value of the concrete compressive strength in MPa. + + Returns: + float: Shear stress resistance tau_Rd in MPa. + + Raises: + ValueError: If any of the parameters are + negative, or if cot_theta_min < 1, + or if cot_theta is not within [1, cot_theta_min]. + """ + if rho_w < 0 or fywd < 0 or cot_theta < 0 or nu < 0 or f_cd < 0: + raise ValueError('All parameters must be positive.') + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + if cot_theta < 1 or cot_theta > cot_theta_min: + raise ValueError( + 'cot_theta must be within [1, cot_theta_min]. ' + + f'Got cot_theta={cot_theta}, cot_theta_min={cot_theta_min}' + ) + + tau_Rd_value = rho_w * fywd * cot_theta + return min(tau_Rd_value, nu * f_cd / 2) + + +def cot_theta_simultaneous( + nu: float, f_cd: float, rho_w: float, fywd: float, cot_theta_min: float +) -> float: + """Calculate the cotangent of the angle of the compression field that + gives simultaneous yielding of the shear reinforcement and failure of the + compression field. + + EN1992-1-1:2023 Eq. (8.42) and (8.44), NOTE 1. + + The value is calculated by equating the shear stress resistance from + reinforcement and the compression field, then clamped to + [1, cot_theta_min]. + + According to clause (6), when the angle of the compression field is + evaluated according to this method (simultaneous failure), nu may be + adopted as 0.5. + + Args: + nu (float): Coefficient. According to clause (6), when the angle is + evaluated using this method (simultaneous failure), nu may be + adopted as 0.5. Otherwise, nu should be calculated according to + Eq. (8.45). + f_cd (float): Design value of the concrete compressive strength in MPa. + rho_w (float): Shear reinforcement ratio, unitless (must be positive). + fywd (float): Design yield strength of the shear reinforcement in MPa + (must be positive). + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + + Returns: + float: Cotangent of the angle of the compression field that gives + simultaneous failure, clamped to [1, cot_theta_min]. + + Raises: + ValueError: If any of the parameters are negative, if + cot_theta_min < 1, if rho_w * fywd is zero (to avoid division + by zero), or if the expression under the square root is negative. + """ + if nu < 0 or f_cd < 0 or rho_w < 0 or fywd < 0: + raise ValueError( + 'All parameters must be positive. ' + + f'Got nu={nu}, f_cd={f_cd}, rho_w={rho_w}, fywd={fywd}' + ) + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + if rho_w == 0 or fywd == 0: + raise ValueError( + 'rho_w and fywd must not be zero to avoid division by zero. ' + + f'Got rho_w={rho_w}, fywd={fywd}' + ) + + # Calculate the value under the square root + sqrt_arg = (nu * f_cd) / (rho_w * fywd) - 1 + if sqrt_arg < 0: + raise ValueError( + 'Expression under square root must be non-negative. ' + + f'Got (nu * f_cd) / (rho_w * fywd) - 1 = {sqrt_arg}' + ) + cot_theta_value = math.sqrt(sqrt_arg) + return min(max(cot_theta_value, 1.0), cot_theta_min) + + +def epsilon_xt(Ftd: float, Es: float, Ast: float) -> float: + """Calculate epsilon_xt. + + EN1992-1-1:2023 Eq. (8.47). + + Ftd and Fcd are chord forces according to Figure 8.9 and Formulae (8.51) + to (8.52). + + Args: + Ftd (float): Tensile force in the flexural tension chord in kN. + Es (float): Modulus of elasticity of the steel in the tension chord in + MPa. + Ast (float): Area of the longitudinal reinforcement in the flexural + tension chord in mm2. + + Returns: + float: epsilon_xt (strain). + + Raises: + ValueError: If Es is not positive, or if Ast is not positive (to avoid + division by zero). + """ + if Es <= 0: + raise ValueError(f'Es must be positive. Got Es={Es}') + if Ast <= 0: + raise ValueError(f'Ast must be positive. Got Ast={Ast}') + return abs(Ftd) * 1000 / (Es * Ast) + + +def epsilon_xc_comp(Fcd: float, Ec: float, Acc: float) -> float: + """Calculate epsilon_xc for compression. + + EN1992-1-1:2023 Eq. (8.48). + + Ftd and Fcd are chord forces according to Figure 8.9 and Formulae (8.51) + to (8.52). Positive values of Fcd refer to compression in the compression + chord. + + Args: + Fcd (float): Compressive force in the flexural compression chord in kN. + Positive values refer to compression. + Ec (float): Modulus of elasticity of the concrete in the compression + chord in MPa. + Acc (float): Area of the flexural compression chord in mm2. + + Returns: + float: epsilon_xc (strain). + + Raises: + ValueError: If Ec is not positive, or if Acc is not positive (to avoid + division by zero). + """ + if Ec <= 0: + raise ValueError(f'Ec must be positive. Got Ec={Ec}') + if Acc <= 0: + raise ValueError(f'Acc must be positive. Got Acc={Acc}') + return abs(Fcd) * 1000 / (Ec * Acc) + + +def epsilon_xc_tens(Fcd: float, Es: float, Asc: float) -> float: + """Calculate epsilon_xc. + + EN1992-1-1:2023 Eq. (8.49). + + Ftd and Fcd are chord forces according to Figure 8.9 and Formulae (8.51) + to (8.52). Positive values of Fcd refer to compression in the compression + chord. + + Args: + Fcd (float): Compressive force in the flexural compression chord in kN. + Positive values refer to compression. + Es (float): Modulus of elasticity of the steel in the compression + chord in MPa. + Asc (float): Area of the longitudinal reinforcement in the flexural + compression chord in mm2. + + Returns: + float: epsilon_xc (strain). + + Raises: + ValueError: If Es is not positive, or if Asc is not positive (to avoid + division by zero). + """ + if Es <= 0: + raise ValueError(f'Es must be positive. Got Es={Es}') + if Asc <= 0: + raise ValueError(f'Asc must be positive. Got Asc={Asc}') + return abs(Fcd) * 1000 / (Es * Asc) + + +def epsilon_x(epsilon_xt: float, epsilon_xc: float) -> float: + """Calculate epsilon_x. + + EN1992-1-1:2023 Eq. (8.46). + + Note on sign conventions: + - epsilon_xt: Positive values refer to tension (elongation) in the + flexural tension chord. + - epsilon_xc: Positive values refer to compression (shortening) in the + flexural compression chord. + + Args: + epsilon_xt (float): Strain in the flexural tension chord. Positive + values refer to tension. + epsilon_xc (float): Strain in the flexural compression chord. Positive + values refer to compression. + + Returns: + float: epsilon_x (strain), clamped to non-negative values. + """ + epsilon_x_value = (epsilon_xt + epsilon_xc) / 2 + return max(epsilon_x_value, 0) + + +def nu(epsilon_x: float, cot_theta: float, cot_theta_min: float) -> float: + """Calculate nu. + + EN1992-1-1:2023 Eq. (8.45). + + Args: + epsilon_x (float): Average strain of the bottom and top chords. + cot_theta (float): Cotangent of the compression field inclination to + the member axis. + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + + Returns: + float: nu (dimensionless factor). + + Raises: + ValueError: If epsilon_x is negative, if cot_theta_min < 1, or if + cot_theta is not within [1, cot_theta_min]. + """ + if epsilon_x < 0: + raise ValueError(f'epsilon_x must not be negative. Got {epsilon_x}') + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + if cot_theta < 1 or cot_theta > cot_theta_min: + raise ValueError( + 'cot_theta must be within [1, cot_theta_min]. ' + + f'Got cot_theta={cot_theta}, cot_theta_min={cot_theta_min}' + ) + + nu_value = 1 / ( + 1.0 + 110 * (epsilon_x + (epsilon_x + 0.001) * cot_theta**2) + ) + return min(nu_value, 1.0) + + +def Nvd(VEd: float, cot_theta: float, cot_theta_min: float) -> float: + """Calculate the additional tensile axial force NVd due to shear VEd. + + EN1992-1-1:2023 Eq. (8.50). + + Args: + VEd (float): Shear force in kN. + cot_theta (float): Cotangent of the angle. + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + + Returns: + float: Additional tensile axial force NVd in kN. + + Raises: + ValueError: If cot_theta_min < 1, or if cot_theta is not within + [1, cot_theta_min]. + """ + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + if cot_theta < 1 or cot_theta > cot_theta_min: + raise ValueError( + 'cot_theta must be within [1, cot_theta_min]. ' + + f'Got cot_theta={cot_theta}, cot_theta_min={cot_theta_min}' + ) + return abs(VEd) * cot_theta + + +def Ftd( + MEd: float, + z: float, + NVd: float, + NE: float, +) -> float: + """Calculate the chord force Ftd. + + EN1992-1-1:2023 Eq. (8.51). + + Args: + MEd (float): Moment in kNm. Positive values refer to moments with top + compressed. + z (float): Lever arm in mm (must be positive to avoid division by + zero). + NVd (float): Additional tensile axial force in kN. + NE (float): Axial force in kN. Positive values refer to tension. + + Returns: + float: Chord force Ftd in kN. + + Raises: + ValueError: If z is not positive (to avoid division by zero). + """ + if z <= 0: + raise ValueError(f'z must be positive. Got z={z}') + return MEd * 1000 / z + (NVd + NE) / 2 + + +def Fcd( + MEd: float, + z: float, + NVd: float, + NE: float, +) -> float: + """Calculate the chord force Fcd. + + EN1992-1-1:2023 Eq. (8.52). + + Args: + MEd (float): Moment in kNm. Positive values refer to moments with top + compressed. + z (float): Lever arm in mm (must be positive to avoid division by + zero). + NVd (float): Additional tensile axial force in kN. + NE (float): Axial force in kN. Positive values refer to tension. + + Returns: + float: Chord force Fcd in kN. Positive values refer to compression. + + Raises: + ValueError: If z is not positive (to avoid division by zero). + """ + if z <= 0: + raise ValueError(f'z must be positive. Got z={z}') + return MEd * 1000 / z - (NVd + NE) / 2 + + +def Ftd_max( + MEd_max: float, + z: float, + NEd: float, +) -> float: + """Calculate the maximum chord force Ftd for cases of direct + intermediate support or concentrated loads. + + EN1992-1-1:2023 Eq. (8.53). + + Args: + MEd_max (float): Maximum moment along the member in kNm. Positive + values refer to moments with top compressed. + z (float): Lever arm in mm (must be positive to avoid division by + zero). + NEd (float): Axial force in kN. Positive values refer to tension. + + Returns: + float: Maximum chord force Ftd in kN. + + Raises: + ValueError: If z is not positive (to avoid division by zero). + """ + if z <= 0: + raise ValueError(f'z must be positive. Got z={z}') + return MEd_max * 1000 / z + NEd / 2 + + +def k_duct( + duct_material: Literal['steel', 'plastic'], + is_grouted: bool, + wall_thickness: float, + duct_diameter: float, +) -> float: + """Calculate the k_duct coefficient based on duct material, filling, and + wall thickness. + + EN1992-1-1:2023 8.2.3 (10). + + Args: + duct_material (str): Material of the duct ('steel' or 'plastic'). + is_grouted (bool): True if the duct is grouted, False otherwise. + wall_thickness (float): Wall thickness of the duct in mm. + duct_diameter (float): Outer diameter of the duct in mm. + + Returns: + float: Coefficient k_duct. + + Raises: + ValueError: If wall_thickness or duct_diameter is not positive. + ValueError: If duct_material is not 'steel' or 'plastic'. + + """ + if wall_thickness <= 0: + raise ValueError( + f'wall_thickness must be positive. Got {wall_thickness}' + ) + if duct_diameter <= 0: + raise ValueError( + f'duct_diameter must be positive. Got {duct_diameter}' + ) + + max_thickness = max(0.035 * duct_diameter, 2.0) + + if duct_material == 'steel' and is_grouted: + return 0.5 + if duct_material == 'plastic': + if is_grouted: + if wall_thickness <= max_thickness: + return 0.8 + return 1.2 + return 1.2 + raise ValueError( + 'Invalid duct material. Expected "steel" or "plastic". ' + + f'Got {duct_material}' + ) + + +def bw_nom( + bw: float, + duct_diameters: Iterable[float], + k_duct: float, +) -> float: + """Calculate the nominal web width considering the presence of ducts + when the sum of duct diameters exceeds bw/8. + + EN1992-1-1:2023 Eq. (8.54). + + Args: + bw (float): Actual web width in mm. + duct_diameters (Iterable[float]): Sequence of duct diameters in mm + (each must be non-negative). Can be a list, tuple, or numpy array. + k_duct (float): Coefficient depending on the material and filling of + the duct. + + Returns: + float: Nominal web width in mm. + + Raises: + ValueError: If bw is negative or if any duct diameter is negative. + ValueError: If the sum of duct diameters exceeds bw/8. + """ + if bw < 0: + raise ValueError(f'bw must not be negative. Got {bw}') + for d in duct_diameters: + if d < 0: + raise ValueError(f'Duct diameters must not be negative. Got {d}') + + sum_phi_duct = sum(duct_diameters) + if sum_phi_duct > bw / 8: + raise ValueError( + 'Sum of duct diameters exceeds bw/8.' + + f'Got sum {sum_phi_duct}, limit {bw/8}' + ) + + return bw - k_duct * sum_phi_duct + + +def tau_rd( + nu: float, + f_cd: float, + cot_theta: float, + cot_theta_min: float, + cot_beta_incl: float, + rho_w: float, + f_ywd: float, +) -> float: + """Calculate the enhanced shear stress resistance tau_Rd. + + EN1992-1-1:2023 Eq. (8.55). + + Args: + nu (float): The factor nu (unitless) (must be non-negative). + f_cd (float): Design value of concrete compressive strength in MPa + (must be positive). + cot_theta (float): Cotangent of the inclination of compression field. + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + cot_beta_incl (float): Cotangent of the inclination of load (cot + beta_incl). + rho_w (float): Reinforcement ratio rho_w (must be non-negative). + f_ywd (float): Design yield strength of shear reinforcement in MPa + (must be positive). + + Returns: + float: Enhanced shear stress resistance tau_Rd in MPa. + + Raises: + ValueError: If nu, f_cd, rho_w, or f_ywd is negative, if f_cd or + f_ywd is zero, if cot_theta_min < 1, or if cot_theta is not + within [1, cot_theta_min]. + """ + if nu < 0: + raise ValueError(f'nu must not be negative. Got {nu}') + if f_cd <= 0: + raise ValueError(f'f_cd must be positive. Got {f_cd}') + if rho_w < 0: + raise ValueError(f'rho_w must not be negative. Got {rho_w}') + if f_ywd <= 0: + raise ValueError(f'f_ywd must be positive. Got {f_ywd}') + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + if cot_theta < 1 or cot_theta > cot_theta_min: + raise ValueError( + 'cot_theta must be within [1, cot_theta_min]. ' + + f'Got cot_theta={cot_theta}, cot_theta_min={cot_theta_min}' + ) + + tau_rd_value = ( + nu * f_cd * (cot_theta - cot_beta_incl) / (1 + cot_theta**2) + + rho_w * f_ywd * cot_beta_incl + ) + tau_rd_max = nu * f_cd * cot_theta / (1 + cot_theta**2) + + return min(tau_rd_value, tau_rd_max) + + +def cot_theta_max_shear_constant_nu( + cot_beta_incl: float, + cot_theta_min: float, +) -> float: + """Calculate the optimum cot_theta for maximum shear resistance + for the case of a constant value nu according to clause (6). + + EN1992-1-1:2023 Section 8.2, NOTE 3. + + Args: + cot_beta_incl (float): Cotangent of the inclination of load (cot + beta_incl). + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + + Returns: + float: Optimum cotangent of the inclination of compression field, + capped at cot_theta_min. + + Raises: + ValueError: If cot_theta_min < 1 or if the calculated cot_theta < 1. + """ + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + # Constant nu according to clause (6) + cot_theta = cot_beta_incl + math.sqrt(1 + cot_beta_incl**2) + + # Cap at cot_theta_min + cot_theta = min(cot_theta, cot_theta_min) + + # Ensure cot_theta >= 1 + if cot_theta < 1: + raise ValueError( + f'Calculated cot_theta ({cot_theta}) is less than 1. ' + + 'This indicates invalid input parameters.' + ) + + return cot_theta + + +def cot_theta_max_shear_variable_nu( + a: float, + z: float, + cot_theta_min: float, +) -> float: + """Calculate the optimum cot_theta for maximum shear resistance + for the case of a variable nu according to clause (7). + + EN1992-1-1:2023 Section 8.2, NOTE 3. + + but should not be larger than cot_theta according to 8.2.3(5), NOTE 1. + + Args: + a (float): Distance from support to concentrated load in mm + (must be positive). + z (float): Lever arm in mm (must be positive). + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + + Returns: + float: Optimum cotangent of the inclination of compression field, + capped at cot_theta_min. + + Raises: + ValueError: If a or z is non-positive, if cot_theta_min < 1, or if + the calculated cot_theta < 1. + """ + if a <= 0: + raise ValueError(f'a must be positive. Got {a}') + if z <= 0: + raise ValueError(f'z must be positive. Got {z}') + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + # Variable nu according to clause (7) + cot_theta = 1.3 * a / z + + # Cap at cot_theta_min + cot_theta = min(cot_theta, cot_theta_min) + + # Ensure cot_theta >= 1 + if cot_theta < 1: + raise ValueError( + f'Calculated cot_theta ({cot_theta}) is less than 1. ' + + 'This indicates invalid input parameters.' + ) + + return cot_theta + + +def sigma_swd( + Es: float, + eps_x: float, + f_ywd: float, + cot_theta: float, + cot_theta_min: float, +) -> float: + """Calculate the stress sigma_swd in the shear reinforcement. + + EN1992-1-1:2023 Eq. (8.56). + + Args: + Es (float): Modulus of elasticity of steel Es in MPa (must be + positive). + eps_x (float): Longitudinal strain epsilon_x. + f_ywd (float): Design yield strength of shear reinforcement in MPa + (must be positive). + cot_theta (float): Cotangent of the inclination of compression field. + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + + Returns: + float: Stress sigma_swd in the shear reinforcement in MPa. + + Raises: + ValueError: If Es or f_ywd is negative or zero, if cot_theta_min < 1, + or if cot_theta is not within [1, cot_theta_min]. + """ + if Es <= 0: + raise ValueError(f'Es must be positive. Got {Es}') + if f_ywd <= 0: + raise ValueError(f'f_ywd must be positive. Got {f_ywd}') + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + if cot_theta < 1 or cot_theta > cot_theta_min: + raise ValueError( + 'cot_theta must be within [1, cot_theta_min]. ' + + f'Got cot_theta={cot_theta}, cot_theta_min={cot_theta_min}' + ) + + sigma_swd_value = Es * (cot_theta**2 * (eps_x + 0.001) - 0.001) + + return min(sigma_swd_value, f_ywd) + + +def delta_MEd( + tau_ed: float, + rho_w: float, + f_ywd: float, + cot_theta: float, + cot_theta_min: float, + z: float, + b_w: float, + a: float, + x: float, +) -> float: + """Calculate the additional moment delta MEd. + + EN1992-1-1:2023 Eq. (8.57). + + Args: + tau_ed (float): Shear stress tau_Ed in MPa (must be non-negative). + rho_w (float): Reinforcement ratio rho_w. + f_ywd (float): Design yield strength of shear reinforcement in MPa + (must be positive). + cot_theta (float): Cotangent of the inclination of compression field. + cot_theta_min (float): Minimum cotangent of the compression field + inclination angle (must be >= 1). + z (float): Lever arm z in mm (must be positive). + b_w (float): Width of the web bw in mm (must be positive). + a (float): Distance between the axis of the support and the + concentrated force in mm (must be non-negative). + x (float): Distance between the support and the investigated + cross-section in mm (must be non-negative). + + Returns: + float: Additional moment delta MEd in kNm. + + Raises: + ValueError: If tau_ed, a, or x is negative, if f_ywd, z, or b_w is + non-positive, if cot_theta_min < 1, or if cot_theta is not + within [1, cot_theta_min]. + """ + if tau_ed < 0: + raise ValueError(f'tau_ed must not be negative. Got {tau_ed}') + if f_ywd <= 0: + raise ValueError(f'f_ywd must be positive. Got {f_ywd}') + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + if cot_theta < 1 or cot_theta > cot_theta_min: + raise ValueError( + 'cot_theta must be within [1, cot_theta_min]. ' + + f'Got cot_theta={cot_theta}, cot_theta_min={cot_theta_min}' + ) + if z <= 0: + raise ValueError(f'z must be positive. Got {z}') + if b_w <= 0: + raise ValueError(f'b_w must be positive. Got {b_w}') + if a < 0: + raise ValueError(f'a must not be negative. Got {a}') + if x < 0: + raise ValueError(f'x must not be negative. Got {x}') + + return ( + (tau_ed - rho_w * f_ywd * cot_theta) * z * b_w * (a / 2 - x) + ) / 1e6 # Convert to kNm + + +def cot_theta_inclined( + cot_theta: float, + alpha_w: float, + cot_theta_min: float, +) -> float: + """Calculate and validate cot_theta for inclined shear reinforcement. + + EN1992-1-1:2023 Eq. (8.58). + + Args: + cot_theta (float): Cotangent of the inclination of compression field. + alpha_w (float): Angle of inclined shear reinforcement alpha_w in + degrees (must be in range [45, 90]). + cot_theta_min (float): Maximum value for cot_theta (must be >= 1). + + Returns: + float: Validated cot_theta value clamped to satisfy the constraint + tan(alpha_w/2) <= cot_theta <= cot_theta_min. + + Raises: + ValueError: If alpha_w is outside the range [45, 90] degrees, or if + cot_theta_min < 1. + """ + if alpha_w < 45 or alpha_w > 90: + raise ValueError( + f'alpha_w must be in the range [45, 90] degrees. Got {alpha_w}' + ) + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + alpha_w_rad = math.radians(alpha_w) + cot_theta_min_constraint = math.tan(alpha_w_rad / 2) + + return min(max(cot_theta_min_constraint, cot_theta), cot_theta_min) + + +def tau_Rd_sy_inclined( + rho_w: float, + f_ywd: float, + cot_theta: float, + alpha_w: float, + cot_theta_min: float, +) -> float: + """Calculate the shear stress resistance tau_Rd,sy for inclined shear + reinforcement. + + EN1992-1-1:2023 Eq. (8.59). + + Args: + rho_w (float): Reinforcement ratio rho_w (must be non-negative). + f_ywd (float): Design yield strength of shear reinforcement in MPa + (must be positive, cannot be zero). + cot_theta (float): Cotangent of the inclination of compression field. + Will be clamped according to Eq. (8.58). + alpha_w (float): Angle of inclined shear reinforcement alpha_w in + degrees (must be in range [45, 90]). + cot_theta_min (float): Maximum value for cot_theta (must be >= 1). + + Returns: + float: Shear stress resistance tau_Rd,sy in MPa. + + Raises: + ValueError: If rho_w is negative, if f_ywd is not positive (cannot be + zero), if alpha_w is outside the range [45, 90] degrees, or if + cot_theta_min < 1. + """ + if rho_w < 0: + raise ValueError(f'rho_w must not be negative. Got {rho_w}') + if f_ywd <= 0: + raise ValueError( + f'f_ywd must be positive (cannot be zero). Got {f_ywd}' + ) + if alpha_w < 45 or alpha_w > 90: + raise ValueError( + f'alpha_w must be in the range [45, 90] degrees. Got {alpha_w}' + ) + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + # Validate cot_theta using Eq. (8.58) + cot_theta_validated = cot_theta_inclined(cot_theta, alpha_w, cot_theta_min) + + alpha_w_rad = math.radians(alpha_w) + # Handle alpha_w = 90° explicitly to avoid numerical issues with tan(90°) + if math.isclose(alpha_w, 90.0): + cot_alpha_w = 0.0 + else: + cot_alpha_w = 1 / math.tan(alpha_w_rad) + + return ( + rho_w + * f_ywd + * (cot_theta_validated + cot_alpha_w) + * math.sin(alpha_w_rad) + ) + + +def sigma_cd_inclined( + tau_ed: float, + cot_theta: float, + alpha_w: float, + nu: float, + f_cd: float, + cot_theta_min: float, +) -> float: + """Calculate the compression stress sigma_cd for inclined shear + reinforcement. + + EN1992-1-1:2023 Eq. (8.60). + + Args: + tau_ed (float): Shear stress tau_Ed in MPa (must be non-negative). + cot_theta (float): Cotangent of the inclination of compression field. + Will be clamped according to Eq. (8.58). + alpha_w (float): Angle of inclined shear reinforcement alpha_w in + degrees (must be in range [45, 90]). + nu (float): The factor nu (must be non-negative). + f_cd (float): Design value of concrete compressive strength in MPa + (must be positive, cannot be zero). + cot_theta_min (float): Maximum value for cot_theta (must be >= 1). + + Returns: + float: Compression stress sigma_cd in MPa. + + Raises: + ValueError: If tau_ed is negative, if nu is negative, if f_cd is not + positive (cannot be zero), if alpha_w is outside the range + [45, 90] degrees, or if cot_theta_min < 1. + """ + if tau_ed < 0: + raise ValueError(f'tau_ed must not be negative. Got {tau_ed}') + if nu < 0: + raise ValueError(f'nu must be non-negative. Got {nu}') + if f_cd <= 0: + raise ValueError(f'f_cd must be positive (cannot be zero). Got {f_cd}') + if alpha_w < 45 or alpha_w > 90: + raise ValueError( + f'alpha_w must be in the range [45, 90] degrees. Got {alpha_w}' + ) + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + # Validate cot_theta using Eq. (8.58) + cot_theta_validated = cot_theta_inclined(cot_theta, alpha_w, cot_theta_min) + # Handle alpha_w = 90° explicitly to avoid numerical issues with tan(90°) + if math.isclose(alpha_w, 90.0): + cot_alpha_w = 0.0 + else: + alpha_w_rad = math.radians(alpha_w) + cot_alpha_w = 1 / math.tan(alpha_w_rad) + + sigma_cd_value = ( + tau_ed + * (1 + cot_theta_validated**2) + / (cot_theta_validated + cot_alpha_w) + ) + + return min(sigma_cd_value, nu * f_cd) + + +def NVds_inclined( + VEd: float, cot_theta: float, alpha_w: float, cot_theta_min: float +) -> float: + """Calculate the axial tensile force NVd. + + EN1992-1-1:2023 Eq. (8.58), (8.61). + + Args: + VEd (float): Design shear force VEd in kN. + cot_theta (float): Cotangent of the inclination of compression field. + Will be clamped according to Eq. (8.58). + alpha_w (float): Angle of inclined shear reinforcement alpha_w in + degrees (must be in range [45, 90]). + cot_theta_min (float): Maximum value for cot_theta (must be >= 1). + + Returns: + float: Axial tensile force NVd in kN. + + Raises: + ValueError: If alpha_w is outside the range [45, 90] degrees, or if + cot_theta_min < 1. + """ + if alpha_w < 45 or alpha_w > 90: + raise ValueError( + f'alpha_w must be in the range [45, 90] degrees. Got {alpha_w}' + ) + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + # Validate cot_theta using Eq. (8.58) + cot_theta_validated = cot_theta_inclined(cot_theta, alpha_w, cot_theta_min) + + alpha_w_rad = math.radians(alpha_w) + # Handle alpha_w = 90° explicitly to avoid numerical issues with tan(90°) + if math.isclose(alpha_w, 90.0): + cot_alpha_w = 0.0 + else: + cot_alpha_w = 1 / math.tan(alpha_w_rad) + + return abs(VEd) * (cot_theta_validated - cot_alpha_w) + + +def tau_Rd_inclined( + nu: float, + f_cd: float, + cot_theta: float, + cot_beta_incl: float, + rho_w: float, + f_ywd: float, + alpha_w: float, + cot_theta_min: float, +) -> float: + """Calculate the shear stress resistance tau_Rd for members with inclined + shear reinforcement. + + EN1992-1-1:2023 Eq. (8.62). + + Args: + nu (float): The factor nu (unitless, must be non-negative). + f_cd (float): Design value of concrete compressive strength in MPa + (must be positive, cannot be zero). + cot_theta (float): Cotangent of the inclination of compression field. + Will be clamped according to Eq. (8.58). + cot_beta_incl (float): Cotangent of the inclination of load (cot + beta_incl). + rho_w (float): Reinforcement ratio rho_w (unitless, must be + non-negative). + f_ywd (float): Design yield strength of shear reinforcement in MPa + (must be positive, cannot be zero). + alpha_w (float): Angle of inclined shear reinforcement alpha_w in + degrees (must be in range [45, 90]). + cot_theta_min (float): Maximum value for cot_theta (must be >= 1). + + Returns: + float: Shear stress resistance tau_Rd in MPa. + + Raises: + ValueError: If nu is negative, if f_cd is not positive (cannot be + zero), if f_ywd is not positive (cannot be zero), if rho_w is + negative, if alpha_w is outside the range [45, 90] degrees, or + if cot_theta_min < 1. + """ + if nu < 0: + raise ValueError(f'nu must be non-negative. Got {nu}') + if f_cd <= 0: + raise ValueError(f'f_cd must be positive (cannot be zero). Got {f_cd}') + if f_ywd <= 0: + raise ValueError( + f'f_ywd must be positive (cannot be zero). Got {f_ywd}' + ) + if rho_w < 0: + raise ValueError(f'rho_w must be non-negative. Got {rho_w}') + if alpha_w < 45 or alpha_w > 90: + raise ValueError( + f'alpha_w must be in the range [45, 90] degrees. Got {alpha_w}' + ) + if cot_theta_min < 1: + raise ValueError(f'cot_theta_min must be >= 1. Got {cot_theta_min}') + + # Validate cot_theta using Eq. (8.58) + cot_theta_validated = cot_theta_inclined(cot_theta, alpha_w, cot_theta_min) + + alpha_w_rad = math.radians(alpha_w) + # Handle alpha_w = 90° explicitly to avoid numerical issues with tan(90°) + if math.isclose(alpha_w, 90.0): + cot_alpha_w = 0.0 + else: + cot_alpha_w = 1 / math.tan(alpha_w_rad) + + tau_rd_value = ( + nu + * f_cd + * (cot_theta_validated - cot_beta_incl) + / (1 + cot_theta_validated**2) + ) + rho_w * f_ywd * (cot_beta_incl + cot_alpha_w) * math.sin(alpha_w_rad) + + tau_rd_max = ( + nu + * f_cd + * (cot_theta_validated + cot_alpha_w) + / (1 + cot_theta_validated**2) + ) + + return min(tau_rd_value, tau_rd_max) + + +def sigma_swd_inclined( + Es: float, + eps_x: float, + cot_theta: float, + alpha_w: float, + f_ywd: float, +) -> float: + """Calculate the stress sigma_swd in the shear reinforcement for + compression field inclinations. + + EN1992-1-1:2023 Eq. (8.63). + + Compression field inclinations with cotθ < tan(αw/2) are allowed if the + yield strength fywd in Formula (8.62) is replaced by the stress σswd + according to this formula. + + Args: + Es (float): Modulus of elasticity of steel Es in MPa (must be positive, + cannot be zero). + eps_x (float): Longitudinal strain epsilon_x (unitless). + cot_theta (float): Cotangent inclination of compression field. + alpha_w (float): Angle of inclined shear reinforcement alpha_w in + degrees (must be in range [45, 90]). + f_ywd (float): Design yield strength of shear reinforcement in MPa + (must be positive, cannot be zero). + + Returns: + float: Stress sigma_swd in the shear reinforcement in MPa. + + Raises: + ValueError: If Es is not positive (cannot be zero), if f_ywd is not + positive (cannot be zero), or if alpha_w is outside the range + [45, 90] degrees. + """ + if Es <= 0: + raise ValueError(f'Es must be positive (cannot be zero). Got {Es}') + if f_ywd <= 0: + raise ValueError( + f'f_ywd must be positive (cannot be zero). Got {f_ywd}' + ) + if alpha_w < 45 or alpha_w > 90: + raise ValueError( + f'alpha_w must be in the range [45, 90] degrees. Got {alpha_w}' + ) + + alpha_w_rad = math.radians(alpha_w) + # Handle alpha_w = 90° explicitly to avoid numerical issues with tan(90°) + if math.isclose(alpha_w, 90.0): + cot_alpha_w = 0.0 + else: + cot_alpha_w = 1 / math.tan(alpha_w_rad) + + sigma_swd_value = Es * ( + (eps_x + 0.001) + * ((cot_theta + cot_alpha_w) ** 2 / (1 + cot_alpha_w**2)) + - 0.001 + ) + + return min(sigma_swd_value, f_ywd) + + +def tau_Rdm(tau_rd: float, m_ed: float, m_rd: float) -> float: + """Calculate the shear stress resistance reduced by the influence of + transverse bending. + + EN1992-1-1:2023 Eq. (8.64). + + Args: + tau_rd (float): Shear resistance tau_Rd in MPa (must be non-negative). + m_ed (float): Applied transverse bending moment mEd in kNm (must be + non-negative). + m_rd (float): Bending resistance without interaction with shear mRd in + kNm (must be positive, cannot be zero). + + Returns: + float: Reduced shear stress resistance tau_Rdm in MPa. + + Raises: + ValueError: If tau_rd or m_ed is negative, or if m_rd is not positive + (cannot be zero). + """ + if tau_rd < 0: + raise ValueError(f'tau_rd must not be negative. Got {tau_rd}') + if m_ed < 0: + raise ValueError(f'm_ed must not be negative. Got {m_ed}') + if m_rd <= 0: + raise ValueError(f'm_rd must be positive (cannot be zero). Got {m_rd}') + + return tau_rd * (1 - (m_ed / m_rd)) + + +def tau_Ed_flange( + delta_Fd: float, + hf: float, + delta_x: float, +) -> float: + """Calculate the longitudinal shear stress at the junction between a flange + and web. + + EN1992-1-1:2023 Eq. (8.65). + + Args: + delta_Fd (float): Change of axial force in the flange over + the length delta x in kN. + hf (float): Thickness of the flange at the junction in mm. + delta_x (float): Length under consideration delta x in mm. + + Returns: + float: Longitudinal shear stress tau_Ed in MPa. + + Raises: + ValueError: If any of the input parameters are negative. + """ + if delta_Fd < 0: + raise ValueError(f'delta_fd must not be negative. Got {delta_Fd}') + if hf <= 0: + raise ValueError(f'hf must be positive. Got {hf}') + if delta_x <= 0: + raise ValueError(f'delta_x must be positive. Got {delta_x}') + + return delta_Fd * 1000 / (hf * delta_x) + + +def check_tau_Ed_flange_verification( + tau_ed: float, Ast_min: float, sf: float, hf: float, fyd: float +) -> bool: + """Check if further verification of shear between web and flanges may be + omitted. + + EN1992-1-1:2023 Eq. (8.66). + + Args: + tau_ed (float): Longitudinal shear stress tau_Ed in MPa (must be + non-negative). + Ast_min (float): Minimum transverse reinforcement according to Table + 12.1 (NDP) in mm2 (must be non-negative). + sf (float): Spacing of reinforcement sf in mm (must be positive). + hf (float): Thickness of the flange at the junction in mm (must be + positive). + fyd (float): Design yield strength of reinforcement fyd in MPa (must be + positive). + + Returns: + bool: True meaning further verification of shear between + web and flanges may be omitted. False otherwise. + + Raises: + ValueError: If tau_ed or Ast_min is negative, or if sf, hf, or fyd is + not positive. + """ + if tau_ed < 0: + raise ValueError(f'tau_ed must not be negative. Got {tau_ed}') + if Ast_min < 0: + raise ValueError(f'Ast_min must not be negative. Got {Ast_min}') + if sf <= 0: + raise ValueError(f'sf must be positive. Got {sf}') + if hf <= 0: + raise ValueError(f'hf must be positive. Got {hf}') + if fyd <= 0: + raise ValueError(f'fyd must be positive. Got {fyd}') + + return tau_ed <= (Ast_min / (sf * hf)) * fyd + + +def Asf_flange( + tau_ed: float, sf: float, hf: float, fyd: float, cot_theta_f: float +) -> float: + """Calculate the minimum transverse reinforcement in flanges. + + EN1992-1-1:2023 Eq. (8.69). + + Args: + tau_ed (float): Longitudinal shear stress tau_Ed in MPa (must be + non-negative). + sf (float): Spacing of reinforcement sf in mm (must be positive). + hf (float): Thickness of the flange at the junction in mm (must be + positive). + fyd (float): Design yield strength of reinforcement fyd in MPa (must be + positive, cannot be zero). + cot_theta_f (float): Cotangent of the inclination angle of the + compression field in the flange thetaf (must be >= 1). For + compression flanges: 1 ≤ cot_theta_f ≤ 3.0 (8.67). + For tension flanges: 1 ≤ cot_theta_f ≤ 1.25 (8.68). + + Returns: + float: Required transverse reinforcement area Asf in mm2. + + Raises: + ValueError: If tau_ed is negative, if sf, hf, or fyd is not positive + (cannot be zero), or if cot_theta_f < 1. + """ + if tau_ed < 0: + raise ValueError(f'tau_ed must not be negative. Got {tau_ed}') + if sf <= 0: + raise ValueError(f'sf must be positive. Got {sf}') + if hf <= 0: + raise ValueError(f'hf must be positive. Got {hf}') + if fyd <= 0: + raise ValueError(f'fyd must be positive (cannot be zero). Got {fyd}') + if cot_theta_f < 1: + raise ValueError(f'cot_theta_f must be >= 1. Got {cot_theta_f}') + + return (tau_ed * sf * hf) / (fyd * cot_theta_f) + + +def sigma_cd_flange( + tau_ed: float, + cot_theta_f: float, + fcd: float, + nu: float = 0.5, +) -> float: + """Calculate the compression field stress in the flange. + + EN1992-1-1:2023 Eq. (8.70), (8.71). + + Args: + tau_ed (float): Longitudinal shear stress tau_Ed in MPa (must be + non-negative). + cot_theta_f (float): Cotangent of the inclination angle of the + compression field in the flange thetaf (must be >= 1). For + compression flanges: 1 ≤ cotθf ≤ 3.0. For tension flanges: + 1 ≤ cotθf ≤ 1.25. + fcd (float): Design compressive strength of concrete fcd in MPa (must + be positive, cannot be zero). + nu (float, optional): Strength reduction factor (must be positive, + cannot be zero). Default is 0.5. + + Returns: + float: The compression field stress in MPa. + + Raises: + ValueError: If tau_ed is negative, if cot_theta_f < 1, if fcd is not + positive (cannot be zero), or if nu is not positive (cannot be + zero). + """ + if tau_ed < 0: + raise ValueError(f'tau_ed must not be negative. Got {tau_ed}') + if cot_theta_f < 1: + raise ValueError(f'cot_theta_f must be >= 1. Got {cot_theta_f}') + if fcd <= 0: + raise ValueError(f'fcd must be positive (cannot be zero). Got {fcd}') + if nu <= 0: + raise ValueError(f'nu must be positive (cannot be zero). Got {nu}') + + sigma_cd = tau_ed * (cot_theta_f + 1 / cot_theta_f) + + return min(sigma_cd, nu * fcd) + + +def eps_x_flang(Ftd: float, Ast: float, Es: float) -> float: + """Calculate the longitudinal strain in the tensile flange based on the + thickness of the tensile flange and the distance from the neutral axis. + + EN1992-1-1:2023 Eq. (8.72) + + Args: + Ftd (float): Force in the tension chord in kN (must be positive). + Ast (float): Longitudinal reinforcement area in mm2 (must be positive). + Es (float): Steel modulus of elasticity in MPa (must be positive, + cannot be zero). + + Returns: + float: The longitudinal strain in the tensile flange (unitless). + + Raises: + ValueError: If Ftd is not positive, if Ast is not positive, or if Es + is not positive (cannot be zero). + """ + if Ftd <= 0: + raise ValueError(f'Ftd must be positive. Got {Ftd}') + if Ast <= 0: + raise ValueError(f'Ast must be positive. Got {Ast}') + if Es <= 0: + raise ValueError(f'Es must be positive (cannot be zero). Got {Es}') + + return Ftd * 1000 / (Ast * Es) + + +def tau_Edi(VEdi: float, Ai: float) -> float: + """Calculate the design value of the shear stress at an interface. + + EN1992-1-1:2023 Eq. (8.74). + + Args: + VEdi (float): Shear force acting parallel to the interface in kN (must + be non-negative). + Ai (float): Area of the interface in mm2 (must be positive, cannot be + zero). + + Returns: + float: Shear stress at the interface in MPa. + + Raises: + ValueError: If VEdi is negative, or if Ai is not positive (cannot be + zero). + """ + if VEdi < 0: + raise ValueError(f'VEdi must not be negative. Got {VEdi}') + if Ai <= 0: + raise ValueError(f'Ai must be positive (cannot be zero). Got {Ai}') + + return VEdi * 1000 / Ai + + +def tau_Edi_composite( + beta_new: float, VEd: float, z: float, bi: float +) -> float: + """Calculate the longitudinal shear stress between concrete interfaces due + to composite action. + + EN1992-1-1:2023 Eq. (8.75). + + Args: + beta_new (float): Ratio of the longitudinal force in the new concrete + to the total longitudinal force, dimensionless (must be + non-negative). + VEd (float): Shear force acting perpendicular to the interface in kN + (must be non-negative). + z (float): Lever arm of the composite section in mm (must be positive, + cannot be zero). + bi (float): Width of the interface in mm (must be positive, cannot be + zero). + + Returns: + float: Longitudinal shear stress at the interface in MPa. + + Raises: + ValueError: If beta_new or VEd is negative, or if z or bi is not + positive (cannot be zero). + """ + if beta_new < 0: + raise ValueError(f'beta_new must not be negative. Got {beta_new}') + if VEd < 0: + raise ValueError(f'VEd must not be negative. Got {VEd}') + if z <= 0: + raise ValueError(f'z must be positive (cannot be zero). Got {z}') + if bi <= 0: + raise ValueError(f'bi must be positive (cannot be zero). Got {bi}') + + return beta_new * VEd * 1000 / (z * bi) + + +def tau_Rdi( + fck: float, + sigma_n: float, + Ai: float, + Asi: float, + fyd: float, + alpha_deg: float, + cv1: float, + mu_v: float, + gamma_c: float, + fcd: float, +) -> float: + """Calculate the design shear stress resistance at the interface for + scenarios without reinforcement or where reinforcement is sufficiently + anchored. + + EN1992-1-1:2023 Eq. (8.76). + + Args: + fck (float): Lowest compressive strength of the concretes at the + interface in MPa (must be positive, cannot be zero). + sigma_n (float): Normal stress over the interface area in MPa. Sign + convention: positive values represent compressive stress. + Ai (float): Area of the interface in mm2 (must be positive, cannot be + zero). + Asi (float): Cross-sectional area of bonded reinforcement crossing the + interface in mm2 (must be non-negative). + fyd (float): Design yield strength of the reinforcement in MPa (must + be positive, cannot be zero). + alpha_deg (float): Angle of reinforcement crossing the interface in + degrees (must be between 35 and 135). + cv1 (float): Coefficient depending on the roughness of the interface, + dimensionless (must be non-negative). + mu_v (float): Friction coefficient depending on the roughness of the + interface, dimensionless (must be non-negative). + gamma_c (float): Safety factor for concrete (must be positive, + cannot be zero). + fcd (float): Design compressive strength of concrete in MPa (must be + positive, cannot be zero). Calculated according to EN1992-1-1:2023 + Eq. (5.3) as fcd = eta_cc * k_tc * fck / gamma_c. + + Returns: + float: Shear stress resistance at the interface in MPa. + + Raises: + ValueError: If fck, Ai, fyd, gamma_c, or fcd is not positive (cannot be + zero), if Asi, cv1, or mu_v is negative, or if alpha_deg is + outside the range [35, 135]. + """ + if fck <= 0: + raise ValueError(f'fck must be positive (cannot be zero). Got {fck}') + if Ai <= 0: + raise ValueError(f'Ai must be positive (cannot be zero). Got {Ai}') + if Asi < 0: + raise ValueError(f'Asi must not be negative. Got {Asi}') + if fyd <= 0: + raise ValueError(f'fyd must be positive (cannot be zero). Got {fyd}') + if gamma_c <= 0: + raise ValueError( + f'gamma_c must be positive (cannot be zero). Got {gamma_c}' + ) + if fcd <= 0: + raise ValueError(f'fcd must be positive (cannot be zero). Got {fcd}') + if cv1 < 0: + raise ValueError(f'cv1 must not be negative. Got {cv1}') + if mu_v < 0: + raise ValueError(f'mu_v must not be negative. Got {mu_v}') + if not (35 <= alpha_deg <= 135): + raise ValueError( + f'alpha_deg must be between 35 and 135 degrees. Got {alpha_deg}' + ) + + sigma_n = max(0, min(sigma_n, 0.6 * fcd)) + + alpha_rad = math.radians(alpha_deg) + rho_i = Asi / Ai + + tau_rdi = ( + cv1 * math.sqrt(fck) / gamma_c + + mu_v * sigma_n + + rho_i * fyd * (mu_v * math.sin(alpha_rad) + math.cos(alpha_rad)) + ) + + # Limiting tau_rdi according to EN1992-1-1:2023 Eq. (8.76) + return min(tau_rdi, 0.30 * fcd + rho_i * fyd * math.cos(alpha_rad)) + + +def cv1( + surface_roughness: Literal[ + 'very smooth', 'smooth', 'rough', 'very rough', 'keyed' + ], + tensile_stress: bool = False, +) -> float: + """Get the cv1 coefficient based on the surface roughness and tensile + stress condition. + + EC1992-1-1:2023 Table (8.2). + + Args: + surface_roughness (str): Description of the surface roughness. + tensile_stress (bool): True if tensile stresses are present. + + Returns: + float: The cv1 coefficient. + + Raises: + ValueError: If an unknown surface roughness is provided. + """ + if tensile_stress: + return 0 + + coefficients = { + 'very smooth': 0.01, + 'smooth': 0.08, + 'rough': 0.15, + 'very rough': 0.19, + 'keyed': 0.37, + } + if surface_roughness not in coefficients: + raise ValueError( + f'Unknown surface roughness: {surface_roughness}. ' + f'Must be one of: {list(coefficients.keys())}' + ) + return coefficients[surface_roughness] + + +def mu_v( + surface_roughness: Literal[ + 'very smooth', 'smooth', 'rough', 'very rough', 'keyed' + ], +) -> float: + """Get the mu_v coefficient based on the surface roughness. + + EC1992-1-1:2023 Table (8.2). + + Args: + surface_roughness (str): Description of the surface roughness. + + Returns: + float: The mu_v coefficient. + + Raises: + ValueError: If an unknown surface roughness is provided. + """ + coefficients = { + 'very smooth': 0.5, + 'smooth': 0.6, + 'rough': 0.7, + 'very rough': 0.9, + 'keyed': 0.9, + } + if surface_roughness not in coefficients: + raise ValueError( + f'Unknown surface roughness: {surface_roughness}. ' + f'Must be one of: {list(coefficients.keys())}' + ) + return coefficients[surface_roughness] + + +def cv2( + surface_roughness: Literal['very smooth', 'smooth', 'rough', 'very rough'], + tensile_stress: bool = False, +) -> float: + """Get the cv2 coefficient based on the surface roughness and tensile + stress condition. + + EC1992-1-1:2023 Table (8.2). + + Args: + surface_roughness (str): Description of the surface roughness. + tensile_stress (bool): True if tensile stresses are present. + + Returns: + float: The cv2 coefficient, or None for keyed surfaces. + + Raises: + ValueError: If an unknown surface roughness is provided. + """ + if tensile_stress: + return 0 + + coefficients = { + 'very smooth': 0, + 'smooth': 0, + 'rough': 0.08, + 'very rough': 0.15, + } + if surface_roughness not in coefficients: + raise ValueError( + f'Unknown surface roughness: {surface_roughness}. ' + f'Must be one of: {list(coefficients.keys())}' + ) + return coefficients[surface_roughness] + + +def kv( + surface_roughness: Literal['very smooth', 'smooth', 'rough', 'very rough'], +) -> float: + """Get the kv coefficient based on the surface roughness. + + EC1992-1-1:2023 Table (8.2). + + Args: + surface_roughness (str): Description of the surface roughness. + + Returns: + float: The kv coefficient, or None for keyed surfaces. + + Raises: + ValueError: If an unknown surface roughness is provided. + """ + coefficients = { + 'very smooth': 0, + 'smooth': 0.5, + 'rough': 0.5, + 'very rough': 0.5, + } + if surface_roughness not in coefficients: + raise ValueError( + f'Unknown surface roughness: {surface_roughness}. ' + f'Must be one of: {list(coefficients.keys())}' + ) + return coefficients[surface_roughness] + + +def kdowel( + surface_roughness: Literal['very smooth', 'smooth', 'rough', 'very rough'], +) -> float: + """Get the kdowel coefficient based on the surface roughness. + + EC1992-1-1:2023 Table (8.2). + + Args: + surface_roughness (str): Description of the surface roughness. + + Returns: + float: The kdowel coefficient, or None for keyed surfaces. + + Raises: + ValueError: If an unknown surface roughness is provided. + """ + coefficients = { + 'very smooth': 1.5, + 'smooth': 1.1, + 'rough': 0.9, + 'very rough': 0.9, + } + if surface_roughness not in coefficients: + raise ValueError( + f'Unknown surface roughness: {surface_roughness}. ' + f'Must be one of: {list(coefficients.keys())}' + ) + return coefficients[surface_roughness] + + +def tau_Rdi_no_yielding( + cv2: float, + fck: float, + gamma_c: float, + mu_v: float, + sigma_n: float, + kv: float, + rho_i: float, + fyd: float, + kdowel: float, + fcd: float, +) -> float: + """Calculate the shear stress resistance at the interface when yielding is + not ensured at the interface. + + EN 1992-1-1:2022 Eq. (8.77). + + Args: + cv2 (float): Coefficient depending on the roughness of the interface + (unitless, must be non-negative). + fck (float): Concrete compressive resistance in MPa (must be positive, + cannot be zero). + gamma_c (float): Partial safety factor for concrete (unitless, must be + positive, cannot be zero). + mu_v (float): Coefficient mu_v from the Eurocode (unitless, must be + non-negative). + sigma_n (float): Normal stress in the interface in MPa. Sign + convention: positive values represent compressive stress (acting + perpendicular to the interface, compressing the interface), + negative values represent tensile stress (acting perpendicular to + the interface, pulling apart the interface). The value is clamped + to be non-negative (tensile stresses are treated as zero). + kv (float): Coefficient kv from the Eurocode (unitless, must be + non-negative). + rho_i (float): Reinforcement ratio at the interface (unitless, must be + non-negative). + fyd (float): Design yield strength of reinforcement in MPa (must be + positive, cannot be zero). + kdowel (float): Coefficient for dowel action of reinforcement + (unitless, must be non-negative). + fcd (float): Design compressive strength of concrete in MPa (must be + positive, cannot be zero). Calculated according to EN1992-1-1:2023 + Eq. (5.3) as fcd = eta_cc * k_tc * fck / gamma_c. + + Returns: + float: Shear stress resistance tau_Rdi in MPa. + + Raises: + ValueError: If fck, gamma_c, fcd, or fyd is not positive (cannot be + zero), or if cv2, mu_v, kv, rho_i, or kdowel is negative. + """ + if fck <= 0: + raise ValueError(f'fck must be positive (cannot be zero). Got {fck}') + if gamma_c <= 0: + raise ValueError( + f'gamma_c must be positive (cannot be zero). Got {gamma_c}' + ) + if fcd <= 0: + raise ValueError(f'fcd must be positive (cannot be zero). Got {fcd}') + if fyd <= 0: + raise ValueError(f'fyd must be positive (cannot be zero). Got {fyd}') + if cv2 < 0: + raise ValueError(f'cv2 must not be negative. Got {cv2}') + if mu_v < 0: + raise ValueError(f'mu_v must not be negative. Got {mu_v}') + if kv < 0: + raise ValueError(f'kv must not be negative. Got {kv}') + if rho_i < 0: + raise ValueError(f'rho_i must not be negative. Got {rho_i}') + if kdowel < 0: + raise ValueError(f'kdowel must not be negative. Got {kdowel}') + + sigma_n = max(0, sigma_n) + tau_rdi = ( + cv2 * math.sqrt(fck) / gamma_c + + mu_v * sigma_n + + kv * rho_i * fyd * mu_v + + kdowel * rho_i * math.sqrt(fyd * fcd) + ) + return min(tau_rdi, 0.25 * fcd) # Cap tau_Rdi according to the formula + + +def as_min(tmin: float, fctm: float, fyk: float) -> float: + """Calculate the minimum interface reinforcement per unit length along the + edge of composite slabs. + + EN 1992-1-1:2022 Eq. (8.78). + + Args: + tmin (float): Smaller value of the thickness of new and old concrete + layers in mm. + fctm (float): Mean tensile strength of the respective concrete layer in + MPa. + fyk (float): Characteristic yield strength of the reinforcement in MPa. + + Returns: + float: Minimum interface reinforcement per unit length in mm/mm. + + Raises: + ValueError: If any input is negative or zero where it shouldn't be. + """ + if tmin <= 0: + raise ValueError(f'tmin must be positive. Got {tmin}') + if fctm <= 0: + raise ValueError(f'fctm must be positive. Got {fctm}') + if fyk <= 0: + raise ValueError(f'fyk must be positive. Got {fyk}') + + return tmin * fctm / fyk diff --git a/tests/test_ec2_2023/test_ec2_2023_section_8_2_shear.py b/tests/test_ec2_2023/test_ec2_2023_section_8_2_shear.py new file mode 100644 index 00000000..c978f53f --- /dev/null +++ b/tests/test_ec2_2023/test_ec2_2023_section_8_2_shear.py @@ -0,0 +1,2543 @@ +"""Tests for the section EC2 2023 8.2 Shear.""" + +import math + +import pytest + +from structuralcodes.codes.ec2_2023 import _section_8_2_shear + + +@pytest.mark.parametrize( + 'VEd, bw, d, expected', + [ + (100.0, 0.3, 0.5, 100.0 * 1000.0 / (0.3 * 0.9 * 0.5)), + (200.0, 0.5, 0.6, 200.0 * 1000.0 / (0.5 * 0.9 * 0.6)), + ], +) +def test_tao_Ed(VEd, bw, d, expected): + """Test shear_stress_linear_members.""" + assert _section_8_2_shear.tau_Ed(VEd, bw, d) == pytest.approx(expected) + + +@pytest.mark.parametrize( + 'VEd, bw, d', + [ + (100.0, -0.3, 0.5), + (100.0, 0.3, -0.5), + (100.0, -0.3, -0.5), + ], +) +def test_tau_Ed_value_errors(VEd, bw, d): + """Test tao_Ed raises ValueError for negative bw or d.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Ed(VEd, bw, d) + + +@pytest.mark.parametrize( + 'vEd, d, expected', + [ + (100.0, 500.0, 100.0 / (0.9 * 500.0)), + (200.0, 400.0, 200.0 / (0.9 * 400.0)), + (50.0, 250.0, 50.0 / (0.9 * 250.0)), + (0.0, 300.0, 0.0), # Zero shear force + ], +) +def test_tao_Ed_planar_valid(vEd, d, expected): + """Test tao_Ed_planar with valid inputs.""" + assert _section_8_2_shear.tau_Ed_planar(vEd, d) == pytest.approx( + expected, rel=1e-9 + ) + + +@pytest.mark.parametrize( + 'vEd, d', + [ + (100.0, -500.0), + (0.0, -1.0), + (50.0, -0.01), + ], +) +def test_tao_Ed_planar_invalid_depth(vEd, d): + """Test tao_Ed_planar raises ValueError for negative d.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Ed_planar(vEd, d) + + +@pytest.mark.parametrize( + 'f_ck, d_lower, expected', + [ + (30, 20, 36), # f_ck <= 60: min(16 + 20, 40) = 36 + (40, 25, 40), # f_ck <= 60: min(16 + 25, 40) = 40 + (70, 30, 38.0204), # f_ck > 60: min(16 + 30 * (60/70)^2, 40) = 38.0204 + (80, 20, 27.25), # f_ck > 60: min(16 + 20 * (60/80)^2, 40) = 27.25 + ], +) +def test_d_dg(f_ck, d_lower, expected): + """Test the d_dg function with example values.""" + result = _section_8_2_shear.d_dg(f_ck, d_lower) + assert result == pytest.approx(expected, rel=1e-3) + + +@pytest.mark.parametrize( + 'gamma_v, f_ck, f_yd, d, d_lower, expected', + [ + (1.4, 30, 500, 500, 20, 0.51642), + (1.5, 40, 600, 600, 25, 0.48888), + (1.3, 70, 700, 700, 30, 0.62377), + ], +) +def test_tau_rdc_min(gamma_v, f_ck, f_yd, d, d_lower, expected): + """Test the calculate_tau_rdc_min function with example values.""" + d_dg = _section_8_2_shear.d_dg(f_ck, d_lower) + result = _section_8_2_shear.tau_rdc_min(gamma_v, f_ck, f_yd, d, d_dg) + assert result == pytest.approx(expected, rel=1e-4) + + +@pytest.mark.parametrize( + 'vEd_x, vEd_y, expected', + [ + (3.0, 4.0, 5.0), # Pythagorean triplet + (0.0, 4.0, 4.0), # One direction zero + (3.0, 0.0, 3.0), # One direction zero + ], +) +def test_v_Ed(vEd_x, vEd_y, expected): + """Test calculation of design shear force per unit width.""" + assert math.isclose( + _section_8_2_shear.v_Ed(vEd_x, vEd_y), + expected, + rel_tol=1e-9, + ) + + +@pytest.mark.parametrize( + 'dx, dy, vEd_x, vEd_y, expected', + [ + (300, 400, 3.0, 1.0, 300), # Ratio <= 0.5 + (300, 400, 3.0, 3.0, 350), # Ratio between 0.5 and 2 + (300, 400, 1.0, 3.0, 400), # Ratio >= 2 + ], +) +def test_d_eff(dx, dy, vEd_x, vEd_y, expected): + """Test calculation of effective depth based on shear force ratio.""" + assert math.isclose( + _section_8_2_shear.d_eff(dx, dy, vEd_x, vEd_y), expected, rel_tol=1e-9 + ) + + +@pytest.mark.parametrize( + 'dx, dy, vEd_x, vEd_y, expected', + [ + (300, 400, 3.0, 0.0, 300), # alpha_v = 0 + (300, 400, 0.0, 3.0, 400), # alpha_v = 90 degrees + (300, 400, 3.0, 3.0, 350), # alpha_v = 45 degrees + ], +) +def test_ed_eff_with_angle(dx, dy, vEd_x, vEd_y, expected): + """Test calculation of effective depth based on angle alpha_v.""" + assert math.isclose( + _section_8_2_shear.d_eff_angle(dx, dy, vEd_x, vEd_y), + expected, + rel_tol=1e-9, + ) + + +@pytest.mark.parametrize( + 'dx, dy, vEd_x, vEd_y', + [ + (-300, 400, 3.0, 1.0), # Negative dx + (300, -400, 3.0, 1.0), # Negative dy + (-300, -400, 3.0, 1.0), # Both dx and dy negative + ], +) +def test_d_eff_with_angle_value_errors(dx, dy, vEd_x, vEd_y): + """Test d_eff_with_angle raises ValueError for negative dx or dy.""" + with pytest.raises(ValueError): + _section_8_2_shear.d_eff_angle(dx, dy, vEd_x, vEd_y) + + +@pytest.mark.parametrize( + 'gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min, expected', + [ + (1.5, 0.02, 30, 500, 16, 0.3, 0.5468), + (1.4, 0.03, 40, 450, 20, 0.5, 0.82366), + (1.6, 0.025, 35, 600, 18, 0.1, 0.5690), + ], +) +def test_calculate_tau_Rdc( + gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min, expected +): + """Test the calculation of the shear stress resistance.""" + result = _section_8_2_shear.tau_Rdc( + gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min + ) + assert math.isclose( + result, + expected, + rel_tol=1e-3, + ) + + +@pytest.mark.parametrize( + 'f_ck, d_lower', + [ + (-30, 20), + (30, -20), + ], +) +def test_d_dg_value_errors(f_ck, d_lower): + """Test d_dg raises ValueError for negative arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.d_dg(f_ck, d_lower) + + +@pytest.mark.parametrize( + 'gamma_v, f_ck, f_yd, d, d_dg', + [ + (-1.0, 30, 500, 500, 36), + (1.4, -30, 500, 500, 36), + (1.4, 30, -500, 500, 36), + (1.4, 30, 500, -500, 36), + (1.4, 30, 500, 500, -36), + ], +) +def test_tau_rdc_min_value_errors(gamma_v, f_ck, f_yd, d, d_dg): + """Test tau_rdc_min raises ValueError for negative arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_rdc_min(gamma_v, f_ck, f_yd, d, d_dg) + + +@pytest.mark.parametrize( + 'A_sl, b_w, d, expected', + [ + (300, 200, 500, 0.003), + (400, 250, 600, 0.00266667), + (500, 300, 700, 0.00238095), + ], +) +def test_rho_l(A_sl, b_w, d, expected): + """Test the calculation of the reinforcement ratio.""" + assert math.isclose( + _section_8_2_shear.rho_l(A_sl, b_w, d), expected, rel_tol=1e-5 + ) + + +@pytest.mark.parametrize( + 'A_sl, b_w, d', + [ + (-1, 200, 500), # Negative A_sl + (300, -200, 500), # Negative b_w + (300, 200, -500), # Negative d + (0, 200, 500), # Zero A_sl + (300, 0, 500), # Zero b_w + (300, 200, 0), # Zero d + ], +) +def test_rho_l_value_errors(A_sl, b_w, d): + """Test that rho_l raises ValueError for non-positive arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.rho_l(A_sl, b_w, d) + + +@pytest.mark.parametrize( + 'gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min', + [ + (-1.0, 0.02, 30, 500, 16, 0.3), # Negative gamma_v + (1.5, -0.02, 30, 500, 16, 0.3), # Negative rho_l + (1.5, 0.02, -30, 500, 16, 0.3), # Negative f_ck + (1.5, 0.02, 30, -500, 16, 0.3), # Negative d + (1.5, 0.02, 30, 500, -16, 0.3), # Negative d_dg + (0.0, 0.02, 30, 500, 16, 0.3), # Zero gamma_v + (1.5, 0.02, 30, 0.0, 16, 0.3), # Zero d + ], +) +def test_tau_Rdc_value_errors(gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min): + """Test tau_Rdc raises ValueError for negative values or zero gamma_v/d.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rdc(gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min) + + +@pytest.mark.parametrize( + 'gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min, expected', + [ + ( + 1.5, + 0.0, + 30, + 500, + 16, + 0.3, + 0.3, + ), # Zero rho_l - should return tau_rdc_min + ( + 1.5, + 0.02, + 0.0, + 500, + 16, + 0.3, + 0.3, + ), # Zero f_ck - should return tau_rdc_min + ( + 1.5, + 0.02, + 30, + 500, + 0.0, + 0.3, + 0.3, + ), # Zero d_dg - should return tau_rdc_min + ], +) +def test_tau_Rdc_zero_values( + gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min, expected +): + """Test tau_Rdc handles zero values correctly (should not raise errors).""" + result = _section_8_2_shear.tau_Rdc( + gamma_v, rho_l, f_ck, d, d_dg, tau_rdc_min + ) + assert result == pytest.approx(expected, rel=1e-3) + + +@pytest.mark.parametrize( + 'dx, dy, vEd_x, vEd_y', + [ + (-300, 400, 3.0, 1.0), # Negative dx + (300, -400, 3.0, 1.0), # Negative dy + (-300, -400, 3.0, 1.0), # Both dx and dy negative + ], +) +def test_d_eff_value_errors(dx, dy, vEd_x, vEd_y): + """Test d_eff raises ValueError for negative dx or dy.""" + with pytest.raises(ValueError): + _section_8_2_shear.d_eff(dx, dy, vEd_x, vEd_y) + + +@pytest.mark.parametrize( + 'a_cs, d, expected', + [ + (300, 200, 122.4744), + (400, 250, 158.1138), + (500, 300, 193.6491), + ], +) +def test_a_v(a_cs, d, expected): + """Test the a_v.""" + result = _section_8_2_shear.a_v(a_cs, d) + assert math.isclose(result, expected, rel_tol=1e-5) + + +@pytest.mark.parametrize( + 'a_cs, d', + [ + (-300, 200), # Negative a_cs + (300, -200), # Negative d + (0, 200), # Zero a_cs + (300, 0), # Zero d + (-300, -200), # Both negative + (0, 0), # Both zero + ], +) +def test_a_v_value_errors(a_cs, d): + """Test that a_v raises ValueError for non-positive arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.a_v(a_cs, d) + + +@pytest.mark.parametrize( + 'M_Ed, V_Ed, d, expected', + [ + (1000, 100, 500, 10000), + (2000, 200, 600, 10000), + (1500, 150, 700, 10000), + ], +) +def test_calculate_a_cs(M_Ed, V_Ed, d, expected): + """Test the calculation of the effective shear span.""" + result = _section_8_2_shear.a_cs(M_Ed, V_Ed, d) + assert math.isclose(result, expected, rel_tol=1e-5) + + +@pytest.mark.parametrize( + 'M_Ed, V_Ed, d', + [ + (1000, 100, -500), # Negative d + (1000, 100, 0), # Zero d + ], +) +def test_a_cs_value_error_d(M_Ed, V_Ed, d): + """Test a_cs raises ValueError for non-positive d.""" + with pytest.raises(ValueError): + _section_8_2_shear.a_cs(M_Ed, V_Ed, d) + + +@pytest.mark.parametrize( + 'N_Ed, V_Ed, d, a_cs, expected', + [ + (100, 50, 500, 1500, 1.2222), + (200, 100, 600, 1600, 1.25), + (-150, 75, 700, 1700, 0.7254), + ], +) +def test_calculate_k_vp(N_Ed, V_Ed, d, a_cs, expected): + """Test the calculation of the coefficient k_vp.""" + result = _section_8_2_shear.k_vp(N_Ed, V_Ed, d, a_cs) + assert math.isclose(result, expected, rel_tol=1e-3) + + +@pytest.mark.parametrize( + 'N_Ed, V_Ed, d, a_cs', + [ + (100, 50, -500, 1500), # Negative d + (100, 50, 500, -1500), # Negative a_cs + (100, 50, 0, 1500), # Zero d + (100, 50, 500, 0), # Zero a_cs + (100, 50, 0, 0), # Both zero + (100, 50, -500, -1500), # Both negative + ], +) +def test_k_vp_value_errors(N_Ed, V_Ed, d, a_cs): + """Test k_vp raises ValueError for non-positive d or a_cs.""" + with pytest.raises(ValueError): + _section_8_2_shear.k_vp(N_Ed, V_Ed, d, a_cs) + + +@pytest.mark.parametrize( + 'gamma_v, rho_l, f_ck, d, d_dg, expected', + [ + (1.5, 0.02, 30, 500, 16, 0.5468), + (1.4, 0.03, 40, 450, 20, 0.8236), + (1.6, 0.025, 35, 600, 18, 0.5690), + ], +) +def test_calculate_tau_Rdc_0(gamma_v, rho_l, f_ck, d, d_dg, expected): + """Test the sh stress resistance wo/ axial force effects.""" + result = _section_8_2_shear.tau_Rdc_0(gamma_v, rho_l, f_ck, d, d_dg) + assert math.isclose( + result, + expected, + rel_tol=1e-3, + ) + + +@pytest.mark.parametrize( + 'gamma_v, rho_l, f_ck, d, d_dg', + [ + (0, 0.02, 30, 500, 16), # gamma_v zero + (-1, 0.02, 30, 500, 16), # gamma_v negative + (1.5, 0, 30, 500, 16), # rho_l zero + (1.5, -0.01, 30, 500, 16), # rho_l negative + (1.5, 0.02, 0, 500, 16), # f_ck zero + (1.5, 0.02, -30, 500, 16), # f_ck negative + (1.5, 0.02, 30, 0, 16), # d zero + (1.5, 0.02, 30, -500, 16), # d negative + (1.5, 0.02, 30, 500, 0), # d_dg zero + (1.5, 0.02, 30, 500, -16), # d_dg negative + ], +) +def test_tau_Rdc_0_value_errors(gamma_v, rho_l, f_ck, d, d_dg): + """Test tau_Rdc_0 raises ValueError for non-positive arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rdc_0(gamma_v, rho_l, f_ck, d, d_dg) + + +@pytest.mark.parametrize( + 'tau_Rdc_0, k1, sigma_cp, tau_Rdc_max, tau_rdc_min, expected', + [ + (1, 0.5, 0.1, 2, 0.3, 0.95), + (1, 0.6, 0.2, 2, 0.3, 0.88), + (1, 0.4, 0.3, 2, 0.3, 0.88), + (1, 0.5, 0.1, 0.8, 0.3, 0.8), # Limited by tau_Rdc_max + (1, 0.5, 0.5, 2, 0.8, 0.8), # Limited by tau_rdc_min + ], +) +def test_calculate_tau_Rdc_comp( + tau_Rdc_0, k1, sigma_cp, tau_Rdc_max, tau_rdc_min, expected +): + """Test the calculation of the shear considering comp normal forces.""" + assert math.isclose( + _section_8_2_shear.tau_Rdc_comp( + tau_Rdc_0, k1, sigma_cp, tau_Rdc_max, tau_rdc_min + ), + expected, + rel_tol=1e-5, + ) + + +@pytest.mark.parametrize( + 'tau_Rdc_0, k1, sigma_cp, tau_Rdc_max, tau_rdc_min', + [ + (0, 0.5, 0.1, 2, 0.3), # tau_Rdc_0 zero + (-1, 0.5, 0.1, 2, 0.3), # tau_Rdc_0 negative + (1, 0, 0.1, 2, 0.3), # k1 zero + (1, -0.5, 0.1, 2, 0.3), # k1 negative + (1, 0.5, -0.1, 2, 0.3), # sigma_cp negative + (1, 0.5, 0.1, 0, 0.3), # tau_Rdc_max zero + (1, 0.5, 0.1, -2, 0.3), # tau_Rdc_max negative + (1, 0.5, 0.1, 2, 0), # tau_rdc_min zero + (1, 0.5, 0.1, 2, -0.3), # tau_rdc_min negative + ], +) +def test_tau_Rdc_comp_value_errors( + tau_Rdc_0, k1, sigma_cp, tau_Rdc_max, tau_rdc_min +): + """Test tau_Rdc_comp raises ValueError.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rdc_comp( + tau_Rdc_0, k1, sigma_cp, tau_Rdc_max, tau_rdc_min + ) + + +@pytest.mark.parametrize( + 'a_cs_0, e_p, A_c, b_w, z, d, expected', + [ + (1000, 50, 10000, 200, 500, 200, 0.000000429), + (1200, 60, 12000, 250, 600, 200, 0.000000263), + (1100, 55, 11000, 220, 550, 200, 0.0000003396), + ], +) +def test_calculate_k1(a_cs_0, e_p, A_c, b_w, z, d, expected): + """Test the calculation of the factor k1.""" + result = _section_8_2_shear.k1(a_cs_0, e_p, A_c, b_w, z, d) + assert math.isclose(result, expected, rel_tol=1e-3) + + +@pytest.mark.parametrize( + 'a_cs_0, e_p, A_c, b_w, z, d', + [ + (0, 50, 10000, 200, 500, 200), # a_cs_0 zero + (-1000, 50, 10000, 200, 500, 200), # a_cs_0 negative + (1000, -1, 10000, 200, 500, 200), # e_p negative + (1000, 50, 0, 200, 500, 200), # A_c zero + (1000, 50, -10000, 200, 500, 200), # A_c negative + (1000, 50, 10000, 0, 500, 200), # b_w zero + (1000, 50, 10000, -200, 500, 200), # b_w negative + (1000, 50, 10000, 200, 0, 200), # z zero + (1000, 50, 10000, 200, -500, 200), # z negative + (1000, 50, 10000, 200, 500, 0), # d zero + (1000, 50, 10000, 200, 500, -200), # d negative + ], +) +def test_k1_value_errors(a_cs_0, e_p, A_c, b_w, z, d): + """Test k1 raises ValueError for non-positive or negative arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.k1(a_cs_0, e_p, A_c, b_w, z, d) + + +@pytest.mark.parametrize( + 'tau_Rdc_0, a_cs_0, d, expected', + [ + (1, 1000, 500, 2.4132), + (0.9, 900, 450, 2.1719), + (1.1, 1100, 550, 2.6546), + ], +) +def test_calculate_tau_Rdc_max(tau_Rdc_0, a_cs_0, d, expected): + """Test the calculation of the maximum shear stress resistance.""" + result = _section_8_2_shear.tau_Rdc_max(tau_Rdc_0, a_cs_0, d) + assert math.isclose( + result, + expected, + rel_tol=1e-3, + ) + + +@pytest.mark.parametrize( + 'tau_Rdc_0, a_cs_0, d', + [ + (0, 1000, 500), # tau_Rdc_0 zero + (-1, 1000, 500), # tau_Rdc_0 negative + (1, 0, 500), # a_cs_0 zero + (1, -1000, 500), # a_cs_0 negative + (1, 1000, 0), # d zero + (1, 1000, -500), # d negative + (0, 0, 0), # all zero + (-1, -1000, -500), # all negative + ], +) +def test_tau_Rdc_max_value_errors(tau_Rdc_0, a_cs_0, d): + """Test tau_Rdc_max raises ValueError for non-positive arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rdc_max(tau_Rdc_0, a_cs_0, d) + + +@pytest.mark.parametrize( + 'ds, As, dp, Ap, expected', + [ + (500, 2000, 600, 1500, 545.45), + (400, 1000, 450, 500, 418.0), + ], +) +def test_d_eff_p(ds, As, dp, Ap, expected): + """Test the calculation of effective depth.""" + result = _section_8_2_shear.d_eff_p(ds, As, dp, Ap) + assert math.isclose( + result, + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'ds, As, dp, Ap', + [ + (-1, 2000, 600, 1500), # Negative ds + (500, -2000, 600, 1500), # Negative As + (500, 2000, -600, 1500), # Negative dp + (500, 2000, 600, -1500), # Negative Ap + (-1, -2000, -600, -1500), # All negative + (0, 0, 0, 0), # All zero (division by zero) + (500, 0, 600, 0), # As and Ap zero (division by zero) + (0, 2000, 0, 1500), # ds and dp zero (division by zero) + ], +) +def test_d_eff_p_value_errors(ds, As, dp, Ap): + """Test that d_eff_p raises ValueError for negative arguments or + division by zero. + """ + with pytest.raises(ValueError): + _section_8_2_shear.d_eff_p(ds, As, dp, Ap) + + +@pytest.mark.parametrize( + 'ds, As, dp, Ap, bw, d, expected', + [ + (500, 2000, 600, 1500, 300, 545.45, 0.02128), + (400, 1000, 450, 500, 250, 425.00, 0.0138), + ], +) +def test_calculate_reinforcement_ratio(ds, As, dp, Ap, bw, d, expected): + """Test the calculation of reinforcement ratio.""" + result = _section_8_2_shear.rho_l_p(ds, As, dp, Ap, bw, d) + assert math.isclose( + result, + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'ds, As, dp, Ap, bw, d', + [ + (-1, 2000, 600, 1500, 300, 545.45), # Negative ds + (500, -2000, 600, 1500, 300, 545.45), # Negative As + (500, 2000, -600, 1500, 300, 545.45), # Negative dp + (500, 2000, 600, -1500, 300, 545.45), # Negative Ap + (500, 2000, 600, 1500, -300, 545.45), # Negative bw + (500, 2000, 600, 1500, 0, 545.45), # Zero bw + (500, 2000, 600, 1500, 300, -545.45), # Negative d + ], +) +def test_rho_l_p_value_errors(ds, As, dp, Ap, bw, d): + """Test that rho_l_p raises ValueError for neg arguments or zero bw.""" + with pytest.raises(ValueError): + _section_8_2_shear.rho_l_p(ds, As, dp, Ap, bw, d) + + +@pytest.mark.parametrize( + 'vEd_y, vEd_x, rho_l_x, rho_l_y, expected', + [ + (20, 40, 0.005, 0.008, 0.005), # vEd_y/vEd_x <= 0.5 + (40, 20, 0.005, 0.008, 0.008), # vEd_y/vEd_x >= 2 + (30, 40, 0.005, 0.008, 0.00308), # 0.5 < vEd_y/vEd_x < 2 + (20, 0, 0.005, 0.008, 0.008), # vEd_x = 0 (returns rho_l_y) + (25, 50, 0.005, 0.008, 0.005), # Additional test to cover line 598 + ], +) +def test_rho_l_planar(vEd_y, vEd_x, rho_l_x, rho_l_y, expected): + """Test the calculation of reinforcement ratio for planar members.""" + result = _section_8_2_shear.rho_l_planar(vEd_y, vEd_x, rho_l_x, rho_l_y) + assert math.isclose( + result, + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'vEd_y, vEd_x, rho_l_x, rho_l_y', + [ + (-1.0, 40, 0.005, 0.008), # Negative vEd_y + (20, -40, 0.005, 0.008), # Negative vEd_x + (20, 40, -0.005, 0.008), # Negative rho_l_x + (20, 40, 0.005, -0.008), # Negative rho_l_y + ], +) +def test_rho_l_planar_value_errors(vEd_y, vEd_x, rho_l_x, rho_l_y): + """Test that rho_l_planar raises ValueError for negative inputs.""" + with pytest.raises(ValueError): + _section_8_2_shear.rho_l_planar(vEd_y, vEd_x, rho_l_x, rho_l_y) + + +# Tests using pytest +def test_cot_theta_min(): + """Tests the function cot_theta_min with various scenarios.""" + Ac = 100000 # 100,000 mm2 + # No axial force (NEd = 0) + assert _section_8_2_shear.cot_theta_min(0, 100, 10, 400, Ac) == 2.5 + + # Tension case (NEd > 0) + assert _section_8_2_shear.cot_theta_min(10, 100, 10, 400, Ac) == 2.49 + + # Compression case with x < 0.25d (uses interpolation) + # NEd = -100 kN, sigma_c = 1 MPa, interpolation: 2.5 + 1.0/6.0 = 2.666... + assert _section_8_2_shear.cot_theta_min( + -100, 100, 50, 400, Ac + ) == pytest.approx(2.5 + 1.0 / 6.0, rel=1e-9) + + # Compression case with significant stress and x < 0.25d + NEd = -300 # -300 kN (3 MPa stress: 300 * 1000 / 100000 = 3.0 MPa) + assert _section_8_2_shear.cot_theta_min(NEd, 100, 50, 400, Ac) == 3.0 + + # Compression case with very high stress (6 MPa) and x < 0.25d + NEd_high = -600 # -600 kN (6 MPa stress: 600 * 1000 / 100000 = 6.0 MPa) + assert _section_8_2_shear.cot_theta_min(NEd_high, 100, 50, 400, Ac) == 3.0 + + # Compression case with intermediate stress (4.5 MPa) and x < 0.25d + # sigma_c >= 3.0, so returns 3.0 (capped) + NEd_intermediate = -450 # -450 kN (4.5 MPa stress) + assert ( + _section_8_2_shear.cot_theta_min(NEd_intermediate, 100, 50, 400, Ac) + == 3.0 + ) + + # Compression case with significant stress but x >= 0.25d (tension formula) + # sigma_c = 3.0 MPa, x = 120 >= 100, so: 2.5 - 0.1*(-300)/100 = 2.8 + assert _section_8_2_shear.cot_theta_min(NEd, 100, 120, 400, Ac) == 2.8 + + # Compression case with low stress and x < 0.25d (uses interpolation) + NEd_low = -100 # -100 kN (1 MPa stress: 100 * 1000 / 100000 = 1.0 MPa) + assert _section_8_2_shear.cot_theta_min( + NEd_low, 100, 50, 400, Ac + ) == pytest.approx(2.5 + 1.0 / 6.0, rel=1e-9) + + # Test ductility class A (20% reduction) + assert _section_8_2_shear.cot_theta_min( + 0, 100, 10, 400, Ac, apply_ductility_class_a_reduction=True + ) == pytest.approx(2.0, rel=1e-9) + assert _section_8_2_shear.cot_theta_min( + 10, 100, 10, 400, Ac, apply_ductility_class_a_reduction=True + ) == pytest.approx(1.992, rel=1e-9) + assert _section_8_2_shear.cot_theta_min( + NEd_high, 100, 50, 400, Ac, apply_ductility_class_a_reduction=True + ) == pytest.approx(2.4, rel=1e-9) + + +@pytest.mark.parametrize( + 'NEd, VEd, x, d, Ac, expected', + [ + # No axial force cases + (0, 100, 10, 400, 100000, 2.5), + (0, 200, 20, 500, 100000, 2.5), + # Tension cases (NEd > 0) + (10, 100, 10, 400, 100000, 2.49), + (50, 200, 20, 500, 100000, 2.475), + (100, 100, 10, 400, 100000, 2.4), + (200, 100, 10, 400, 100000, 2.3), + (500, 100, 10, 400, 100000, 2.0), # 2.5 - 0.1*500/100 = 2.0 + (1000, 100, 10, 400, 100000, 1.5), # 2.5 - 0.1*1000/100 = 1.5 + # Compression cases with x < 0.25d (uses interpolation) + (-10, 100, 10, 400, 100000, 2.5 + 0.1 / 6.0), # 0.1 MPa + (-50, 200, 20, 500, 100000, 2.5 + 0.5 / 6.0), # 0.5 MPa + (-100, 100, 10, 400, 100000, 2.5 + 1.0 / 6.0), # 1.0 MPa + (-200, 100, 10, 400, 100000, 2.5 + 2.0 / 6.0), # 2.0 MPa + # Compression cases with significant stress and x < 0.25d + (-300, 100, 50, 400, 100000, 3.0), # 3 MPa stress, x < 0.25d + (-400, 200, 80, 500, 100000, 3.0), # 4 MPa stress, x < 0.25d + (-500, 100, 90, 400, 100000, 3.0), # 5 MPa stress, x < 0.25d + # Compression cases with significant stress but x >= 0.25d + # (tension formula) + (-300, 100, 120, 400, 100000, 2.8), # 3 MPa, x >= 0.25d + (-400, 200, 150, 500, 100000, 2.7), # 4 MPa, x >= 0.25d + # Compression cases with low stress and x < 0.25d (uses interpolation) + (-100, 100, 50, 400, 100000, 2.5 + 1.0 / 6.0), # 1 MPa stress + (-200, 200, 80, 500, 100000, 2.5 + 2.0 / 6.0), # 2 MPa stress + ], +) +def test_cot_theta_min_comprehensive(NEd, VEd, x, d, Ac, expected): + """Test cot_theta_min with comprehensive scenarios.""" + result = _section_8_2_shear.cot_theta_min(NEd, VEd, x, d, Ac) + assert result == pytest.approx(expected, rel=1e-9) + + +def test_cot_theta_min_edge_cases(): + """Test cot_theta_min edge cases and boundary conditions.""" + # Test exactly at 3 MPa stress boundary + Ac = 100000 # 100,000 mm2 + NEd_exact = -300 # Exactly 3 MPa: 300 * 1000 / 100000 = 3.0 MPa + assert _section_8_2_shear.cot_theta_min(NEd_exact, 100, 50, 400, Ac) == 3.0 + + # Test just below 3 MPa stress + NEd_below = -299 # 2.99 MPa: 299 * 1000 / 100000 = 2.99 MPa + # sigma_c = 2.99 MPa, x < 0.25d, so: 2.5 + 2.99/6.0 = 2.9983... + assert _section_8_2_shear.cot_theta_min( + NEd_below, 100, 50, 400, Ac + ) == pytest.approx(2.5 + 2.99 / 6.0, rel=1e-9) + + # Test exactly at x = 0.25d boundary (x = 100, not < 100, so tension) + x_exact = 100 # 0.25 * 400 + # sigma_c = 3.0 MPa, x = 100 (not < 100), so: 2.5 - 0.1*(-300)/100 = 2.8 + assert ( + _section_8_2_shear.cot_theta_min(NEd_exact, 100, x_exact, 400, Ac) + == 2.8 + ) + + # Test just above x = 0.25d boundary (tension formula) + x_above = 101 # Just above 0.25 * 400 + assert ( + _section_8_2_shear.cot_theta_min(NEd_exact, 100, x_above, 400, Ac) + == 2.8 + ) + + # Test minimum tension value (1.0) + NEd_high_tension = 1500 # High tension force + result = _section_8_2_shear.cot_theta_min( + NEd_high_tension, 100, 10, 400, Ac + ) + assert result == 1.0 + + # Test with zero VEd when NEd is zero (valid case) + assert _section_8_2_shear.cot_theta_min(0, 0, 10, 400, Ac) == 2.5 + + # Test with zero VEd when NEd is not zero (should raise ValueError) + with pytest.raises(ValueError, match='VEd must not be zero'): + _section_8_2_shear.cot_theta_min(10, 0, 10, 400, Ac) + + +def test_cot_theta_min_ductility_class_a_reduction(): + """Test cot_theta_min ductility class A reduction.""" + Ac = 100000 # 100,000 mm2 + # Test with and without reduction + base_value = _section_8_2_shear.cot_theta_min(0, 100, 10, 400, Ac) + reduced_value = _section_8_2_shear.cot_theta_min( + 0, 100, 10, 400, Ac, apply_ductility_class_a_reduction=True + ) + + assert reduced_value == base_value * 0.8 + assert reduced_value == 2.0 + + +def test_cot_theta_min_interpolation(): + """Test cot_theta_min interpolation for intermediate stress values.""" + Ac = 100000 # 100,000 mm2 + + # Test various stress levels from 0 to 3 MPa and above + test_cases = [ + (0.0, 2.5), # Exactly 0 MPa: 2.5 + 0/6.0 = 2.5 + (1.0, 2.5 + 1.0 / 6.0), # 1 MPa: 2.5 + 1.0/6.0 = 2.666... + (2.0, 2.5 + 2.0 / 6.0), # 2 MPa: 2.5 + 2.0/6.0 = 2.833... + (2.5, 2.5 + 2.5 / 6.0), # 2.5 MPa: 2.5 + 2.5/6.0 = 2.916... + (3.0, 3.0), # Exactly 3 MPa (capped at 3.0) + (4.0, 3.0), # 4 MPa (capped at 3.0) + (5.0, 3.0), # 5 MPa (capped at 3.0) + (6.0, 3.0), # 6 MPa (capped at 3.0) + (7.0, 3.0), # 7 MPa (capped at 3.0) + ] + + for stress_mpa, expected in test_cases: + # Convert stress (MPa) to NEd (kN): sigma_c = abs(NEd) * 1000 / Ac + # So: abs(NEd) = sigma_c * Ac / 1000 + NEd = -stress_mpa * Ac / 1000 # Convert to kN + result = _section_8_2_shear.cot_theta_min(NEd, 100, 50, 400, Ac) + assert result == pytest.approx(expected, rel=1e-3) + + +@pytest.mark.parametrize( + 'NEd, VEd, x, d, Ac', + [ + (0, 100, -1, 400, 100000), # x negative + (0, 100, 10, 0, 100000), # d zero + (0, 100, -1, 0, 100000), # x negative and d zero + (0, 100, 10, -400, 100000), # d negative + (0, 100, -10, -400, 100000), # both negative + (-10, 100, 10, 0, 100000), # d zero with NEd negative + (10, 100, 10, 0, 100000), # d zero with NEd positive + (0, 100, 10, 400, 0), # Ac zero + (0, 100, 10, 400, -100000), # Ac negative + (10, 0, 10, 400, 100000), # VEd zero with NEd positive + (-10, 0, 10, 400, 100000), # VEd zero with NEd negative + ], +) +def test_cot_theta_min_value_error_all_cases(NEd, VEd, x, d, Ac): + """Test cot_theta_min raises ValueError.""" + with pytest.raises(ValueError): + _section_8_2_shear.cot_theta_min(NEd, VEd, x, d, Ac) + + +@pytest.mark.parametrize( + 'NEd, VEd, x, d, Ac', + [ + (0, 100, 10, 0, 100000), # d is zero + (0, 100, -10, 400, 100000), # x is negative + (0, 100, -10, 0, 100000), # x negative and d zero + (0, 100, 10, -400, 100000), # d negative + (0, 100, -10, -400, 100000), # both negative + (0, 100, 10, 400, 0), # Ac zero + (0, 100, 10, 400, -100000), # Ac negative + (10, 0, 10, 400, 100000), # VEd zero with NEd positive + (-10, 0, 10, 400, 100000), # VEd zero with NEd negative + ], +) +def test_cot_theta_min_value_error(NEd, VEd, x, d, Ac): + """Test cot_theta_min raises ValueError for invalid dimensions.""" + with pytest.raises(ValueError): + _section_8_2_shear.cot_theta_min(NEd, VEd, x, d, Ac) + + +def test_tau_Rd_sy(): + """Tests the function tau_Rd_sy.""" + assert _section_8_2_shear.tau_Rd_sy(0.01, 500, 2, 2.5) == 10 + assert _section_8_2_shear.tau_Rd_sy(0.02, 400, 2.5, 3.0) == 20 + # Test boundary cases + # cot_theta = 1 (minimum) + assert _section_8_2_shear.tau_Rd_sy(0.01, 500, 1.0, 2.5) == 5 + # cot_theta = cot_theta_min (maximum) + assert _section_8_2_shear.tau_Rd_sy(0.01, 500, 2.5, 2.5) == 12.5 + # cot_theta_min = 1 (minimum allowed) + assert _section_8_2_shear.tau_Rd_sy(0.01, 500, 1.0, 1.0) == 5 + + +@pytest.mark.parametrize( + 'rho_w, fywd, cot_theta, cot_theta_min', + [ + (-0.01, 500, 2, 2.5), # Negative rho_w + (0.01, -500, 2, 2.5), # Negative fywd + (-0.01, -500, 2, 2.5), # Both negative + (0, 500, 2, 2.5), # Zero rho_w + (0.01, 0, 2, 2.5), # Zero fywd + (0, 0, 2, 2.5), # Both zero + (-0.01, 0, 2, 2.5), # Negative rho_w, zero fywd + (0, -500, 2, 2.5), # Zero rho_w, negative fywd + (0.01, 500, 2, 0), # Zero cot_theta_min + (0.01, 500, 2, -2.5), # Negative cot_theta_min + (0.01, 500, 2, 0.5), # cot_theta_min < 1 + (0.01, 500, 2, 0.9), # cot_theta_min < 1 (edge case) + (0.01, 500, 0.5, 2.5), # cot_theta < 1 + (0.01, 500, 3.0, 2.5), # cot_theta > cot_theta_min + (0.01, 500, 2.6, 2.5), # cot_theta > cot_theta_min (edge case) + ], +) +def test_tau_Rd_sy_value_errors(rho_w, fywd, cot_theta, cot_theta_min): + """Test tau_Rd_sy raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rd_sy(rho_w, fywd, cot_theta, cot_theta_min) + + +def test_rho_w(): + """Tests the function rho_w.""" + assert _section_8_2_shear.rho_w(100, 200, 50) == 0.01 + assert _section_8_2_shear.rho_w(200, 200, 100) == 0.01 + + +@pytest.mark.parametrize( + 'Asw, bw, s', + [ + (-100, 200, 50), # Negative Asw + (100, -200, 50), # Negative bw + (100, 200, -50), # Negative s + (0, 200, 50), # Zero Asw + (100, 0, 50), # Zero bw + (100, 200, 0), # Zero s + (0, 0, 0), # All zero + (-100, -200, -50), # All negative + ], +) +def test_rho_w_value_errors(Asw, bw, s): + """Test that rho_w raises ValueError for non-positive arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.rho_w(Asw, bw, s) + + +def test_sigma_cd(): + """Tests the function sigma_cd.""" + # tan_theta = 1/cot_theta = 1/2 = 0.5 + # sigma_cd = 1 * (2 + 0.5) = 2.5, min(2.5, 0.5 * 20) = 2.5 + assert _section_8_2_shear.sigma_cd(1, 2, 2.5, 0.5, 20) == 2.5 + # Test with cot_theta = cot_theta_min + assert _section_8_2_shear.sigma_cd(1, 2.5, 2.5, 0.5, 20) == 2.9 + # Test with cot_theta = 1 + assert _section_8_2_shear.sigma_cd(1, 1, 2.5, 0.5, 20) == 2.0 + # Test with cot_theta_min = 1 (boundary case) + assert _section_8_2_shear.sigma_cd(1, 1, 1, 0.5, 20) == 2.0 + + +@pytest.mark.parametrize( + 'tau_Ed, cot_theta, cot_theta_min, nu, f_cd', + [ + (-1, 2, 2.5, 0.5, 20), # Negative tau_Ed + (1, 2, 0, 0.5, 20), # Zero cot_theta_min + (1, 2, -2.5, 0.5, 20), # Negative cot_theta_min + (1, 2, 0.5, 0.5, 20), # cot_theta_min < 1 + (1, 2, 0.9, 0.5, 20), # cot_theta_min < 1 (edge case) + (1, 0.5, 2.5, 0.5, 20), # cot_theta < 1 + (1, 3.0, 2.5, 0.5, 20), # cot_theta > cot_theta_min + (1, 2.6, 2.5, 0.5, 20), # cot_theta > cot_theta_min (edge case) + (1, 2, 2.5, -0.5, 20), # Negative nu + (1, 2, 2.5, 0.5, 0), # Zero f_cd + (1, 2, 2.5, 0.5, -20), # Negative f_cd + (-1, 0.5, 0.5, -0.5, -20), # Multiple errors + ], +) +def test_sigma_cd_value_errors(tau_Ed, cot_theta, cot_theta_min, nu, f_cd): + """Test sigma_cd raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.sigma_cd(tau_Ed, cot_theta, cot_theta_min, nu, f_cd) + + +def test_tau_Rd(): + """Tests the function tau_Rd.""" + assert _section_8_2_shear.tau_Rd(0.01, 500, 2, 2.5, 0.5, 50) == 10 + assert ( + _section_8_2_shear.tau_Rd(0.01, 500, 3, 3.0, 0.5, 20) == 5 + ) # Limited by the compression field + # Test boundary cases + assert _section_8_2_shear.tau_Rd(0.01, 500, 1, 2.5, 0.5, 50) == 5 + assert _section_8_2_shear.tau_Rd(0.01, 500, 2.5, 2.5, 0.5, 50) == 12.5 + + +@pytest.mark.parametrize( + 'rho_w, fywd, cot_theta, cot_theta_min, nu, f_cd', + [ + (-0.01, 500, 2, 2.5, 0.5, 50), # Negative rho_w + (0.01, -500, 2, 2.5, 0.5, 50), # Negative fywd + (0.01, 500, -2, 2.5, 0.5, 50), # Negative cot_theta + (0.01, 500, 2, 0.5, 0.5, 50), # cot_theta_min < 1 + (0.01, 500, 2, 2.5, -0.5, 50), # Negative nu + (0.01, 500, 2, 2.5, 0.5, -50), # Negative f_cd + (-0.01, -500, -2, 0.5, -0.5, -50), # All negative + (0.01, 500, 0.5, 2.5, 0.5, 50), # cot_theta < 1 + (0.01, 500, 3, 2.5, 0.5, 50), # cot_theta > cot_theta_min + ], +) +def test_tau_Rd_value_errors(rho_w, fywd, cot_theta, cot_theta_min, nu, f_cd): + """Test tau_Rd raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rd( + rho_w, fywd, cot_theta, cot_theta_min, nu, f_cd + ) + + +def test_cot_theta_simultaneous(): + """Tests the function cot_theta_simultaneous.""" + # Test basic calculation: sqrt((0.5 * 20) / (0.01 * 500) - 1) = sqrt(1) = 1 + assert ( + _section_8_2_shear.cot_theta_simultaneous(0.5, 20, 0.01, 500, 2) == 1 + ) + # Test: sqrt((0.5 * 40) / (0.01 * 500) - 1) = sqrt(3) ≈ 1.732 + assert _section_8_2_shear.cot_theta_simultaneous( + 0.5, 40, 0.01, 500, 2 + ) == pytest.approx(1.732, rel=1e-3) + # Test clamping to minimum (1.0) + # sqrt((0.5 * 10) / (0.01 * 500) - 1) = sqrt(0) = 0, clamped to 1 + assert ( + _section_8_2_shear.cot_theta_simultaneous(0.5, 10, 0.01, 500, 2.5) + == 1.0 + ) + # Test clamping to cot_theta_min + # sqrt((0.5 * 60) / (0.01 * 500) - 1) = sqrt(5) ≈ 2.236 + assert _section_8_2_shear.cot_theta_simultaneous( + 0.5, 60, 0.01, 500, 2.5 + ) == pytest.approx(2.236, rel=1e-3) + + # Test case where expression under square root is negative + # (0.5 * 8) / (0.01 * 500) - 1 = 4 / 5 - 1 = -0.2 < 0 + with pytest.raises(ValueError, match='Expression under square root'): + _section_8_2_shear.cot_theta_simultaneous(0.5, 8, 0.01, 500, 2.5) + + +@pytest.mark.parametrize( + 'nu, f_cd, rho_w, fywd, cot_theta_min', + [ + (-0.1, 20, 0.01, 500, 2), # Negative nu + (0.5, -20, 0.01, 500, 2), # Negative f_cd + (0.5, 20, -0.01, 500, 2), # Negative rho_w + (0.5, 20, 0.01, -500, 2), # Negative fywd + (-0.5, -20, -0.01, -500, 2), # All negative + (0.5, 20, 0.01, 500, 0.5), # cot_theta_min < 1 + (0.5, 20, 0, 500, 2), # rho_w is zero + (0.5, 20, 0.01, 0, 2), # fywd is zero + (0.5, 8, 0.01, 500, 2), # Expression under square root is negative + ], +) +def test_cot_theta_simultaneous_value_errors( + nu, f_cd, rho_w, fywd, cot_theta_min +): + """Test cot_theta_simultaneous raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.cot_theta_simultaneous( + nu, f_cd, rho_w, fywd, cot_theta_min + ) + + +@pytest.mark.parametrize( + 'Ftd, Es, Ast, expected', [(500, 210000, 1000, 0.002380952380952381)] +) +def test_epsilon_xt(Ftd, Es, Ast, expected): + """Test εxt calculation.""" + assert _section_8_2_shear.epsilon_xt(Ftd, Es, Ast) == pytest.approx( + expected + ) + + +@pytest.mark.parametrize( + 'Ftd, Es, Ast', + [ + (500, -210000, 1000), # Negative Es + (500, 210000, -1000), # Negative Ast + (500, -210000, -1000), # Both negative + (500, 0, 1000), # Es is zero + (500, 210000, 0), # Ast is zero + ], +) +def test_epsilon_xt_value_errors(Ftd, Es, Ast): + """Test epsilon_xt raises ValueError for negative or zero Es or Ast.""" + with pytest.raises(ValueError): + _section_8_2_shear.epsilon_xt(Ftd, Es, Ast) + + +@pytest.mark.parametrize( + 'Fcd, Ec, Acc, expected', [(500, 30000, 1000, 0.016666666666666666)] +) +def test_epsilon_xc_compression(Fcd, Ec, Acc, expected): + """Test εxc calculation for compression.""" + assert _section_8_2_shear.epsilon_xc_comp(Fcd, Ec, Acc) == pytest.approx( + expected, rel=10e-3 + ) + + +@pytest.mark.parametrize( + 'Fcd, Ec, Acc', + [ + (500, -30000, 1000), # Negative Ec + (500, 30000, -1000), # Negative Acc + (500, -30000, -1000), # Both negative + (500, 0, 1000), # Ec is zero + (500, 30000, 0), # Acc is zero + ], +) +def test_epsilon_xc_comp_value_errors(Fcd, Ec, Acc): + """Test epsilon_xc_comp raises ValueError for negative or zero Ec or + Acc. + """ + with pytest.raises(ValueError): + _section_8_2_shear.epsilon_xc_comp(Fcd, Ec, Acc) + + +@pytest.mark.parametrize( + 'Fcd, Es, Asc, expected', [(500, 210000, 1000, 0.002380952380952381)] +) +def test_epsilon_xc_tension(Fcd, Es, Asc, expected): + """Test εxc calculation for tension.""" + assert _section_8_2_shear.epsilon_xc_tens(Fcd, Es, Asc) == pytest.approx( + expected, rel=10e-3 + ) + + +@pytest.mark.parametrize( + 'Fcd, Es, Asc', + [ + (500, -210000, 1000), # Negative Es + (500, 210000, -1000), # Negative Asc + (500, -210000, -1000), # Both negative + (500, 0, 1000), # Es is zero + (500, 210000, 0), # Asc is zero + ], +) +def test_epsilon_xc_tens_value_errors(Fcd, Es, Asc): + """Test epsilon_xc_tens raises ValueError for negative or zero Es or + Asc. + """ + with pytest.raises(ValueError): + _section_8_2_shear.epsilon_xc_tens(Fcd, Es, Asc) + + +@pytest.mark.parametrize( + 'epsilon_xt, epsilon_xc, expected', [(0.002, 0.001, 0.0015)] +) +def test_epsilon_x(epsilon_xt, epsilon_xc, expected): + """Test εx calculation.""" + assert _section_8_2_shear.epsilon_x( + epsilon_xt, epsilon_xc + ) == pytest.approx(expected) + + +@pytest.mark.parametrize( + 'epsilon_x, cot_theta, cot_theta_min, expected', + [(0.001, 2, 2.5, 0.5025)], +) +def test_nu(epsilon_x, cot_theta, cot_theta_min, expected): + """Test ν calculation.""" + assert _section_8_2_shear.nu( + epsilon_x, cot_theta, cot_theta_min + ) == pytest.approx(expected, rel=10e-3) + # Test boundary cases + # For cot_theta = 1: nu = 1 / (1.0 + 110 * (0.001 + 0.002 * 1)) ≈ 0.7519 + assert _section_8_2_shear.nu(0.001, 1, 2.5) == pytest.approx( + 0.7519, rel=10e-3 + ) + # For cot_theta = 2.5: nu = 1 / (1.0 + 110 * (0.001 + 0.002 * 6.25)) + # ≈ 0.4024 + assert _section_8_2_shear.nu(0.001, 2.5, 2.5) == pytest.approx( + 0.4024, rel=10e-3 + ) + + +@pytest.mark.parametrize( + 'epsilon_x, cot_theta, cot_theta_min', + [ + (-0.001, 2, 2.5), # Negative epsilon_x + (-1.0, 2, 2.5), # Negative epsilon_x + (-0.5, 2, 2.5), # Negative epsilon_x + (0.001, 2, 0.5), # cot_theta_min < 1 + (0.001, 0.5, 2.5), # cot_theta < 1 + (0.001, 3, 2.5), # cot_theta > cot_theta_min + ], +) +def test_nu_value_errors(epsilon_x, cot_theta, cot_theta_min): + """Test nu raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.nu(epsilon_x, cot_theta, cot_theta_min) + + +@pytest.mark.parametrize( + 'VEd, cot_theta, cot_theta_min, expected', + [ + (100, 2, 2.5, 200), + (150, 1.5, 2.5, 225), + ], +) +def test_calculate_nv(VEd, cot_theta, cot_theta_min, expected): + """Test calculate_nv function with various inputs.""" + assert _section_8_2_shear.Nvd( + VEd, cot_theta, cot_theta_min + ) == pytest.approx(expected, rel=1e-6) + # Test boundary cases + assert _section_8_2_shear.Nvd(100, 1, 2.5) == pytest.approx(100, rel=1e-6) + assert _section_8_2_shear.Nvd(100, 2.5, 2.5) == pytest.approx( + 250, rel=1e-6 + ) + + +@pytest.mark.parametrize( + 'VEd, cot_theta, cot_theta_min', + [ + (100, 2, 0.5), # cot_theta_min < 1 + (100, 0.5, 2.5), # cot_theta < 1 + (100, 3, 2.5), # cot_theta > cot_theta_min + ], +) +def test_Nvd_value_errors(VEd, cot_theta, cot_theta_min): + """Test Nvd raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.Nvd(VEd, cot_theta, cot_theta_min) + + +@pytest.mark.parametrize( + 'MEd, z, NVd, NE, expected', + [ + (200, 400, 100, 50, 575.0), + (300, 600, 150, 100, 625.0), + ], +) +def test_calculate_ftd(MEd, z, NVd, NE, expected): + """Test calculate_ftd function with various inputs.""" + assert _section_8_2_shear.Ftd(MEd, z, NVd, NE) == pytest.approx( + expected, rel=1e-6 + ) + + +@pytest.mark.parametrize( + 'MEd, z, NVd, NE, expected', + [ + (200, 400, 100, 50, 425.0), + (300, 600, 150, 100, 375.0), + ], +) +def test_calculate_fcd(MEd, z, NVd, NE, expected): + """Test calculate_fcd function with various inputs.""" + assert _section_8_2_shear.Fcd(MEd, z, NVd, NE) == pytest.approx( + expected, rel=1e-6 + ) + + +@pytest.mark.parametrize( + 'MEd, z, NVd, NE', + [ + (200, 0, 100, 50), # z is zero + (200, -400, 100, 50), # z is negative + ], +) +def test_Ftd_value_errors(MEd, z, NVd, NE): + """Test Ftd raises ValueError for invalid z.""" + with pytest.raises(ValueError): + _section_8_2_shear.Ftd(MEd, z, NVd, NE) + + +@pytest.mark.parametrize( + 'MEd, z, NVd, NE', + [ + (200, 0, 100, 50), # z is zero + (200, -400, 100, 50), # z is negative + ], +) +def test_Fcd_value_errors(MEd, z, NVd, NE): + """Test Fcd raises ValueError for invalid z.""" + with pytest.raises(ValueError): + _section_8_2_shear.Fcd(MEd, z, NVd, NE) + + +@pytest.mark.parametrize( + 'MEd_max, z, NEd, expected', + [ + (200, 400, 50, 525.0), # 200*1000/400 + 50/2 = 500 + 25 = 525 + (300, 600, 100, 550.0), # 300*1000/600 + 100/2 = 500 + 50 = 550 + ], +) +def test_Ftd_max(MEd_max, z, NEd, expected): + """Test Ftd_max calculation for direct intermediate support or concentrated + loads. + """ + assert _section_8_2_shear.Ftd_max(MEd_max, z, NEd) == pytest.approx( + expected, rel=1e-6 + ) + + +@pytest.mark.parametrize( + 'MEd_max, z, NEd', + [ + (200, 0, 50), # z is zero + (200, -400, 50), # z is negative + ], +) +def test_Ftd_max_value_errors(MEd_max, z, NEd): + """Test Ftd_max raises ValueError for invalid z.""" + with pytest.raises(ValueError): + _section_8_2_shear.Ftd_max(MEd_max, z, NEd) + + +@pytest.mark.parametrize( + 'duct_material, is_grouted, wall_thickness, duct_diameter, expected', + [ + ('steel', True, 3.0, 50.0, 0.5), # Grouted steel ducts + ('plastic', True, 1.0, 50.0, 0.8), # Grouted plastic thin ducts + ('plastic', True, 5.0, 50.0, 1.2), # Grouted plastic thick ducts + ('plastic', False, 1.0, 50.0, 1.2), # Non-grouted plastic ducts + ], +) +def test_calculate_k_duct( + duct_material, is_grouted, wall_thickness, duct_diameter, expected +): + """Test calculate_k_duct with various inputs.""" + assert ( + _section_8_2_shear.k_duct( + duct_material, is_grouted, wall_thickness, duct_diameter + ) + == expected + ) + + +@pytest.mark.parametrize( + 'duct_material, is_grouted, wall_thickness, duct_diameter', + [ + ('steel', True, -1.0, 50.0), # Negative wall_thickness + ('steel', True, 2.0, -50.0), # Negative duct_diameter + ('plastic', False, -0.5, 40.0), # Negative wall_thickness + ('plastic', False, 1.0, -40.0), # Negative duct_diameter + ('steel', True, 0, 50.0), # Zero wall_thickness + ('steel', True, 2.0, 0), # Zero duct_diameter + ], +) +def test_k_duct_negative_dimensions( + duct_material, is_grouted, wall_thickness, duct_diameter +): + """Test k_duct raises ValueError for non-positive dimensions.""" + with pytest.raises(ValueError): + _section_8_2_shear.k_duct( + duct_material, is_grouted, wall_thickness, duct_diameter + ) + + +@pytest.mark.parametrize( + 'duct_material, is_grouted, wall_thickness, duct_diameter', + [ + ('aluminum', True, 2.0, 50.0), # Invalid material + ('concrete', False, 1.0, 40.0), # Invalid material + ('', True, 1.0, 40.0), # Empty string + (None, True, 1.0, 40.0), # None as material + (123, True, 1.0, 40.0), # Non-string type + ], +) +def test_k_duct_invalid_material( + duct_material, is_grouted, wall_thickness, duct_diameter +): + """Test k_duct raises ValueError for invalid duct_material.""" + with pytest.raises(ValueError): + _section_8_2_shear.k_duct( + duct_material, is_grouted, wall_thickness, duct_diameter + ) + + +@pytest.mark.parametrize( + 'bw, duct_diameters, k_duct, expected', + [ + (800, [50, 40], 0.5, 755.0), # Grouted steel ducts + (800, [50, 40], 0.8, 728.0), # Grouted plastic thin ducts + (800, [50, 40], 1.2, 692.0), # Non-grouted or thick plastic ducts + ], +) +def test_calculate_nominal_web_width(bw, duct_diameters, k_duct, expected): + """Test calculate_nominal_web_width with various inputs.""" + assert _section_8_2_shear.bw_nom( + bw, duct_diameters, k_duct + ) == pytest.approx(expected, rel=1e-6) + + +@pytest.mark.parametrize( + 'bw, duct_diameters, k_duct', + [ + (-800, [50, 40], 0.5), # Negative bw + (800, [-50, 40], 0.5), # Negative duct diameter + (800, [50, -40], 0.5), # Negative duct diameter + (800, [-50, -40], 0.5), # All negative duct diameters + (800, [800 / 8 + 1], 0.5), # Sum of duct diameters exceeds bw/8 + (800, [100, 50, 50, 50, 50, 50, 50, 50, 50], 0.5), # Sum exceeds bw/8 + ], +) +def test_bw_nom_value_errors(bw, duct_diameters, k_duct): + """Test bw_nom raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.bw_nom(bw, duct_diameters, k_duct) + + +@pytest.mark.parametrize( + ( + 'nu, f_cd, cot_theta, cot_theta_min, cot_beta_incl, rho_w, f_ywd, ' + 'expected' + ), + [ + (0.6, 30, 1, 2.5, 1.5, 0.01, 500, 3), + (0.5, 40, 1.5, 2.5, 1, 0.02, 400, 9.2307), + ], +) +def test_tau_rd( + nu, f_cd, cot_theta, cot_theta_min, cot_beta_incl, rho_w, f_ywd, expected +): + """Test calculation of enhanced shear stress resistance τRd.""" + assert math.isclose( + _section_8_2_shear.tau_rd( + nu, f_cd, cot_theta, cot_theta_min, cot_beta_incl, rho_w, f_ywd + ), + expected, + rel_tol=1e-2, + ) + assert math.isclose( + _section_8_2_shear.tau_rd(0.6, 30, 1, 2.5, 1.5, 0.01, 500), + 3, + rel_tol=1e-2, + ) + assert math.isclose( + _section_8_2_shear.tau_rd(0.6, 30, 2.5, 2.5, 1.5, 0.01, 500), + 6.207, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'nu, f_cd, cot_theta, cot_theta_min, cot_beta_incl, rho_w, f_ywd', + [ + (-0.1, 30, 1, 2.5, 1.5, 0.01, 500), # Negative nu + (0.6, -30, 1, 2.5, 1.5, 0.01, 500), # Negative f_cd + (0.6, 0, 1, 2.5, 1.5, 0.01, 500), # Zero f_cd + (0.6, 30, 1, 2.5, 1.5, -0.01, 500), # Negative rho_w + (0.6, 30, 1, 2.5, 1.5, 0.01, -500), # Negative f_ywd + (0.6, 30, 1, 2.5, 1.5, 0.01, 0), # Zero f_ywd + (0.6, -30, 1, 2.5, 1.5, 0.01, -500), # Both f_cd and f_ywd negative + (0.6, 30, 1, 0.5, 1.5, 0.01, 500), # cot_theta_min < 1 + (0.6, 30, 0.5, 2.5, 1.5, 0.01, 500), # cot_theta < 1 + (0.6, 30, 3, 2.5, 1.5, 0.01, 500), # cot_theta > cot_theta_min + ], +) +def test_tau_rd_value_errors( + nu, f_cd, cot_theta, cot_theta_min, cot_beta_incl, rho_w, f_ywd +): + """Test tau_rd raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_rd( + nu, + f_cd, + cot_theta, + cot_theta_min, + cot_beta_incl, + rho_w, + f_ywd, + ) + + +@pytest.mark.parametrize( + 'cot_beta_incl, cot_theta_min, expected', + [ + (1.0, 2.5, 2.414213562373095), # sqrt(2) ≈ 1.414 + (1.5, 2.5, 2.5), # Capped at cot_theta_min + (2.0, 3.0, 3.0), # 2.0 + sqrt(5) ≈ 4.236, capped at 3.0 + (0.5, 2.0, 1.618033988749895), # sqrt(1.25) ≈ 1.118 + ], +) +def test_cot_theta_max_shear_constant_nu( + cot_beta_incl, cot_theta_min, expected +): + """Test calculation of optimum cot_theta for constant nu.""" + result = _section_8_2_shear.cot_theta_max_shear_constant_nu( + cot_beta_incl, cot_theta_min + ) + assert math.isclose(result, expected, rel_tol=1e-2) + + +@pytest.mark.parametrize( + 'a, z, cot_theta_min, expected', + [ + (1000, 500, 2.5, 2.5), # 1.3 * 1000 / 500 = 2.6, capped at 2.5 + (1500, 500, 3.0, 3.0), # 1.3 * 1500 / 500 = 3.9, capped at 3.0 + (1000, 1000, 3.0, 1.3), # 1.3 * 1000 / 1000 = 1.3 + (500, 500, 2.5, 1.3), # 1.3 * 500 / 500 = 1.3 + ], +) +def test_cot_theta_max_shear_variable_nu(a, z, cot_theta_min, expected): + """Test calculation of optimum cot_theta for variable nu.""" + result = _section_8_2_shear.cot_theta_max_shear_variable_nu( + a, z, cot_theta_min + ) + assert math.isclose(result, expected, rel_tol=1e-2) + + +@pytest.mark.parametrize( + 'cot_beta_incl, cot_theta_min', + [ + (1.0, 0.5), # cot_theta_min < 1 + (-10.0, 1.0), # Calculated cot_theta < 1 + ], +) +def test_cot_theta_max_shear_constant_nu_value_errors( + cot_beta_incl, cot_theta_min +): + """Test cot_theta_max_shear_constant_nu raises ValueError for invalid + arguments. + """ + with pytest.raises(ValueError): + _section_8_2_shear.cot_theta_max_shear_constant_nu( + cot_beta_incl, cot_theta_min + ) + + +@pytest.mark.parametrize( + 'a, z, cot_theta_min', + [ + (0, 500, 2.5), # a <= 0 + (-1000, 500, 2.5), # a < 0 + (1000, 0, 2.5), # z <= 0 + (1000, -500, 2.5), # z < 0 + (1000, 500, 0.5), # cot_theta_min < 1 + ( + 100, + 10000, + 1.0, + ), # Calculated cot_theta < 1 (1.3 * 100 / 10000 = 0.013) + ], +) +def test_cot_theta_max_shear_variable_nu_value_errors(a, z, cot_theta_min): + """Test cot_theta_max_shear_variable_nu raises ValueError for invalid + arguments. + """ + with pytest.raises(ValueError): + _section_8_2_shear.cot_theta_max_shear_variable_nu(a, z, cot_theta_min) + + +@pytest.mark.parametrize( + 'e_s, eps_x, f_ywd, cot_theta, cot_theta_min, expected', + [ + (200000, 0.001, 500, 2, 2.5, 500), + (210000, 0.002, 450, 1, 2.5, 420), + (200000, 0.001, 500, 2.5, 2.5, 500), # cot_theta = cot_theta_min + ], +) +def test_sigma_swd(e_s, eps_x, f_ywd, cot_theta, cot_theta_min, expected): + """Test calculation of stress σswd in shear reinforcement.""" + assert math.isclose( + _section_8_2_shear.sigma_swd( + e_s, eps_x, f_ywd, cot_theta, cot_theta_min + ), + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'Es, eps_x, f_ywd, cot_theta, cot_theta_min', + [ + (-200000, 0.001, 500, 2, 2.5), # Negative Es + (0, 0.001, 500, 2, 2.5), # Zero Es + (200000, 0.001, -500, 2, 2.5), # Negative f_ywd + (200000, 0.001, 0, 2, 2.5), # Zero f_ywd + (-200000, 0.001, -500, 2, 2.5), # Both negative + (200000, 0.001, 500, 2, 0.5), # cot_theta_min < 1 + (200000, 0.001, 500, 0.5, 2.5), # cot_theta < 1 + (200000, 0.001, 500, 3, 2.5), # cot_theta > cot_theta_min + ], +) +def test_sigma_swd_value_errors(Es, eps_x, f_ywd, cot_theta, cot_theta_min): + """Test sigma_swd raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.sigma_swd( + Es, eps_x, f_ywd, cot_theta, cot_theta_min + ) + + +@pytest.mark.parametrize( + 'tau_ed, rho_w, f_ywd, cot_theta, cot_theta_min, z, b_w, a, x, expected', + [ + (0.5, 0.01, 500, 1, 2.5, 300, 200, 500, 250, 0), + (0.4, 0.02, 400, 1.5, 2.5, 350, 250, 600, 200, -101.5), + ( + 0.5, + 0.01, + 500, + 2.5, + 2.5, + 300, + 200, + 500, + 250, + 0, + ), # cot_theta = cot_theta_min + ], +) +def test_delta_m_ed( + tau_ed, rho_w, f_ywd, cot_theta, cot_theta_min, z, b_w, a, x, expected +): + """Test calculation of additional moment ΔMEd.""" + assert math.isclose( + _section_8_2_shear.delta_MEd( + tau_ed, rho_w, f_ywd, cot_theta, cot_theta_min, z, b_w, a, x + ), + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'tau_ed, rho_w, f_ywd, cot_theta, cot_theta_min, z, b_w, a, x', + [ + (-0.5, 0.01, 500, 1, 2.5, 300, 200, 500, 250), # Negative tau_ed + (0.5, 0.01, 0, 1, 2.5, 300, 200, 500, 250), # Zero f_ywd + (0.5, 0.01, -500, 1, 2.5, 300, 200, 500, 250), # Negative f_ywd + (0.5, 0.01, 500, 1, 0.5, 300, 200, 500, 250), # cot_theta_min < 1 + (0.5, 0.01, 500, 0.5, 2.5, 300, 200, 500, 250), # cot_theta < 1 + ( + 0.5, + 0.01, + 500, + 3, + 2.5, + 300, + 200, + 500, + 250, + ), # cot_theta > cot_theta_min + (0.5, 0.01, 500, 1, 2.5, 0, 200, 500, 250), # Zero z + (0.5, 0.01, 500, 1, 2.5, -300, 200, 500, 250), # Negative z + (0.5, 0.01, 500, 1, 2.5, 300, 0, 500, 250), # Zero b_w + (0.5, 0.01, 500, 1, 2.5, 300, -200, 500, 250), # Negative b_w + (0.5, 0.01, 500, 1, 2.5, 300, 200, -500, 250), # Negative a + (0.5, 0.01, 500, 1, 2.5, 300, 200, 500, -250), # Negative x + ], +) +def test_delta_MEd_value_errors( + tau_ed, rho_w, f_ywd, cot_theta, cot_theta_min, z, b_w, a, x +): + """Test delta_MEd raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.delta_MEd( + tau_ed, rho_w, f_ywd, cot_theta, cot_theta_min, z, b_w, a, x + ) + + +@pytest.mark.parametrize( + 'cot_theta, alpha_w, cot_theta_min, expected', + [ + (1, 45, 2.5, 1.0), # tan(45/2) ≈ 0.414, 1.0 is within range + (2, 60, 3.0, 2.0), # tan(60/2) ≈ 0.577, so 2.0 is valid + (0.5, 60, 3.0, 0.5773502691896257), # Clamped to tan(30) + (4, 60, 3.0, 3.0), # Clamped to cot_theta_min + ], +) +def test_cot_theta_inclined(cot_theta, alpha_w, cot_theta_min, expected): + """Test calculation and validation of cot_theta for inclined + reinforcement (Eq. 8.58). + """ + result = _section_8_2_shear.cot_theta_inclined( + cot_theta, alpha_w, cot_theta_min + ) + assert math.isclose(result, expected, rel_tol=1e-2) + + +@pytest.mark.parametrize( + 'cot_theta, alpha_w, cot_theta_min', + [ + (1, 44, 2.5), # alpha_w < 45 + (1, 100, 2.5), # alpha_w > 90 + (1, 60, 0.5), # cot_theta_min < 1 + ], +) +def test_cot_theta_inclined_value_errors(cot_theta, alpha_w, cot_theta_min): + """Test cot_theta_inclined raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.cot_theta_inclined( + cot_theta, alpha_w, cot_theta_min + ) + + +@pytest.mark.parametrize( + 'rho_w, f_ywd, cot_theta, alpha_w, cot_theta_min, expected', + [ + (0.01, 500, 1, 45, 3.0, 7.071), + (0.02, 450, 2, 60, 3.0, 20.088), + (0.01, 500, 1, 90, 3.0, 5.0), # alpha_w = 90° + ], +) +def test_tau_Rd_sy_inclined( + rho_w, f_ywd, cot_theta, alpha_w, cot_theta_min, expected +): + """Test calculation of shear stress resistance τRd,sy for inclined + reinforcement (Eq. 8.59). + """ + assert math.isclose( + _section_8_2_shear.tau_Rd_sy_inclined( + rho_w, f_ywd, cot_theta, alpha_w, cot_theta_min + ), + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'rho_w, f_ywd, cot_theta, alpha_w, cot_theta_min', + [ + (-0.01, 500, 2, 60, 3.0), # Negative rho_w + (0.01, 0, 2, 60, 3.0), # Zero f_ywd + (0.01, -500, 2, 60, 3.0), # Negative f_ywd + (0.01, 500, 2, 44, 3.0), # alpha_w < 45 + (0.01, 500, 2, 100, 3.0), # alpha_w > 90 + (0.01, 500, 2, 60, 0.5), # cot_theta_min < 1 + ], +) +def test_tau_Rd_sy_inclined_value_errors( + rho_w, f_ywd, cot_theta, alpha_w, cot_theta_min +): + """Test tau_Rd_sy_inclined raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rd_sy_inclined( + rho_w, f_ywd, cot_theta, alpha_w, cot_theta_min + ) + + +@pytest.mark.parametrize( + 'tau_ed, theta, alpha_w, nu, f_cd, cot_theta_min, expected', + [ + (0.5, 1, 45, 0.6, 30, 2, 0.5), + (0.7759, 2, 60, 0.5, 40, 2, 1.50522), + (0.5, 1, 90, 0.6, 30, 2, 1.0), # alpha_w = 90° + ], +) +def test_sigma_cd_inclined( + tau_ed, theta, alpha_w, nu, f_cd, cot_theta_min, expected +): + """Test calculation of compression stress σcd for inclined reinforcement + (Eq. 8.60). + """ + assert math.isclose( + _section_8_2_shear.sigma_cd_inclined( + tau_ed, theta, alpha_w, nu, f_cd, cot_theta_min + ), + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'tau_ed, cot_theta, alpha_w, nu, f_cd, cot_theta_min', + [ + (-0.5, 1, 60, 0.6, 30, 2), # Negative tau_ed + (0.5, 1, 60, -0.6, 30, 2), # Negative nu + (0.5, 1, 60, 0.6, 0, 2), # Zero f_cd + (0.5, 1, 60, 0.6, -30, 2), # Negative f_cd + (0.5, 1, 44, 0.6, 30, 2), # alpha_w < 45 + (0.5, 1, 100, 0.6, 30, 2), # alpha_w > 90 + (0.5, 1, 60, 0.6, 30, 0.5), # cot_theta_min < 1 + ], +) +def test_sigma_cd_inclined_value_errors( + tau_ed, cot_theta, alpha_w, nu, f_cd, cot_theta_min +): + """Test sigma_cd_inclined raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.sigma_cd_inclined( + tau_ed, cot_theta, alpha_w, nu, f_cd, cot_theta_min + ) + + +@pytest.mark.parametrize( + 'v_ed, theta, alpha_w, cot_theta_min, expected', + [ + (100, 0.3, 45, 1, -58.5786), + (80, 0.1, 50, 3, -29.8233), + (100, 1, 90, 2, 100.0), # alpha_w = 90° + ], +) +def test_n_vd(v_ed, theta, alpha_w, cot_theta_min, expected): + """Test calculation of axial tensile force NVd.""" + assert math.isclose( + _section_8_2_shear.NVds_inclined(v_ed, theta, alpha_w, cot_theta_min), + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'VEd, cot_theta, alpha_w, cot_theta_min', + [ + (100, 1, 44, 2), # alpha_w < 45 + (100, 1, 100, 2), # alpha_w > 90 + (100, 1, 60, 0.5), # cot_theta_min < 1 + ], +) +def test_NVds_value_errors(VEd, cot_theta, alpha_w, cot_theta_min): + """Test NVds_inclined raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.NVds_inclined( + VEd, cot_theta, alpha_w, cot_theta_min + ) + + +@pytest.mark.parametrize( + 'nu, f_cd, theta, beta_incl, rho_w, f_ywd, ' + + 'alpha_w, cot_theta_min, expected', + [ + (0.6, 30, 1, 3, 0.01, 500, 45, 2, -3.8578), + (0.5, 40, 0.7, 1, 0.02, 400, 60, 5, 6.9013), + (0.6, 30, 1, 3, 0.01, 500, 90, 2, -3.0), # alpha_w = 90° + ], +) +def test_tau_rd_incl( + nu, f_cd, theta, beta_incl, rho_w, f_ywd, alpha_w, cot_theta_min, expected +): + """Test calculation of shear stress resistance τRd.""" + assert math.isclose( + _section_8_2_shear.tau_Rd_inclined( + nu, f_cd, theta, beta_incl, rho_w, f_ywd, alpha_w, cot_theta_min + ), + expected, + rel_tol=1e-2, + ) + + +@pytest.mark.parametrize( + 'nu, f_cd, cot_theta, cot_beta_incl, rho_w, f_ywd, alpha_w, cot_theta_min', + [ + (-0.1, 30, 1, 3, 0.01, 500, 45, 2), # Negative nu + (0.6, -30, 1, 3, 0.01, 500, 45, 2), # Negative f_cd + (0.6, 0, 1, 3, 0.01, 500, 45, 2), # Zero f_cd + (0.6, 30, 1, 3, -0.01, 500, 45, 2), # Negative rho_w + (0.6, 30, 1, 3, 0.01, -500, 45, 2), # Negative f_ywd + (0.6, 30, 1, 3, 0.01, 0, 45, 2), # Zero f_ywd + (0.6, 30, 1, 3, 0.01, 500, 44, 2), # alpha_w < 45 + (0.6, 30, 1, 3, 0.01, 500, 100, 2), # alpha_w > 90 + (0.6, 30, 1, 3, 0.01, 500, 45, 0.5), # cot_theta_min < 1 + ], +) +def test_tau_rd_incl_value_errors( + nu, f_cd, cot_theta, cot_beta_incl, rho_w, f_ywd, alpha_w, cot_theta_min +): + """Test tau_rd_incl raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rd_inclined( + nu, + f_cd, + cot_theta, + cot_beta_incl, + rho_w, + f_ywd, + alpha_w, + cot_theta_min, + ) + + +@pytest.mark.parametrize( + 'Es, eps_x, cot_theta, alpha_w, f_ywd', + [ + (-200000, 0.001, 2, 45, 500), # Negative Es + (0, 0.001, 2, 45, 500), # Zero Es + (200000, 0.001, 2, 45, -500), # Negative f_ywd + (200000, 0.001, 2, 45, 0), # Zero f_ywd + (200000, 0.001, 2, 44, 500), # alpha_w < 45 + (200000, 0.001, 2, 100, 500), # alpha_w > 90 + (-200000, 0.001, 2, 45, -500), # Both negative + ], +) +def test_sigma_swd_v2_value_errors(Es, eps_x, cot_theta, alpha_w, f_ywd): + """Test sigma_swd_inclined raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.sigma_swd_inclined( + Es, eps_x, cot_theta, alpha_w, f_ywd + ) + + +# Test for sigma_swd_inclined with valid parameters (covers lines 1863-1874) +@pytest.mark.parametrize( + 'Es, eps_x, cot_theta, alpha_w, f_ywd, expected', + [ + # Test with alpha_w = 90.0 to cover lines 1863-1864 (isclose check) + (200000, 0.001, 1.5, 90.0, 500, 500), + # Test with alpha_w = 45.0 to cover line 1866 (else branch) + (200000, 0.001, 1.5, 45.0, 500, 500), + # Test with alpha_w = 60.0 to cover line 1866 (else branch) + (200000, 0.001, 1.5, 60.0, 500, 500), + # Test with alpha_w = 89.0 to cover line 1866 (else branch) + (200000, 0.001, 1.5, 89.0, 500, 500), + # Test with smaller eps_x to get a value less than f_ywd + (200000, 0.0001, 1.5, 90.0, 500, 295.0), + ], +) +def test_sigma_swd_inclined_valid( + Es, eps_x, cot_theta, alpha_w, f_ywd, expected +): + """Test sigma_swd_inclined with valid parameters. + + Covers lines 1863-1874. + """ + result = _section_8_2_shear.sigma_swd_inclined( + Es, eps_x, cot_theta, alpha_w, f_ywd + ) + assert result == pytest.approx(expected, rel=1e-2) + + +@pytest.mark.parametrize( + 'tau_rd, m_ed, m_rd, expected', + [ + (5.0, 2.0, 10.0, 4.0), + (10.0, 3.0, 15.0, 8.0), + ], +) +def test_shear_stress_resistance_reduced(tau_rd, m_ed, m_rd, expected): + """Test calculation of reduced shear stress resistance.""" + assert _section_8_2_shear.tau_Rdm(tau_rd, m_ed, m_rd) == pytest.approx( + expected, rel=10e-2 + ) + + +@pytest.mark.parametrize( + 'tau_rd, m_ed, m_rd', + [ + (-1.0, 2.0, 10.0), # Negative tau_rd + (5.0, -2.0, 10.0), # Negative m_ed + (5.0, 2.0, -10.0), # Negative m_rd + (5.0, 2.0, 0), # Zero m_rd + (-1.0, -2.0, -10.0), # All negative + ], +) +def test_tao_Rd_m_value_errors(tau_rd, m_ed, m_rd): + """Test tau_Rdm raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rdm(tau_rd, m_ed, m_rd) + + +@pytest.mark.parametrize( + 'delta_fd, hf, delta_x, expected', + [ + (100.0, 200.0, 1000.0, 0.5), + (50.0, 150.0, 500.0, 0.666), + ], +) +def test_longitudinal_shear_stress(delta_fd, hf, delta_x, expected): + """Test calculation of longitudinal shear stress.""" + assert _section_8_2_shear.tau_Ed_flange( + delta_fd, hf, delta_x + ) == pytest.approx(expected, rel=10e-2) + + +@pytest.mark.parametrize( + 'delta_fd, hf, delta_x', + [ + (-1.0, 200.0, 1000.0), # Negative delta_fd + (100.0, 0.0, 1000.0), # Zero hf + (100.0, -200.0, 1000.0), # Negative hf + (100.0, 200.0, 0.0), # Zero delta_x + (100.0, 200.0, -1000.0), # Negative delta_x + (-1.0, -200.0, -1000.0), # All negative + ], +) +def test_tao_Ed_flang_value_errors(delta_fd, hf, delta_x): + """Test tao_Ed_flang raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Ed_flange(delta_fd, hf, delta_x) + + +@pytest.mark.parametrize( + 'tau_ed, sf, hf, fyd, cot_theta_f, expected', + [ + (3, 200.0, 200.0, 500.0, 1.0, 240), + (2, 150.0, 150.0, 400.0, 1.5, 75.0), + ], +) +def test_transverse_reinforcement_flange( + tau_ed, sf, hf, fyd, cot_theta_f, expected +): + """Test calculation of transverse reinforcement in the flange.""" + assert _section_8_2_shear.Asf_flange( + tau_ed, sf, hf, fyd, cot_theta_f + ) == pytest.approx(expected, rel=10e-2) + + +@pytest.mark.parametrize( + 'tau_ed, sf, hf, fyd, cot_theta_f', + [ + (-1.0, 150.0, 200.0, 500.0, 1.0), # Negative tau_ed + (2.0, 0.0, 200.0, 500.0, 1.0), # Zero sf + (2.0, -150.0, 200.0, 500.0, 1.0), # Negative sf + (2.0, 150.0, 0.0, 500.0, 1.0), # Zero hf + (2.0, 150.0, -200.0, 500.0, 1.0), # Negative hf + (2.0, 150.0, 200.0, -500.0, 1.0), # Negative fyd + (2.0, 150.0, 200.0, 0, 1.0), # Zero fyd + (2.0, 150.0, 200.0, 500.0, 0.5), # cot_theta_f < 1 + ], +) +def test_Asf_flang_value_errors(tau_ed, sf, hf, fyd, cot_theta_f): + """Test Asf_flange raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.Asf_flange(tau_ed, sf, hf, fyd, cot_theta_f) + + +@pytest.mark.parametrize( + 'tau_ed, cot_theta_f, fcd, nu, expected', + [ + (5, 1.0, 30.0, 0.5, 10), # cot(45°) = 1.0 + (3, 1.732, 25.0, 0.5, 6.9282), # cot(30°) ≈ 1.732 + ], +) +def test_sigma_cd_flange(tau_ed, cot_theta_f, fcd, nu, expected): + """Test check of compression field stress in the flange.""" + assert _section_8_2_shear.sigma_cd_flange( + tau_ed, cot_theta_f, fcd, nu + ) == pytest.approx(expected, rel=10e-2) + + +@pytest.mark.parametrize( + 'tau_ed, Ast_min, sf, hf, fyd, expected', + [ + ( + 1.0, + 100, + 200, + 200, + 500, + True, + ), # tau_ed <= (100/(200*200))*500 = 1.25 + ( + 2.0, + 100, + 200, + 200, + 500, + False, + ), # tau_ed > (100/(200*200))*500 = 1.25 + ( + 1.25, + 100, + 200, + 200, + 500, + True, + ), # tau_ed == (100/(200*200))*500 = 1.25 + ], +) +def test_check_tau_Ed_flange_verification( + tau_ed, Ast_min, sf, hf, fyd, expected +): + """Test check_tau_Ed_flange_verification.""" + assert ( + _section_8_2_shear.check_tau_Ed_flange_verification( + tau_ed, Ast_min, sf, hf, fyd + ) + == expected + ) + + +@pytest.mark.parametrize( + 'tau_ed, Ast_min, sf, hf, fyd', + [ + (-1.0, 100, 200, 200, 500), # Negative tau_ed + (1.0, -100, 200, 200, 500), # Negative Ast_min + (1.0, 100, 0, 200, 500), # Zero sf + (1.0, 100, -200, 200, 500), # Negative sf + (1.0, 100, 200, 0, 500), # Zero hf + (1.0, 100, 200, -200, 500), # Negative hf + (1.0, 100, 200, 200, 0), # Zero fyd + (1.0, 100, 200, 200, -500), # Negative fyd + ], +) +def test_check_tau_Ed_flange_verification_value_errors( + tau_ed, Ast_min, sf, hf, fyd +): + """Test check_tau_Ed_flange_verification raises ValueError.""" + with pytest.raises(ValueError): + _section_8_2_shear.check_tau_Ed_flange_verification( + tau_ed, Ast_min, sf, hf, fyd + ) + + +@pytest.mark.parametrize( + 'tau_ed, cot_theta_f, fcd, nu', + [ + (-1.0, 1.0, 30.0, 0.5), # Negative tau_ed + (5.0, 0.5, 30.0, 0.5), # cot_theta_f < 1 + (5.0, 1.0, -30.0, 0.5), # Negative fcd + (5.0, 1.0, 0, 0.5), # Zero fcd + (5.0, 1.0, 30.0, 0.0), # nu zero + (5.0, 1.0, 30.0, -0.1), # nu negative + (-1.0, 0.5, -30.0, -0.1), # All invalid + ], +) +def test_sigma_cd_flange_value_errors(tau_ed, cot_theta_f, fcd, nu): + """Test sigma_cd_flange raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.sigma_cd_flange(tau_ed, cot_theta_f, fcd, nu) + + +# Valid inputs +@pytest.mark.parametrize( + 'Ftd, Ast, Es, expected', + [ + (100, 500, 200000, 0.001), + (50, 250, 210000, 0.0009523809523809524), + (200, 800, 205000, 0.0012195121951219512), + ], +) +def test_eps_x_flang(Ftd, Ast, Es, expected): + """Test eps_x_flang.""" + assert _section_8_2_shear.eps_x_flang(Ftd, Ast, Es) == pytest.approx( + expected, rel=10e-4 + ) + + +@pytest.mark.parametrize( + 'Ftd, Ast, Es', + [ + (0, 500, 200000), # Ftd zero + (-100, 500, 200000), # Ftd negative + (100, 0, 200000), # Ast zero + (100, -500, 200000), # Ast negative + (100, 500, 0), # Es zero + (100, 500, -200000), # Es negative + (0, 0, 0), # All zero + (-1, -1, -1), # All negative + ], +) +def test_eps_x_flang_value_errors(Ftd, Ast, Es): + """Test eps_x_flang raises ValueError for non-positive arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.eps_x_flang(Ftd, Ast, Es) + + +@pytest.mark.parametrize( + 'VEdi, Ai, expected', + [(1000, 200000, 5), (0, 100000, 0.0), (500, 250000, 2)], +) +def test_calculate_tau_edi(VEdi, Ai, expected): + """Test the basic functionality of calculate_tau_edi.""" + assert _section_8_2_shear.tau_Edi(VEdi, Ai) == pytest.approx( + expected, rel=10e-2 + ) + + +@pytest.mark.parametrize( + 'VEdi, Ai', + [ + (-10, 1000), # Negative VEdi + (100, -1000), # Negative Ai + (100, 0), # Zero Ai + (-5, -1000), # Both negative + ], +) +def test_tau_Edi_value_errors(VEdi, Ai): + """Test tau_Edi raises ValueError for invalid arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Edi(VEdi, Ai) + + +# Tests for calculate_tau_edi_composite +@pytest.mark.parametrize( + 'beta_new, VEd, z, bi, expected', + [ + (0.5, 1000, 200, 1000, 2.5), + (1.0, 1000, 200, 1000, 5.0), + ], +) +def test_calculate_tau_edi_composite(beta_new, VEd, z, bi, expected): + """Test the basic functionality of calculate_tau_edi_composite.""" + assert _section_8_2_shear.tau_Edi_composite( + beta_new, VEd, z, bi + ) == pytest.approx(expected, rel=1e-2) + + +@pytest.mark.parametrize( + 'beta_new, VEd, z, bi', + [ + (-0.1, 1000, 200, 1000), # Negative beta_new + (0.5, -1, 200, 1000), # Negative VEd + (0.5, 1000, 0, 1000), # Zero z + (0.5, 1000, 200, -1), # Negative bi + (0.5, 1000, 200, 0), # Zero bi + ], +) +def test_calculate_tau_edi_composite_errors(beta_new, VEd, z, bi): + """Test that errors are raised for invalid inputs.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Edi_composite(beta_new, VEd, z, bi) + + +@pytest.mark.parametrize( + 'fck, sigma_n, Ai, Asi, fyd, alpha_deg, cv1, mu_v, gamma_c, expected', + [ + (30, 5, 10000, 100, 500, 45, 0.1, 0.2, 1.5, 5.61), + (35, 0, 20000, 200, 550, 90, 0.2, 0.25, 1.5, 2.1638), + (40, 12, 15000, 150, 600, 135, 0.15, 0.3, 1.5, 1.2626), + # alpha_deg = 35 (boundary, covers lines 2225-2228 and 2246) + (30, 5, 10000, 100, 500, 35, 0.1, 0.2, 1.5, 6.0345), + # alpha_deg = 135 (boundary, covers lines 2225-2228 and 2246) + (30, 5, 10000, 100, 500, 135, 0.1, 0.2, 1.5, -1.4633), + ], +) +def test_tau_rdi( + fck, sigma_n, Ai, Asi, fyd, alpha_deg, cv1, mu_v, gamma_c, expected +): + """Test the basic functionality and expected results of tau_Rdi.""" + # Calculate fcd from gamma_c (assuming eta_cc=1, k_tc=1 for test cases) + fcd = fck / gamma_c + assert math.isclose( + _section_8_2_shear.tau_Rdi( + fck, sigma_n, Ai, Asi, fyd, alpha_deg, cv1, mu_v, gamma_c, fcd + ), + expected, + rel_tol=1e-3, + ) + + +@pytest.mark.parametrize( + 'fck, sigma_n, Ai, Asi, fyd, alpha_deg, cv1, mu_v, gamma_c, fcd', + [ + (-30, 5, 10000, 100, 500, 45, 0.1, 0.2, 1.5, 20), # Negative fck + (0, 5, 10000, 100, 500, 45, 0.1, 0.2, 1.5, 0), # Zero fck + (30, 5, -10000, 100, 500, 45, 0.1, 0.2, 1.5, 20), # Negative Ai + (30, 5, 0, 100, 500, 45, 0.1, 0.2, 1.5, 20), # Zero Ai + (30, 5, 10000, -100, 500, 45, 0.1, 0.2, 1.5, 20), # Negative Asi + (30, 5, 10000, 100, -500, 45, 0.1, 0.2, 1.5, 20), # Negative fyd + (30, 5, 10000, 100, 0, 45, 0.1, 0.2, 1.5, 20), # Zero fyd + # alpha_deg validation check (line 2226) + (30, 5, 10000, 100, 500, 20, 0.1, 0.2, 1.5, 20), # alpha_deg < 35 + ( + 30, + 5, + 10000, + 100, + 500, + 34, + 0.1, + 0.2, + 1.5, + 20, + ), # alpha_deg < 35 (edge) + (30, 5, 10000, 100, 500, 140, 0.1, 0.2, 1.5, 20), # alpha_deg > 135 + ( + 30, + 5, + 10000, + 100, + 500, + 136, + 0.1, + 0.2, + 1.5, + 20, + ), # alpha_deg > 135 (edge) + (30, 5, 10000, 100, 500, 45, 0.1, 0.2, -1.5, -20), # Negative gamma_c + (30, 5, 10000, 100, 500, 45, 0.1, 0.2, 0, 0), # Zero gamma_c + (30, 5, 10000, 100, 500, 45, 0.1, 0.2, 1.5, 0), # Zero fcd + (30, 5, 10000, 100, 500, 45, -0.1, 0.2, 1.5, 20), # Negative cv1 + (30, 5, 10000, 100, 500, 45, 0.1, -0.2, 1.5, 20), # Negative mu_v + (-30, 5, -10000, -100, -500, 20, 0.1, 0.2, -1.5, 20), # All negative + ], +) +def test_tau_Rdi_value_errors( + fck, sigma_n, Ai, Asi, fyd, alpha_deg, cv1, mu_v, gamma_c, fcd +): + """Test tau_Rdi raises ValueError.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rdi( + fck, sigma_n, Ai, Asi, fyd, alpha_deg, cv1, mu_v, gamma_c, fcd + ) + + +# Test cv1 function +@pytest.mark.parametrize( + 'surface_roughness, tensile_stress, expected', + [ + ('very smooth', False, 0.01), + ('smooth', True, 0), # Test with tensile stress + ('rough', False, 0.15), + ('very rough', False, 0.19), + ('keyed', False, 0.37), + ], +) +def test_cv1(surface_roughness, tensile_stress, expected): + """Test cv1 coefficient.""" + assert ( + _section_8_2_shear.cv1(surface_roughness, tensile_stress) == expected + ) + + +def test_cv1_value_errors(): + """Test cv1 raises ValueError for unknown surface roughness.""" + with pytest.raises(ValueError): + _section_8_2_shear.cv1('unknown', False) + + +# Test mu_v function +@pytest.mark.parametrize( + 'surface_roughness, expected', + [ + ('very smooth', 0.5), + ('smooth', 0.6), + ('rough', 0.7), + ('very rough', 0.9), + ('keyed', 0.9), + ], +) +def test_mu_v(surface_roughness, expected): + """Test mu_v.""" + assert _section_8_2_shear.mu_v(surface_roughness) == expected + + +def test_mu_v_value_errors(): + """Test mu_v raises ValueError for unknown surface roughness.""" + with pytest.raises(ValueError): + _section_8_2_shear.mu_v('unknown') + + +# Test cv2 function +@pytest.mark.parametrize( + 'surface_roughness, tensile_stress, expected', + [ + ('very smooth', False, 0), + ('smooth', True, 0), # Test with tensile stress + ('rough', False, 0.08), + ('very rough', False, 0.15), + ], +) +def test_cv2(surface_roughness, tensile_stress, expected): + """Test cv2.""" + assert ( + _section_8_2_shear.cv2(surface_roughness, tensile_stress) == expected + ) + + +def test_cv2_value_errors(): + """Test cv2 raises ValueError for unknown surface roughness.""" + with pytest.raises(ValueError): + _section_8_2_shear.cv2('unknown', False) + + +# Test kv function +@pytest.mark.parametrize( + 'surface_roughness, expected', + [ + ('very smooth', 0), + ('smooth', 0.5), + ('rough', 0.5), + ('very rough', 0.5), + ], +) +def test_kv(surface_roughness, expected): + """Test kv.""" + assert _section_8_2_shear.kv(surface_roughness) == expected + + +def test_kv_value_errors(): + """Test kv raises ValueError for unknown surface roughness.""" + with pytest.raises(ValueError): + _section_8_2_shear.kv('unknown') + + +# Test kdowel function +@pytest.mark.parametrize( + 'surface_roughness, expected', + [ + ('very smooth', 1.5), + ('smooth', 1.1), + ('rough', 0.9), + ('very rough', 0.9), + ], +) +def test_kdowel(surface_roughness, expected): + """Test kdowel.""" + assert _section_8_2_shear.kdowel(surface_roughness) == expected + + +def test_kdowel_value_errors(): + """Test kdowel raises ValueError for unknown surface roughness.""" + with pytest.raises(ValueError): + _section_8_2_shear.kdowel('unknown') + + +@pytest.mark.parametrize( + 'cv2, fck, gamma_c, mu_v, sigma_n, kv, rho_i, fyd, kdowel, expected', + [ + (0.1, 30, 1.5, 0.2, 0.3, 0.1, 0.02, 500, 0.0, 0.6251), + ], +) +def test_shear_stress_resistance( + cv2, fck, gamma_c, mu_v, sigma_n, kv, rho_i, fyd, kdowel, expected +): + """Test the shear stress resistance calculation.""" + # Calculate fcd from gamma_c (assuming eta_cc=1, k_tc=1 for test cases) + fcd = fck / gamma_c + result = _section_8_2_shear.tau_Rdi_no_yielding( + cv2, fck, gamma_c, mu_v, sigma_n, kv, rho_i, fyd, kdowel, fcd + ) + assert result == pytest.approx(expected, rel=10e-3) + + +@pytest.mark.parametrize( + 'cv2, fck, gamma_c, mu_v, sigma_n, kv, rho_i, fyd, kdowel, fcd', + [ + (-0.1, 30, 1.5, 0.2, 0.3, 0.1, 0.02, 500, 0.0, 20), # Negative cv2 + (0.1, -30, 1.5, 0.2, 0.3, 0.1, 0.02, 500, 0.0, 20), # Negative fck + (0.1, 0, 1.5, 0.2, 0.3, 0.1, 0.02, 500, 0.0, 0), # Zero fck + ( + 0.1, + 30, + -1.5, + 0.2, + 0.3, + 0.1, + 0.02, + 500, + 0.0, + -20, + ), # Negative gamma_c + (0.1, 30, 0, 0.2, 0.3, 0.1, 0.02, 500, 0.0, 0), # Zero gamma_c + (0.1, 30, 1.5, 0.2, 0.3, 0.1, 0.02, 500, 0.0, 0), # Zero fcd + # fyd validation check (line 2473) + ( + 0.1, + 30, + 1.5, + 0.2, + 0.3, + 0.1, + 0.02, + -500, + 0.0, + 20, + ), # Negative fyd + (0.1, 30, 1.5, 0.2, 0.3, 0.1, 0.02, 0, 0.0, 20), # Zero fyd + (0.1, 30, 1.5, -0.2, 0.3, 0.1, 0.02, 500, 0.0, 20), # Negative mu_v + # kv validation check (line 2479) + ( + 0.1, + 30, + 1.5, + 0.2, + 0.3, + -0.1, + 0.02, + 500, + 0.0, + 20, + ), # Negative kv + # rho_i validation check (line 2481) + ( + 0.1, + 30, + 1.5, + 0.2, + 0.3, + 0.1, + -0.02, + 500, + 0.0, + 20, + ), # Negative rho_i + # kdowel validation check (line 2483) + ( + 0.1, + 30, + 1.5, + 0.2, + 0.3, + 0.1, + 0.02, + 500, + -0.1, + 20, + ), # Negative kdowel + ( + 0.1, + -30, + -1.5, + 0.2, + 0.3, + -0.1, + -0.02, + -500, + -0.1, + 20, + ), # All negative + ], +) +def test_tau_Rdi_no_yielding_value_errors( + cv2, fck, gamma_c, mu_v, sigma_n, kv, rho_i, fyd, kdowel, fcd +): + """Test tau_Rdi_no_yielding raises ValueError.""" + with pytest.raises(ValueError): + _section_8_2_shear.tau_Rdi_no_yielding( + cv2, fck, gamma_c, mu_v, sigma_n, kv, rho_i, fyd, kdowel, fcd + ) + + +@pytest.mark.parametrize( + 'tmin, fctm, fyk, expected', + [ + (200, 2.9, 500, 1.16), + ], +) +def test_min_interface_reinforcement(tmin, fctm, fyk, expected): + """Test the minimum interface reinforcement calculation.""" + result = _section_8_2_shear.as_min(tmin, fctm, fyk) + assert result == pytest.approx(expected, rel=10e-3) + + +@pytest.mark.parametrize( + 'tmin, fctm, fyk', + [ + (0, 2.9, 500), # tmin zero + (-1, 2.9, 500), # tmin negative + (200, 0, 500), # fctm zero + (200, -2.9, 500), # fctm negative + (200, 2.9, 0), # fyk zero + (200, 2.9, -500), # fyk negative + (0, 0, 0), # all zero + (-1, -2.9, -500), # all negative + ], +) +def test_as_min_value_errors(tmin, fctm, fyk): + """Test as_min raises ValueError for non-positive arguments.""" + with pytest.raises(ValueError): + _section_8_2_shear.as_min(tmin, fctm, fyk)