diff --git a/blueprints/checks/eurocode/steel/strength_bending.py b/blueprints/checks/eurocode/steel/strength_bending.py new file mode 100644 index 000000000..9a3f0a626 --- /dev/null +++ b/blueprints/checks/eurocode/steel/strength_bending.py @@ -0,0 +1,284 @@ +"""Module for checking bending moment resistance of steel cross-sections (Eurocode 3).""" + +from dataclasses import dataclass +from typing import ClassVar, Literal + +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.check_result import CheckResult +from blueprints.codes.eurocode.en_1993_1_1_2005 import EN_1993_1_1_2005 +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_6_ultimate_limit_state import ( + formula_6_12, + formula_6_13, + formula_6_14, +) +from blueprints.codes.formula import Formula +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection +from blueprints.type_alias import DIMENSIONLESS, KNM +from blueprints.unit_conversion import KNM_TO_NMM +from blueprints.utils.report import Report + + +@dataclass(frozen=True) +class CheckStrengthBendingClass12: + """Class to perform bending moment resistance check for steel cross-sections, + for cross-section class 1 and 2 only (Eurocode 3). + + Coordinate System: + + z (vertical, usually strong axis) + ↑ + | x (longitudinal beam direction, into screen) + | ↗ + | / + | / + | / + |/ + ←-----O + y (horizontal/side, usually weak axis) + + Parameters + ---------- + steel_cross_section : SteelCrossSection + The steel cross-section to check. + m : KNM, optional + The applied bending moment (positive value), in kNm (default is 0 kNm). + axis : str, optional + Axis of bending: 'My' (bending around y) or 'Mz' (bending around z). Default is 'My'. + gamma_m0 : DIMENSIONLESS, optional + Partial safety factor for resistance of cross-sections, default is 1.0. + section_properties : SectionProperties | None, optional + Pre-calculated section properties. If None, they will be calculated internally. + + Example + ------- + from blueprints.checks.eurocode.steel.bending_moment_strength import CheckStrengthBendingClass12 + from blueprints.materials.steel import SteelMaterial, SteelStrengthClass + from blueprints.structural_sections.steel.standard_profiles.heb import HEB + + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + heb_300_profile = HEB.HEB300.with_corrosion(0) + m = 355 * 1.868 # Applied bending moment in kNm + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = CheckStrengthBendingClass12(heb_300_s355, m, axis='My', gamma_m0=1.0) + calc.report().to_word("bending_moment_strength.docx", language="fy") + """ + + steel_cross_section: SteelCrossSection + m: KNM = 0 + axis: Literal["My", "Mz"] = "My" + gamma_m0: DIMENSIONLESS = 1.0 + section_properties: SectionProperties | None = None + name: str = "Bending moment strength check for steel profiles (Class 1 and 2 only)" + source_docs: ClassVar[list] = [EN_1993_1_1_2005] + + def __post_init__(self) -> None: + """Post-initialization to extract section properties.""" + if self.section_properties is None: + section_properties = self.steel_cross_section.profile.section_properties() + object.__setattr__(self, "section_properties", section_properties) + if self.axis not in ("My", "Mz"): + raise ValueError("Axis must be 'My' or 'Mz'.") + + def calculation_formula(self) -> dict[str, Formula]: + """Calculate bending moment resistance check (Class 1 and 2 only). + + Returns + ------- + dict[str, Formula] + Calculation results keyed by formula number. + """ + f_y = self.steel_cross_section.yield_strength + # For bending about y, the relevant section modulus is sxx; for bending about z, it is syy. + # This is because of the orientation of the axes defined in Blueprints vs. SectionProperties. + w = float(self.section_properties.sxx) if self.axis == "My" else float(self.section_properties.syy) # type: ignore[attr-defined] + + m_ed = abs(self.m) * KNM_TO_NMM # convert kNm to Nmm + m_c_rd = formula_6_13.Form6Dot13MCRdClass1And2(w_pl=w, f_y=f_y, gamma_m0=self.gamma_m0) + check_moment = formula_6_12.Form6Dot12CheckBendingMoment(m_ed=m_ed, m_c_rd=m_c_rd) + + return { + "resistance": m_c_rd, + "check": check_moment, + } + + def result(self) -> CheckResult: + """Calculate result of bending moment resistance (Class 1 and 2). + + Returns + ------- + CheckResult + True if the bending moment check passes, False otherwise. + """ + steps = self.calculation_formula() + provided = abs(self.m) * KNM_TO_NMM + required = steps["resistance"] + return CheckResult.from_comparison(provided=provided, required=float(required)) + + def report(self, n: int = 2) -> Report: + """Returns the report for the bending moment check (Class 1 and 2). + + Parameters + ---------- + n : int, optional + Number of decimal places for numerical values in the report (default is 2). + + Returns + ------- + Report + Report of the bending moment check. + """ + report = Report(f"Check: bending moment steel beam (axis {self.axis})") + if self.m == 0: + report.add_paragraph("No bending moment was applied; therefore, no bending moment check is necessary.") + return report + + calculation = self.calculation_formula() + report.add_paragraph( + rf"Profile {self.steel_cross_section.profile.name} with steel quality {self.steel_cross_section.material.steel_class.name} " + rf"is loaded with a bending moment of {abs(self.m):.{n}f} kNm (axis {self.axis}). " + rf"The resistance is calculated as follows, using cross-section class 1 or 2:" + ) + report.add_formula(calculation["resistance"], n=n) + report.add_paragraph("The unity check is calculated as follows:") + report.add_formula(calculation["check"], n=n) + if self.result().is_ok: + report.add_paragraph("The check for bending moment satisfies the requirements.") + else: + report.add_paragraph("The check for bending moment does NOT satisfy the requirements.") + return report + + +@dataclass(frozen=True) +class CheckStrengthBendingClass3: + """Class to perform bending moment resistance check for steel cross-sections, + for cross-section class 3 only (Eurocode 3). + + Coordinate System: + + z (vertical, usually strong axis) + ↑ + | x (longitudinal beam direction, into screen) + | ↗ + | / + | / + | / + |/ + ←-----O + y (horizontal/side, usually weak axis) + + Parameters + ---------- + steel_cross_section : SteelCrossSection + The steel cross-section to check. + m : KNM, optional + The applied bending moment (positive value), in kNm (default is 0 kNm). + axis : str, optional + Axis of bending: 'My' (bending around y) or 'Mz' (bending around z). Default is 'My'. + gamma_m0 : DIMENSIONLESS, optional + Partial safety factor for resistance of cross-sections, default is 1.0. + section_properties : SectionProperties | None, optional + Pre-calculated section properties. If None, they will be calculated internally. + + Example + ------- + from blueprints.checks.eurocode.steel.strength_bending import CheckStrengthBendingClass3 + from blueprints.materials.steel import SteelMaterial, SteelStrengthClass + from blueprints.structural_sections.steel.standard_profiles.heb import HEB + + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + heb_300_profile = HEB.HEB300.with_corrosion(0) + m = 355 * 1.677 # Applied bending moment in kNm + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = CheckStrengthBendingClass3(heb_300_s355, m, axis='My', gamma_m0=1.0) + calc.report().to_word("bending_moment_strength.docx", language="de") + """ + + steel_cross_section: SteelCrossSection + m: KNM = 0 + axis: Literal["My", "Mz"] = "My" + gamma_m0: DIMENSIONLESS = 1.0 + section_properties: SectionProperties | None = None + name: str = "Bending moment strength check for steel profiles (Class 3 only)" + source_docs: ClassVar[list] = [EN_1993_1_1_2005] + + def __post_init__(self) -> None: + """Post-initialization to extract section properties.""" + if self.section_properties is None: + section_properties = self.steel_cross_section.profile.section_properties() + object.__setattr__(self, "section_properties", section_properties) + if self.axis not in ("My", "Mz"): + raise ValueError("Axis must be 'My' or 'Mz'.") + + def calculation_formula(self) -> dict[str, Formula]: + """Calculate bending moment resistance check (Class 3 only). + + Returns + ------- + dict[str, Formula] + Calculation results keyed by formula number. + """ + f_y = self.steel_cross_section.yield_strength + # For bending about y, the relevant section modulus is sxx; for bending about z, it is syy. + # This is because of the orientation of the axes defined in Blueprints vs. SectionProperties. + if self.axis == "My": + w = min(float(self.section_properties.zxx_plus), float(self.section_properties.zxx_minus)) # type: ignore[attr-defined] + else: + w = min(float(self.section_properties.zyy_plus), float(self.section_properties.zyy_minus)) # type: ignore[attr-defined] + + m_ed = abs(self.m) * KNM_TO_NMM # convert kNm to Nmm + m_c_rd = formula_6_14.Form6Dot14MCRdClass3(w_el_min=w, f_y=f_y, gamma_m0=self.gamma_m0) + check_moment = formula_6_12.Form6Dot12CheckBendingMoment(m_ed=m_ed, m_c_rd=m_c_rd) + + return { + "resistance": m_c_rd, + "check": check_moment, + } + + def result(self) -> CheckResult: + """Calculate result of bending moment resistance (Class 3). + + Returns + ------- + CheckResult + True if the bending moment check passes, False otherwise. + """ + steps = self.calculation_formula() + provided = abs(self.m) * KNM_TO_NMM + required = steps["resistance"] + return CheckResult.from_comparison(provided=provided, required=float(required)) + + def report(self, n: int = 2) -> Report: + """Returns the report for the bending moment check (Class 3). + + Parameters + ---------- + n : int, optional + Number of decimal places for numerical values in the report (default is 2). + + Returns + ------- + Report + Report of the bending moment check. + """ + calculation = self.calculation_formula() + + report = Report(f"Check: bending moment steel beam (axis {self.axis})") + if self.m == 0: + report.add_paragraph("No bending moment was applied; therefore, no bending moment check is necessary.") + return report + report.add_paragraph( + rf"Profile {self.steel_cross_section.profile.name} with steel quality {self.steel_cross_section.material.steel_class.name} " + rf"is loaded with a bending moment of {abs(self.m):.{n}f} kNm (axis {self.axis}). " + rf"The resistance is calculated as follows, using cross-section class 3:" + ) + report.add_formula(calculation["resistance"], n=n) + report.add_paragraph("The unity check is calculated as follows:") + report.add_formula(calculation["check"], n=n) + if self.result().is_ok: + report.add_paragraph("The check for bending moment satisfies the requirements.") + else: + report.add_paragraph("The check for bending moment does NOT satisfy the requirements.") + return report diff --git a/blueprints/checks/eurocode/steel/strength_compression.py b/blueprints/checks/eurocode/steel/strength_compression.py index 0c5914551..3ee608b81 100644 --- a/blueprints/checks/eurocode/steel/strength_compression.py +++ b/blueprints/checks/eurocode/steel/strength_compression.py @@ -23,7 +23,7 @@ class CheckStrengthCompressionClass123: """Class to perform compression force resistance check for steel cross-sections, for cross-section class 1, 2, and 3 (Eurocode 3). - Coordinate System: + Coordinate System: z (vertical, usually strong axis) ↑ @@ -49,7 +49,7 @@ class CheckStrengthCompressionClass123: Example ------- - from blueprints.checks.eurocode.steel.compression_strength import CompressionForceCheck + from blueprints.checks.eurocode.steel.strength_compression import CompressionForceCheck from blueprints.materials.steel import SteelMaterial, SteelStrengthClass from blueprints.structural_sections.steel.standard_profiles.heb import HEB @@ -127,14 +127,18 @@ def report(self, n: int = 2) -> Report: if self.n == 0: report.add_paragraph("No compressive force was applied; therefore, no compression force check is necessary.") return report + + # Cache calculation formulas to avoid redundant recalculations + formulas = self.calculation_formula() + report.add_paragraph( rf"Profile {self.steel_cross_section.profile.name} with steel quality {self.steel_cross_section.material.steel_class.name} " rf"is loaded with a compressive force of {abs(self.n):.{n}f} kN. " rf"The resistance is calculated as follows:" ) - report.add_formula(self.calculation_formula()["resistance"], n=n) + report.add_formula(formulas["resistance"], n=n) report.add_paragraph("The unity check is calculated as follows:") - report.add_formula(self.calculation_formula()["check"], n=n) + report.add_formula(formulas["check"], n=n) if self.result().is_ok: report.add_paragraph("The check for compression force satisfies the requirements.") else: diff --git a/blueprints/checks/eurocode/steel/strength_i_profile.py b/blueprints/checks/eurocode/steel/strength_i_profile.py index 8f3f48cac..9c5a11c40 100644 --- a/blueprints/checks/eurocode/steel/strength_i_profile.py +++ b/blueprints/checks/eurocode/steel/strength_i_profile.py @@ -4,13 +4,13 @@ """ from dataclasses import dataclass, field -from typing import Any, ClassVar, Optional +from typing import Any, ClassVar, cast from sectionproperties.post.post import SectionProperties from blueprints.checks.check_protocol import CheckProtocol from blueprints.checks.check_result import CheckResult -from blueprints.checks.eurocode.steel import strength_compression, strength_tension +from blueprints.checks.eurocode.steel import strength_bending, strength_compression, strength_tension from blueprints.codes.eurocode.en_1993_1_1_2005 import EN_1993_1_1_2005 from blueprints.saf.results.result_internal_force_1d import ResultFor, ResultInternalForce1D, ResultOn from blueprints.structural_sections.steel.profile_definitions.i_profile import IProfile @@ -55,7 +55,7 @@ class CheckStrengthIProfileClass3: Example ------- - from blueprints.checks.eurocode.steel.i_profile_strength_class_3 import CheckStrengthIProfileClass3 + from blueprints.checks.eurocode.steel.strength_i_profile import CheckStrengthIProfileClass3 from blueprints.materials.steel import SteelMaterial, SteelStrengthClass from blueprints.structural_sections.steel.standard_profiles.heb import HEB @@ -71,7 +71,6 @@ class CheckStrengthIProfileClass3: heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) calc = CheckStrengthIProfileClass3(heb_300_s355, n, v_y, v_z, m_x, m_y, m_z, gamma_m0=1.0) calc.report().to_word("compression_strength.docx", language="nl") - """ steel_cross_section: SteelCrossSection @@ -86,12 +85,12 @@ class CheckStrengthIProfileClass3: section_properties: SectionProperties | None = None ignore_checks: list[str] | None = None - profile: Any = field(init=False, repr=False) + profile: IProfile = field(init=False, repr=False) material: Any = field(init=False, repr=False) result_internal_force_1d: ResultInternalForce1D = field(init=False, repr=False) name: str = "Check for steel I-profiles of Class 3" - source_docs: ClassVar[list] = [EN_1993_1_1_2005] + source_docs: ClassVar[list[Any]] = [EN_1993_1_1_2005] def __post_init__(self) -> None: """Post-initialization checks and type enforcement for forces/moments.""" @@ -120,29 +119,43 @@ def __post_init__(self) -> None: ), ) - def subchecks(self) -> dict[str, Optional["CheckProtocol"]]: + def subchecks(self) -> dict[str, CheckProtocol | None]: """Perform calculation steps for all strength checks, optionally ignoring specified checks.""" - all_checks = { - "compression": None, - "tension": None, - "bending about z": None, - "bending about y": None, + all_checks: dict[str, CheckProtocol | None] = { + "compression": cast( + CheckProtocol, + strength_compression.CheckStrengthCompressionClass123(self.steel_cross_section, self.n, self.gamma_m0, self.section_properties), + ), + "tension": cast( + CheckProtocol, + strength_tension.CheckStrengthTensionClass1234(self.steel_cross_section, self.n, self.gamma_m0, self.section_properties), + ), + "bending about z": cast( + CheckProtocol, + strength_bending.CheckStrengthBendingClass3( + self.steel_cross_section, self.m_z, axis="Mz", gamma_m0=self.gamma_m0, section_properties=self.section_properties + ), + ), + "bending about y": cast( + CheckProtocol, + strength_bending.CheckStrengthBendingClass3( + self.steel_cross_section, self.m_y, axis="My", gamma_m0=self.gamma_m0, section_properties=self.section_properties + ), + ), "shear z": None, "shear y": None, "torsion": None, + "torsion and shear z": None, + "torsion and shear y": None, "bending and shear": None, "bending and axial": None, "bending, shear and axial": None, } # Only perform compression check if n < 0, tension if n > 0 - if self.n < 0: - all_checks["compression"] = strength_compression.CheckStrengthCompressionClass123( - self.steel_cross_section, self.n, self.gamma_m0, self.section_properties - ) - elif self.n > 0: - all_checks["tension"] = strength_tension.CheckStrengthTensionClass1234( - self.steel_cross_section, self.n, self.gamma_m0, self.section_properties - ) + if self.n > 0: + all_checks["compression"] = None + elif self.n < 0: + all_checks["tension"] = None if self.ignore_checks: return {k: v for k, v in all_checks.items() if k not in self.ignore_checks} @@ -152,7 +165,7 @@ def result(self) -> CheckResult: """Perform all strength checks and return the overall result.""" checks = list(self.subchecks().values()) unity_checks = [c.result().unity_check for c in checks if c is not None] - filtered_unity_checks = [0] + [uc for uc in unity_checks if isinstance(uc, int | float)] + filtered_unity_checks: list[float] = [0.0] + [float(uc) for uc in unity_checks if isinstance(uc, int | float)] return CheckResult.from_unity_check(max(filtered_unity_checks)) def report(self, n: int = 2) -> Report: diff --git a/blueprints/checks/eurocode/steel/strength_shear.py b/blueprints/checks/eurocode/steel/strength_shear.py new file mode 100644 index 000000000..7708d206f --- /dev/null +++ b/blueprints/checks/eurocode/steel/strength_shear.py @@ -0,0 +1,313 @@ +"""Module for checking plastic shear force resistance of steel(Eurocode 3).""" + +from dataclasses import dataclass +from typing import ClassVar, Literal + +import numpy as np +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.check_result import CheckResult +from blueprints.codes.eurocode.en_1993_1_1_2005 import EN_1993_1_1_2005 +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_6_ultimate_limit_state import formula_6_17, formula_6_18, formula_6_18_sub_av, formula_6_19 +from blueprints.codes.formula import Formula +from blueprints.saf.results.result_internal_force_1d import ResultFor, ResultInternalForce1D, ResultOn +from blueprints.structural_sections.steel.profile_definitions.i_profile import IProfile +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection +from blueprints.type_alias import DIMENSIONLESS, KN +from blueprints.unit_conversion import KN_TO_N +from blueprints.utils.report import Report + + +@dataclass(frozen=True) +class CheckStrengthShearClass12IProfile: + """Class to perform plastic shear force resistance check for steel I-profiles of cross-section class 1 and 2 (Eurocode 3). + + Coordinate System: + + z (vertical, usually strong axis) + ↑ + | x (longitudinal beam direction, into screen) + | ↗ + | / + | / + | / + |/ + ←-----O + y (horizontal/side, usually weak axis) + + Parameters + ---------- + steel_cross_section : SteelCrossSection + The steel cross-section, of type I-profile, to check. + v : KN + The applied shear force (in kN). + axis : Literal["Vz", "Vy"] + Axis along which the shear force is applied. "Vz" (default) for z (vertical), "Vy" for y (horizontal). + gamma_m0 : DIMENSIONLESS, optional + Partial safety factor for resistance of cross-sections, default is 1.0. + section_properties : SectionProperties | None, optional + Pre-calculated section properties. If None, they will be calculated internally. + + Example + ------- + from blueprints.checks.eurocode.steel.strength_shear import CheckStrengthShearClass12IProfile + from blueprints.materials.steel import SteelMaterial, SteelStrengthClass + from blueprints.structural_sections.steel.standard_profiles.heb import HEB + + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + heb_300_profile = HEB.HEB300.with_corrosion(1.5) + v = 100 # Applied shear force in kN + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = CheckStrengthShearClass12IProfile(heb_300_s355, v, axis="Vz", gamma_m0=1.0) + calc.report().to_word("shear_strength.docx", language="nl") + + """ + + steel_cross_section: SteelCrossSection + v: KN = 0 + axis: Literal["Vz", "Vy"] = "Vz" + gamma_m0: DIMENSIONLESS = 1.0 + section_properties: SectionProperties | None = None + name: str = "Plastic shear strength check for steel I-profiles" + source_docs: ClassVar[list] = [EN_1993_1_1_2005] + + def __post_init__(self) -> None: + """Post-initialization to extract section properties and check profile type.""" + if not isinstance(self.steel_cross_section.profile, IProfile): + raise TypeError("The provided profile is not an I-profile.") + if self.section_properties is None: + section_properties = self.steel_cross_section.profile.section_properties() + object.__setattr__(self, "section_properties", section_properties) + + def calculation_formula(self) -> dict[str, Formula]: + """Calculate plastic shear force resistance check. + + Returns + ------- + dict[str, Formula] + Calculation results keyed by formula number. Returns an empty dict if no shear force is applied. + """ + # Get parameters from profile, average top and bottom flange properties + a = float(self.section_properties.area) # type: ignore[attr-defined] + b1 = self.steel_cross_section.profile.top_flange_width # type: ignore[attr-defined] + b2 = self.steel_cross_section.profile.bottom_flange_width # type: ignore[attr-defined] + tf1 = self.steel_cross_section.profile.top_flange_thickness # type: ignore[attr-defined] + tf2 = self.steel_cross_section.profile.bottom_flange_thickness # type: ignore[attr-defined] + tw = self.steel_cross_section.profile.web_thickness # type: ignore[attr-defined] + hw = self.steel_cross_section.profile.total_height - ( # type: ignore[attr-defined] + self.steel_cross_section.profile.top_flange_thickness + self.steel_cross_section.profile.bottom_flange_thickness # type: ignore[attr-defined] + ) + r1 = self.steel_cross_section.profile.top_radius # type: ignore[attr-defined] + r2 = self.steel_cross_section.profile.bottom_radius # type: ignore[attr-defined] + + if self.axis == "Vz" and self.steel_cross_section.fabrication_method in ["hot-rolled", "cold-formed"]: + av = formula_6_18_sub_av.Form6Dot18SubARolledIandHSection(a=a, b1=b1, b2=b2, hw=hw, r1=r1, r2=r2, tf1=tf1, tf2=tf2, tw=tw, eta=1.0) + elif self.axis == "Vz" and self.steel_cross_section.fabrication_method == "welded": + av = formula_6_18_sub_av.Form6Dot18SubDWeldedIHandBoxSection(hw_list=[hw], tw_list=[tw], eta=1.0) + else: # axis == "Vy" + av = formula_6_18_sub_av.Form6Dot18SubEWeldedIHandBoxSection(a=a, hw_list=[hw], tw_list=[tw]) + + f_y = self.steel_cross_section.yield_strength + v_ed = abs(self.v) * KN_TO_N + v_pl_rd = formula_6_18.Form6Dot18DesignPlasticShearResistance(a_v=av, f_y=f_y, gamma_m0=self.gamma_m0) + check_shear = formula_6_17.Form6Dot17CheckShearForce(v_ed=v_ed, v_c_rd=v_pl_rd) + return { + "shear_area": av, + "resistance": v_pl_rd, + "check": check_shear, + } + + def result(self) -> CheckResult: + """Calculate result of plastic shear force resistance. + + Returns + ------- + CheckResult + True if the shear force check passes, False otherwise. + """ + steps = self.calculation_formula() + provided = abs(self.v) * KN_TO_N + required = steps["resistance"] + return CheckResult.from_comparison(provided=provided, required=required) + + def report(self, n: int = 2) -> Report: + """Returns the report for the plastic shear force check. + + Parameters + ---------- + n : int, optional + Number of decimal places for numerical values in the report (default is 2). + + Returns + ------- + Report + Report of the plastic shear force check. + """ + report = Report("Check: shear force steel I-beam") + if self.v == 0: + report.add_paragraph("No shear force was applied; therefore, no shear force check is necessary.") + return report + axis_label = "(vertical) z" if self.axis == "Vz" else "(horizontal) y" + report.add_paragraph( + rf"Profile {self.steel_cross_section.profile.name} with steel quality {self.steel_cross_section.material.steel_class.name} " + rf"is loaded with a shear force of {abs(self.v):.{n}f} kN in the {axis_label}-direction. " + rf"The shear area $A_v$ is calculated as follows:" + ) + formulas = self.calculation_formula() + report.add_formula(formulas["shear_area"], n=n, split_after=[(2, "="), (7, "+"), (3, "=")]) + report.add_paragraph("The shear resistance is calculated as follows:") + report.add_formula(formulas["resistance"], n=n) + report.add_paragraph("The unity check is calculated as follows:") + report.add_formula(formulas["check"], n=n) + if self.result().is_ok: + report.add_paragraph("The check for plastic shear force satisfies the requirements.") + else: + report.add_paragraph("The check for plastic shear force does NOT satisfy the requirements.") + return report + + +@dataclass(frozen=True) +class CheckStrengthShearClass34: + """Class to perform plastic shear force resistance check for steel cross-section class 3 and 4 (Eurocode 3). + + Coordinate System: + + z (vertical, usually strong axis) + ↑ + | x (longitudinal beam direction, into screen) + | ↗ + | / + | / + | / + |/ + ←-----O + y (horizontal/side, usually weak axis) + + Parameters + ---------- + steel_cross_section : SteelCrossSection + The steel cross-section, of type I-profile, to check. + v : KN + The applied shear force (in kN). + axis : Literal["Vz", "Vy"] + Axis along which the shear force is applied. "Vz" (default) for z (vertical), "Vy" for y (horizontal). + gamma_m0 : DIMENSIONLESS, optional + Partial safety factor for resistance of cross-sections, default is 1.0. + section_properties : SectionProperties | None, optional + Pre-calculated section properties. If None, they will be calculated internally. + + Example + ------- + from blueprints.checks.eurocode.steel.strength_shear import CheckStrengthShearClass34IProfile + from blueprints.materials.steel import SteelMaterial, SteelStrengthClass + from blueprints.structural_sections.steel.standard_profiles.heb import HEB + + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + heb_300_profile = HEB.HEB300.with_corrosion(1.5) + v = 100 # Applied shear force in kN + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = CheckStrengthShearClass34IProfile(heb_300_s355, v, axis="Vz", gamma_m0=1.0) + calc.report().to_word("shear_strength.docx", language="nl") + + """ + + steel_cross_section: SteelCrossSection + v: KN = 0 + axis: Literal["Vz", "Vy"] = "Vz" + gamma_m0: DIMENSIONLESS = 1.0 + section_properties: SectionProperties | None = None + name: str = "Elastic shear strength check" + source_docs: ClassVar[list] = [EN_1993_1_1_2005] + + def __post_init__(self) -> None: + """Post-initialization to extract section properties and check profile type.""" + if self.section_properties is None: + section_properties = self.steel_cross_section.profile.section_properties() + object.__setattr__(self, "section_properties", section_properties) + + def calculation_formula(self) -> dict[str, Formula | float | int]: + """Calculate plastic shear force resistance check. + + Returns + ------- + dict[str, Formula] + Calculation results keyed by formula number. Returns an empty dict if no shear force is applied. + """ + rif1d = ResultInternalForce1D( + result_on=ResultOn.ON_BEAM, + member="N/A", + result_for=ResultFor.LOAD_CASE, + load_case="N/A", + vy=1 if self.axis == "Vy" else 0, + vz=1 if self.axis == "Vz" else 0, + ) + + unit_stress = self.steel_cross_section.profile.calculate_stress(rif1d) + sig_zxy_data = unit_stress.get_stress()[0]["sig_zxy"] + sig_zxy = float(np.max(np.abs(sig_zxy_data))) * abs(self.v) + resistance = float(self.steel_cross_section.yield_strength / np.sqrt(3) / self.gamma_m0 / sig_zxy * self.v * KN_TO_N) + + check_shear = formula_6_19.Form6Dot19CheckDesignElasticShearResistance( + tau_ed=sig_zxy, f_y=self.steel_cross_section.yield_strength, gamma_m0=self.gamma_m0 + ) + + return { + "shear_stress": sig_zxy, + "resistance": resistance, + "check": check_shear, + } + + def result(self) -> CheckResult: + """Calculate result of plastic shear force resistance. + + Returns + ------- + CheckResult + True if the shear force check passes, False otherwise. + """ + steps = self.calculation_formula() + provided = abs(self.v) * KN_TO_N + required = steps["resistance"] + return CheckResult.from_comparison(provided=provided, required=required) + + def report(self, n: int = 2) -> Report: + """Returns the report for the elastic shear force check. + + Parameters + ---------- + n : int, optional + Number of decimal places for numerical values in the report (default is 2). + + Returns + ------- + Report + Report of the elastic shear force check. + """ + report = Report("Check: shear force steel I-beam (Class 3/4)") + if self.v == 0: + report.add_paragraph("No shear force was applied; therefore, no shear force check is necessary.") + return report + axis_label = "(vertical) z" if self.axis == "Vz" else "(horizontal) y" + report.add_paragraph( + rf"Profile {self.steel_cross_section.profile.name} with steel quality {self.steel_cross_section.material.steel_class.name} " + rf"is loaded with a shear force of {abs(self.v):.{n}f} kN in the {axis_label}-direction. " + rf"The shear stress $\tau_{{ed}}$ is calculated using elastic theory. " + ) + formulas = self.calculation_formula() + report.add_paragraph(f"The maximum shear stress is: {formulas['shear_stress']:.{n}f} N/mm². ") + + tau_max = round(self.steel_cross_section.yield_strength / (np.sqrt(3) * self.gamma_m0), n) + report.add_paragraph("The maximum allowed shear stress is calculated as follows:") + report.add_paragraph(rf"$f_y / (\sqrt{{3}} \cdot \gamma_{{M0}})$ = {tau_max} N/mm². ") + + report.add_paragraph("The unity check is calculated as follows:") + check_formula = formulas["check"] + assert isinstance(check_formula, Formula), "Expected Formula for check" + report.add_formula(check_formula, n=n) + if self.result().is_ok: + report.add_paragraph("The check for elastic shear force satisfies the requirements.") + else: + report.add_paragraph("The check for elastic shear force does NOT satisfy the requirements.") + return report diff --git a/blueprints/checks/eurocode/steel/strength_tension.py b/blueprints/checks/eurocode/steel/strength_tension.py index c7af070b8..642410b68 100644 --- a/blueprints/checks/eurocode/steel/strength_tension.py +++ b/blueprints/checks/eurocode/steel/strength_tension.py @@ -48,7 +48,7 @@ class CheckStrengthTensionClass1234: Example ------- - from blueprints.checks.eurocode.steel.tension_strength import CheckStrengthTensionClass1234 + from blueprints.checks.eurocode.steel.strength_tension import CheckStrengthTensionClass1234 from blueprints.materials.steel import SteelMaterial, SteelStrengthClass from blueprints.structural_sections.steel.standard_profiles.heb import HEB @@ -126,14 +126,18 @@ def report(self, n: int = 2) -> Report: if self.n == 0: report.add_paragraph("No tensile force was applied; therefore, no tensile force check is necessary.") return report + + # Cache calculation formulas to avoid redundant recalculations + formulas = self.calculation_formula() + report.add_paragraph( rf"Profile {self.steel_cross_section.profile.name} with steel quality {self.steel_cross_section.material.steel_class.name} " rf"is loaded with a tensile force of {self.n:.{n}f} kN. " rf"The resistance is calculated as follows:" ) - report.add_formula(self.calculation_formula()["resistance"], n=n) + report.add_formula(formulas["resistance"], n=n) report.add_paragraph("The unity check is calculated as follows:") - report.add_formula(self.calculation_formula()["check"], n=n) + report.add_formula(formulas["check"], n=n) if self.result().is_ok: report.add_paragraph("The check for tensile force satisfies the requirements.") else: diff --git a/blueprints/checks/eurocode/steel/strength_torsion.py b/blueprints/checks/eurocode/steel/strength_torsion.py new file mode 100644 index 000000000..f02cb6cb7 --- /dev/null +++ b/blueprints/checks/eurocode/steel/strength_torsion.py @@ -0,0 +1,174 @@ +"""Module for checking torsional shear stress resistance (Eurocode 2, formula 6.23).""" + +from dataclasses import dataclass +from typing import ClassVar + +import numpy as np +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.check_result import CheckResult +from blueprints.codes.eurocode.en_1993_1_1_2005 import EN_1993_1_1_2005 +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_6_ultimate_limit_state.formula_6_23 import Form6Dot23CheckTorsionalMoment +from blueprints.codes.formula import Formula +from blueprints.saf.results.result_internal_force_1d import ResultFor, ResultInternalForce1D, ResultOn +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection +from blueprints.type_alias import DIMENSIONLESS, KNM +from blueprints.unit_conversion import KNM_TO_NMM +from blueprints.utils.report import Report + + +@dataclass(frozen=True) +class CheckStrengthStVenantTorsionClass1234: + """Class to perform torsion resistance check using St. Venant torsion (Eurocode 3). + + Coordinate System: + + z (vertical, usually strong axis) + ↑ + | x (longitudinal beam direction, into screen) + | ↗ + | / + | / + | / + |/ + ←-----O + y (horizontal/side, usually weak axis) + + Parameters + ---------- + steel_cross_section : SteelCrossSection + The steel cross-section, of type I-profile, to check. + m_x : KNM + The applied torsional moment (in kNm). + gamma_m0 : DIMENSIONLESS, optional + Partial safety factor for resistance of cross-sections, default is 1.0. + section_properties : SectionProperties | None, optional + Pre-calculated section properties. If None, they will be calculated internally. + + Example + ------- + from blueprints.checks.eurocode.steel.torsion_strength import TorsionStrengthCheck + from blueprints.materials.steel import SteelMaterial, SteelStrengthClass + from blueprints.structural_sections.steel.standard_profiles.heb import HEB + + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + heb_300_profile = HEB.HEB300.with_corrosion(1.5) + m_x = 10 # Applied torsional moment in kNm + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = TorsionStrengthCheck(heb_300_s355, m_x, gamma_m0=1.0) + calc.report().to_word("torsion_strength.docx", language="nl") + + """ + + steel_cross_section: SteelCrossSection + m_x: KNM = 0 + gamma_m0: DIMENSIONLESS = 1.0 + section_properties: SectionProperties | None = None + name: str = "Torsion strength check" + source_docs: ClassVar[list] = [EN_1993_1_1_2005] + + def __post_init__(self) -> None: + """Post-initialization to extract section properties.""" + if self.section_properties is None: + section_properties = self.steel_cross_section.profile.section_properties() + object.__setattr__(self, "section_properties", section_properties) + + def calculation_formula(self) -> dict[str, Formula | float]: + """Calculate torsion resistance check. + + Returns + ------- + dict[str, Formula | float] + Calculation results keyed by formula number. Returns an empty dict if no torsion is applied. + """ + rif1d = ResultInternalForce1D( + result_on=ResultOn.ON_BEAM, + member="N/A", + result_for=ResultFor.LOAD_CASE, + load_case="N/A", + mx=1, # 1 kNm + ) + + unit_stress = self.steel_cross_section.profile.calculate_stress(rif1d) + unit_sig_zxy = unit_stress.get_stress()[0]["sig_zxy"] + unit_max_sig_zxy = float(np.max(np.abs(unit_sig_zxy))) + + t_rd = self.steel_cross_section.yield_strength / self.gamma_m0 / np.sqrt(3) / unit_max_sig_zxy + t_ed = abs(self.m_x) + + check_torsion = Form6Dot23CheckTorsionalMoment(t_ed=t_ed, t_rd=t_rd) + + return { + "unit_shear_stress": unit_max_sig_zxy, + "resistance": t_rd, + "check": check_torsion, + } + + def result(self) -> CheckResult: + """Calculate result of torsion resistance. + + Returns + ------- + CheckResult + True if the torsion check passes, False otherwise. + """ + steps = self.calculation_formula() + provided = abs(self.m_x) * KNM_TO_NMM + required = steps["resistance"] * KNM_TO_NMM + return CheckResult.from_comparison(provided=provided, required=float(required)) + + def report(self, n: int = 2) -> Report: + """Returns the report for the torsion check. + + Parameters + ---------- + n : int, optional + Number of decimal places for numerical values in the report (default is 2). + + Returns + ------- + Report + Report of the torsion check. + """ + report = Report("Check: torsion steel beam") + if self.m_x == 0: + report.add_paragraph("No torsion was applied; therefore, no torsion check is necessary.") + return report + + # Cache calculation formulas to avoid redundant recalculations + formulas = self.calculation_formula() + + # Get information for the introduction of the report + profile_name = self.steel_cross_section.profile.name + steel_quality = self.steel_cross_section.material.steel_class.name + m_x_val = f"{self.m_x:.{n}f}" + unit_stress_val = f"{formulas['unit_shear_stress']:.{n}f}" + + report.add_paragraph( + rf"Profile {profile_name} with steel quality {steel_quality} " + rf"is loaded with a torsion of {m_x_val} kNm. " + rf"First, the unit torsional stress (at 1 kNm) is defined as {unit_stress_val} MPa. " + rf"The torsional resistance is calculated as follows:" + ) + + # Get values for the formula of torsion resistance + fy = self.steel_cross_section.yield_strength + gamma_m0 = self.gamma_m0 + unit_stress = formulas["unit_shear_stress"] + result = formulas["resistance"] + + eqn_1 = ( + rf"T_{{Rd}} = \frac{{f_y}}{{\gamma_{{M0}} \cdot \sqrt{{3}} \cdot \text{{unit-stress}}}} = " + rf"\frac{{{fy:.{n}f}}}{{{gamma_m0:.{n}f} \cdot \sqrt{{3}} \cdot {unit_stress:.{n}f}}} = {result:.{n}f} \ kNm" + ) + report.add_equation(eqn_1) + report.add_paragraph("The unity check is calculated as follows:") + check_formula = formulas["check"] + assert isinstance(check_formula, Formula), "Expected Formula for check" + report.add_formula(check_formula, n=n) + if self.result().is_ok: + report.add_paragraph("The check for torsion satisfies the requirements.") + else: + report.add_paragraph("The check for torsion does NOT satisfy the requirements.") + return report diff --git a/blueprints/checks/eurocode/steel/strength_torsion_shear.py b/blueprints/checks/eurocode/steel/strength_torsion_shear.py new file mode 100644 index 000000000..2bdeeab22 --- /dev/null +++ b/blueprints/checks/eurocode/steel/strength_torsion_shear.py @@ -0,0 +1,378 @@ +"""Module for checking torsional shear stress resistance with shear force present (Eurocode 2, formula 6.23).""" + +from dataclasses import dataclass +from typing import ClassVar, Literal + +import numpy as np +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.check_result import CheckResult +from blueprints.checks.eurocode.steel.strength_shear import CheckStrengthShearClass12IProfile +from blueprints.codes.eurocode.en_1993_1_1_2005 import EN_1993_1_1_2005 +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_6_ultimate_limit_state.formula_6_19 import Form6Dot19CheckDesignElasticShearResistance +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_6_ultimate_limit_state.formula_6_25 import Form6Dot25CheckCombinedShearForceAndTorsionalMoment +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_6_ultimate_limit_state.formula_6_26 import Form6Dot26VplTRdIOrHSection +from blueprints.codes.formula import Formula +from blueprints.saf.results.result_internal_force_1d import ResultFor, ResultInternalForce1D, ResultOn +from blueprints.structural_sections.steel.profile_definitions.i_profile import IProfile +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection +from blueprints.type_alias import DIMENSIONLESS, KN, KNM +from blueprints.unit_conversion import KN_TO_N +from blueprints.utils.report import Report + + +@dataclass(frozen=True) +class CheckStrengthTorsionShearClass12IProfile: + """Class to perform torsion resistance check with extra shear force for I profiles cross section 1 and 2 (Eurocode 3), using St. Venant torsion. + + Coordinate System: + + z (vertical, usually strong axis) + ↑ + | x (longitudinal beam direction, into screen) + | ↗ + | / + | / + | / + |/ + ←-----O + y (horizontal/side, usually weak axis) + + Parameters + ---------- + steel_cross_section : SteelCrossSection + The steel cross-section, of type I-profile, to check. + mx : KNM + The applied torsional moment (positive value, in kNm). + v : KN + The applied shear force (positive value, in kN). + axis : Literal["Vz", "Vy"] + Axis along which the shear force is applied. "Vz" (default) for z (vertical), "Vy" for y (horizontal). + gamma_m0 : DIMENSIONLESS, optional + Partial safety factor for resistance of cross-sections, default is 1.0. + section_properties : SectionProperties | None, optional + Pre-calculated section properties. If None, they will be calculated internally. + + Example + ------- + from blueprints.checks.eurocode.steel.strength_torsion_shear import CheckStrengthTorsionShearClass12IProfile + from blueprints.materials.steel import SteelMaterial, SteelStrengthClass + from blueprints.structural_sections.steel.standard_profiles.heb import HEB + + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + heb_300_profile = HEB.HEB300.with_corrosion(1.5) + m_x = 10 # Applied torsional moment in kNm + v = 100 # Applied shear force in kN + axis = "Vz" # Shear force applied in z-direction + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = CheckStrengthTorsionShearClass12IProfile(heb_300_s355, mx, v=v, axis=axis, gamma_m0=1.0) + calc.report().to_word("torsion_and_shear_strength.docx", language="nl") + + """ + + steel_cross_section: SteelCrossSection + m_x: KNM = 0 + v: KN = 0 + axis: Literal["Vz", "Vy"] = "Vz" + gamma_m0: DIMENSIONLESS = 1.0 + section_properties: SectionProperties | None = None + name: str = "Torsion strength check for steel I-profiles class 1 and 2" + source_docs: ClassVar[list] = [EN_1993_1_1_2005] + + def __post_init__(self) -> None: + """Post-initialization to extract section properties and check profile type.""" + if not isinstance(self.steel_cross_section.profile, IProfile): + raise TypeError("The provided profile is not an I-profile.") + if self.section_properties is None: + section_properties = self.steel_cross_section.profile.section_properties() + object.__setattr__(self, "section_properties", section_properties) + + def calculation_formula(self) -> dict[str, Formula | float]: + """Calculate torsion resistance check. + + Returns + ------- + dict[str, Formula | float] + Calculation results keyed by formula number. Returns an empty dict if no torsion is applied. + """ + shear_calculation = CheckStrengthShearClass12IProfile( + steel_cross_section=self.steel_cross_section, + v=self.v, + axis=self.axis, + gamma_m0=self.gamma_m0, + section_properties=self.section_properties, + ) + + shear_formulas = shear_calculation.calculation_formula() + a_v = shear_formulas["shear_area"] + v_pl_rd = shear_formulas["resistance"] + + rif1d = ResultInternalForce1D( + result_on=ResultOn.ON_BEAM, + member="N/A", + result_for=ResultFor.LOAD_CASE, + load_case="N/A", + mx=1, # 1 kNm + ) + + unit_stress = self.steel_cross_section.profile.calculate_stress(rif1d) + unit_sig_zxy = unit_stress.get_stress()[0]["sig_zxy"] + unit_max_sig_zxy = float(np.max(np.abs(unit_sig_zxy))) + + tau_t_ed = abs(self.m_x) * unit_max_sig_zxy + v_ed = abs(self.v) * KN_TO_N + + v_pl_t_rd = Form6Dot26VplTRdIOrHSection( + tau_t_ed=tau_t_ed, f_y=self.steel_cross_section.yield_strength, gamma_m0=self.gamma_m0, v_pl_rd=v_pl_rd + ) + + check_torsion_with_shear = Form6Dot25CheckCombinedShearForceAndTorsionalMoment(v_ed=v_ed, v_pl_t_rd=v_pl_t_rd) + + return { + "unit_shear_stress": unit_max_sig_zxy, + "shear_area": a_v, + "raw_shear_resistance": v_pl_rd, + "resistance": v_pl_t_rd, + "check": check_torsion_with_shear, + } + + def result(self) -> CheckResult: + """Calculate result of torsion resistance. + + Returns + ------- + CheckResult + True if the torsion check passes, False otherwise. + """ + steps = self.calculation_formula() + provided = 0 if self.m_x == 0 else abs(self.v) * KN_TO_N + required = steps["resistance"] + return CheckResult.from_comparison(provided=provided, required=float(required)) + + def report(self, n: int = 2) -> Report: + """Returns the report for the torsion check. + + Parameters + ---------- + n : int, optional + Number of decimal places for numerical values in the report (default is 2). + + Returns + ------- + Report + Report of the torsion check. + """ + report = Report("Check: torsion with shear force on steel beam") + if self.m_x == 0: + report.add_paragraph("No torsion was applied; therefore, no combined torsion with shear force check is necessary.") + return report + if self.v == 0: + report.add_paragraph("No shear force was applied; therefore, no combined torsion with shear force check is necessary.") + return report + + # Cache calculation formulas to avoid redundant recalculations + formulas = self.calculation_formula() + + # Get information for the introduction of the report + profile_name = self.steel_cross_section.profile.name + steel_quality = self.steel_cross_section.material.steel_class.name + m_x_val = f"{self.m_x:.{n}f}" + unit_stress_val = f"{formulas['unit_shear_stress']:.{n}f}" + axis_label = "(vertical) z" if self.axis == "Vz" else "(horizontal) y" + total_stress_val = f"{formulas['unit_shear_stress'] * self.m_x:.{n}f}" + + report.add_paragraph( + rf"Profile {profile_name} with steel quality {steel_quality} " + rf"is loaded with a torsion of {m_x_val} kNm a shear force of {abs(self.v):.{n}f} kN in the {axis_label}-direction. " + rf"First, the unit torsional stress (at 1 kNm) is defined as {unit_stress_val} MPa. " + rf"With the applied torsion, this results in a torsional stress of {total_stress_val} MPa. " + rf"The shear area and resistance (without torsion) are calculated as follows:" + ) + + shear_area_formula = formulas["shear_area"] + raw_shear_resistance_formula = formulas["raw_shear_resistance"] + resistance_formula = formulas["resistance"] + check_formula = formulas["check"] + + assert isinstance(shear_area_formula, Formula), "Expected Formula for shear_area" + assert isinstance(raw_shear_resistance_formula, Formula), "Expected Formula for raw_shear_resistance" + assert isinstance(resistance_formula, Formula), "Expected Formula for resistance" + assert isinstance(check_formula, Formula), "Expected Formula for check" + + report.add_formula(shear_area_formula, n=n, split_after=[(2, "="), (7, "+"), (3, "=")]) + report.add_formula(raw_shear_resistance_formula, n=n) + report.add_paragraph("Next, the combined torsion and shear resistance is calculated as follows:") + report.add_formula(resistance_formula, n=n) + report.add_paragraph("The unity check is calculated as follows:") + report.add_formula(check_formula, n=n) + if self.result().is_ok: + report.add_paragraph("The check for torsion satisfies the requirements.") + else: + report.add_paragraph("The check for torsion does NOT satisfy the requirements.") + return report + + +@dataclass(frozen=True) +class CheckStrengthTorsionShearClass34: + """Class to perform torsion resistance check with extra shear force for cross section class 3 and 4 (Eurocode 3), using St. Venant torsion. + + Coordinate System: + + z (vertical, usually strong axis) + ↑ + | x (longitudinal beam direction, into screen) + | ↗ + | / + | / + | / + |/ + ←-----O + y (horizontal/side, usually weak axis) + + Parameters + ---------- + steel_cross_section : SteelCrossSection + The steel cross-section to check. + m_x : KNM + The applied torsional moment (positive value, in kNm). + v : KN + The applied shear force (positive value, in kN). + axis : Literal["Vz", "Vy"] + Axis along which the shear force is applied. "Vz" (default) for z (vertical), "Vy" for y (horizontal). + gamma_m0 : DIMENSIONLESS, optional + Partial safety factor for resistance of cross-sections, default is 1.0. + section_properties : SectionProperties | None, optional + Pre-calculated section properties. If None, they will be calculated internally. + + Example + ------- + from blueprints.checks.eurocode.steel.strength_torsion_shear import CheckStrengthTorsionShearClass34 + from blueprints.materials.steel import SteelMaterial, SteelStrengthClass + from blueprints.structural_sections.steel.standard_profiles.heb import HEB + + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + heb_300_profile = HEB.HEB300.with_corrosion(1.5) + m_x = 10 # Applied torsional moment in kNm + v = 100 # Applied shear force in kN + axis = "Vz" # Shear force applied in z-direction + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = CheckStrengthTorsionShearClass34(heb_300_s355, m_x, v=v, axis=axis, gamma_m0=1.0) + calc.report().to_word("torsion_and_shear_strength.docx", language="nl") + + """ + + steel_cross_section: SteelCrossSection + m_x: KNM = 0 + v: KN = 0 + axis: Literal["Vz", "Vy"] = "Vz" + gamma_m0: DIMENSIONLESS = 1.0 + section_properties: SectionProperties | None = None + name: str = "Torsion strength check for steel class 3 and 4" + source_docs: ClassVar[list] = [EN_1993_1_1_2005] + + def __post_init__(self) -> None: + """Post-initialization to extract section properties and check profile type.""" + if self.section_properties is None: + section_properties = self.steel_cross_section.profile.section_properties() + object.__setattr__(self, "section_properties", section_properties) + + def calculation_formula(self) -> dict[str, Formula | float]: + """Calculate torsion resistance check. + + Returns + ------- + dict[str, Formula | float] + Calculation results keyed by formula number. Returns an empty dict if no torsion is applied. + """ + rif1d = ResultInternalForce1D( + result_on=ResultOn.ON_BEAM, + member="N/A", + result_for=ResultFor.LOAD_CASE, + load_case="N/A", + vy=self.v if self.axis == "Vy" else 0, + vz=self.v if self.axis == "Vz" else 0, + mx=self.m_x, + ) + + stress = self.steel_cross_section.profile.calculate_stress(rif1d) + sig_zxy = stress.get_stress()[0]["sig_zxy"] + max_sig_zxy = float(np.max(np.abs(sig_zxy))) + + shear_resistance = self.steel_cross_section.yield_strength / np.sqrt(3) / self.gamma_m0 + + check_torsion_with_shear = Form6Dot19CheckDesignElasticShearResistance( + tau_ed=max_sig_zxy, f_y=self.steel_cross_section.yield_strength, gamma_m0=self.gamma_m0 + ) + + return { + "shear_stress": max_sig_zxy, + "resistance": shear_resistance, + "check": check_torsion_with_shear, + } + + def result(self) -> CheckResult: + """Calculate result of torsion resistance. + + Returns + ------- + CheckResult + True if the torsion check passes, False otherwise. + """ + steps = self.calculation_formula() + provided = steps["shear_stress"] + required = steps["resistance"] + return CheckResult.from_comparison(provided=provided, required=float(required)) + + def report(self, n: int = 2) -> Report: + """Returns the report for the elastic torsion check (Class 3/4). + + Parameters + ---------- + n : int, optional + Number of decimal places for numerical values in the report (default is 2). + + Returns + ------- + Report + Report of the elastic torsion check. + """ + report = Report("Check: elastic torsion with shear force on steel beam (Class 3/4)") + if self.m_x == 0: + report.add_paragraph("No torsion was applied; therefore, no combined torsion with shear force check is necessary.") + return report + if self.v == 0: + report.add_paragraph("No shear force was applied; therefore, no combined torsion with shear force check is necessary.") + return report + + # Cache calculation formulas to avoid redundant recalculations + formulas = self.calculation_formula() + + # Get information for the introduction of the report + profile_name = self.steel_cross_section.profile.name + steel_quality = self.steel_cross_section.material.steel_class.name + m_x_val = f"{self.m_x:.{n}f}" + axis_label = "(vertical) z" if self.axis == "Vz" else "(horizontal) y" + shear_stress_val = f"{formulas['shear_stress']:.{n}f}" + + report.add_paragraph( + rf"Profile {profile_name} with steel quality {steel_quality} " + rf"is loaded with a torsion of {m_x_val} kNm and a shear force of {abs(self.v):.{n}f} kN in the {axis_label}-direction. " + rf"For class 3/4 sections, the combined shear stress from torsion and shear is calculated using elastic theory. " + rf"The maximum combined shear stress is: {shear_stress_val} N/mm²." + ) + + tau_max = round(self.steel_cross_section.yield_strength / (np.sqrt(3) * self.gamma_m0), n) + report.add_paragraph("The maximum allowed elastic shear stress is calculated as follows:") + report.add_paragraph(rf"$f_y / (\sqrt{{3}} \cdot \gamma_{{M0}})$ = {tau_max} N/mm².") + + report.add_paragraph("The unity check is calculated as follows:") + check_formula = formulas["check"] + assert isinstance(check_formula, Formula), "Expected Formula for check" + report.add_formula(check_formula, n=n) + if self.result().is_ok: + report.add_paragraph("The check for elastic torsion with shear satisfies the requirements.") + else: + report.add_paragraph("The check for elastic torsion with shear does NOT satisfy the requirements.") + return report diff --git a/blueprints/codes/eurocode/en_1993_1_1_2005/chapter_6_ultimate_limit_state/formula_6_18_sub_av.py b/blueprints/codes/eurocode/en_1993_1_1_2005/chapter_6_ultimate_limit_state/formula_6_18_sub_av.py index 06ca59514..ba919f57e 100644 --- a/blueprints/codes/eurocode/en_1993_1_1_2005/chapter_6_ultimate_limit_state/formula_6_18_sub_av.py +++ b/blueprints/codes/eurocode/en_1993_1_1_2005/chapter_6_ultimate_limit_state/formula_6_18_sub_av.py @@ -10,7 +10,10 @@ class Form6Dot18SubARolledIandHSection(Formula): - r"""Class representing formula 6.18suba for the calculation of shear area for a rolled I and H section.""" + r"""Class representing formula 6.18suba for the calculation of shear area for a rolled I and H section. + + The equations has been slightly modified to split effects of top and bottom flange. + """ label = "6.18suba" source_document = EN_1993_1_1_2005 @@ -18,10 +21,13 @@ class Form6Dot18SubARolledIandHSection(Formula): def __init__( self, a: MM2, - b: MM, + b1: MM, + b2: MM, hw: MM, - r: MM, - tf: MM, + r1: MM, + r2: MM, + tf1: MM, + tf2: MM, tw: MM, eta: DIMENSIONLESS, ) -> None: @@ -33,14 +39,20 @@ def __init__( ---------- a : MM2 [$A$] Cross-sectional area [$mm^2$]. - b : MM - [$b$] Overall breadth [$mm$]. + b1 : MM + [$b$] Overall breadth of flange 1 [$mm$]. + b2 : MM + [$b$] Overall breadth of flange 2 [$mm$]. hw : MM [$h_w$] Depth of the web [$mm$]. - r : MM - [$r$] Root radius [$mm$]. - tf : MM - [$t_f$] Flange thickness [$mm$]. + r1 : MM + [$r$] Root radius at flange 1 [$mm$]. + r2 : MM + [$r$] Root radius at flange 2 [$mm$]. + tf1 : MM + [$t_f$] Flange thickness 1 [$mm$]. + tf2 : MM + [$t_f$] Flange thickness 2 [$mm$]. tw : MM [$t_w$] Web thickness [$mm$]. If the web thickness is not constant, tw should be taken as the minimum thickness. eta : DIMENSIONLESS, optional @@ -48,42 +60,54 @@ def __init__( """ super().__init__() self.a = a - self.b = b + self.b1 = b1 + self.b2 = b2 self.hw = hw - self.r = r - self.tf = tf + self.r1 = r1 + self.r2 = r2 + self.tf1 = tf1 + self.tf2 = tf2 self.tw = tw self.eta = eta @staticmethod def _evaluate( a: MM2, - b: MM, + b1: MM, + b2: MM, hw: MM, - r: MM, - tf: MM, + r1: MM, + r2: MM, + tf1: MM, + tf2: MM, tw: MM, eta: DIMENSIONLESS, ) -> MM2: """Evaluates the formula, for more information see the __init__ method.""" - raise_if_negative(a=a, b=b, hw=hw, r=r, tf=tf, tw=tw, eta=eta) + raise_if_negative(a=a, b1=b1, b2=b2, hw=hw, r1=r1, r2=r2, tf1=tf1, tf2=tf2, tw=tw, eta=eta) - av = a - 2 * b * tf + (tw + 2 * r) * tf + av = a - b1 * tf1 - b2 * tf2 + (tw + 2 * r1) * tf1 / 2 + (tw + 2 * r2) * tf2 / 2 av_min = eta * hw * tw return max(0, av, av_min) def latex(self, n: int = 3) -> LatexFormula: """Returns LatexFormula object for formula 6.18suba.""" - _equation: str = r"max(A - 2 \cdot b \cdot t_f + (t_w + 2 \cdot r) \cdot t_f; \eta \cdot h_w \cdot t_w)" + _equation: str = ( + r"\max(A - b_1 \cdot t_{f1} - b_2 \cdot t_{f2} + (t_w + 2 \cdot r_1) \cdot \frac{t_{f1}}{2} + " + r"(t_w + 2 \cdot r_2) \cdot \frac{t_{f2}}{2}; \eta \cdot h_w \cdot t_w)" + ) _numeric_equation: str = latex_replace_symbols( _equation, { r"A": f"{self.a:.{n}f}", - r"b": f"{self.b:.{n}f}", + r"b_1": f"{self.b1:.{n}f}", + r"b_2": f"{self.b2:.{n}f}", r"h_w": f"{self.hw:.{n}f}", - r"r": f"{self.r:.{n}f}", - r"t_f": f"{self.tf:.{n}f}", + r"r_1": f"{self.r1:.{n}f}", + r"r_2": f"{self.r2:.{n}f}", + r"t_{f1}": f"{self.tf1:.{n}f}", + r"t_{f2}": f"{self.tf2:.{n}f}", r"t_w": f"{self.tw:.{n}f}", r"\eta": f"{self.eta:.{n}f}", }, diff --git a/blueprints/structural_sections/_profile.py b/blueprints/structural_sections/_profile.py index 146ad3a19..5ca030a52 100644 --- a/blueprints/structural_sections/_profile.py +++ b/blueprints/structural_sections/_profile.py @@ -9,12 +9,14 @@ import matplotlib.pyplot as plt from sectionproperties.analysis import Section from sectionproperties.post.post import SectionProperties +from sectionproperties.post.stress_post import StressPost from sectionproperties.pre import Geometry from shapely import Point, Polygon from shapely.affinity import rotate, translate +from blueprints.saf.results.result_internal_force_1d import ResultInternalForce1D from blueprints.type_alias import DEG, M3_M, MM, MM2 -from blueprints.unit_conversion import M_TO_MM, MM3_TO_M3 +from blueprints.unit_conversion import KN_TO_N, KNM_TO_NMM, M_TO_MM, MM3_TO_M3 @dataclass(frozen=True) @@ -185,6 +187,47 @@ def plotter(self) -> Callable[[Any], plt.Figure]: """Default plotter function for the profile.""" raise AttributeError("No plotter is defined.") + def calculate_stress(self, result_internal_force_1d: ResultInternalForce1D) -> StressPost: + """Calculate the stress distribution for the profile given internal forces. + + Parameters + ---------- + result_internal_force_1d : ResultInternalForce1D + The internal forces and moments to calculate the stress for. + + Returns + ------- + Callable[..., StressPost] + A function that calculates the stress distribution when called. + """ + section = self._section() + section.calculate_geometric_properties() + section.calculate_warping_properties() + # Note: The mapping of internal forces to sectionproperties parameters + # Blueprints uses x for longitudinal axis, y for horizontal, z for vertical + # sectionproperties uses x for horizontal, y for vertical, z for longitudinal + + # Coordinate System Blueprints: Coordinate System SectionProperties: + # z (vertical, usually strong axis) y (vertical, usually strong axis) + # ↑ ↑ + # | x (longitudinal beam direction, into screen) | z (longitudinal beam direction, into screen) + # | ↗ | ↗ + # | / | / + # | / | / + # | / | / + # |/ |/ + # ←-----O O------> + # y (horizontal/side, usually weak axis) x (horizontal/side, usually weak axis) + + return section.calculate_stress( + n=float(result_internal_force_1d.n) * KN_TO_N, + vx=-float(result_internal_force_1d.vy) * KN_TO_N, + vy=float(result_internal_force_1d.vz) * KN_TO_N, + mxx=-float(result_internal_force_1d.my) * KNM_TO_NMM, + myy=float(result_internal_force_1d.mz) * KNM_TO_NMM, + mzz=float(result_internal_force_1d.mx) * KNM_TO_NMM, + ) + def plot(self, plotter: Callable[[Any], plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: """Plot the profile. Making use of the standard plotter. diff --git a/blueprints/utils/_report_to_word.py b/blueprints/utils/_report_to_word.py index 2b84fcf21..cf5a2ad21 100644 --- a/blueprints/utils/_report_to_word.py +++ b/blueprints/utils/_report_to_word.py @@ -26,7 +26,7 @@ class _ReportToWordConverter: Limitations and Usage Notes: - Text blocks using \text{...} (can also be \textbf{...} and/or \textit{...} for bold/italic). - - Equations in \begin{equation}...\end{equation} environments (with optional \tag{...}). + - Equations in \begin{multline}...\end{multline} or \begin{equation}...\end{equation} environments (with optional \tag{...}). - Titles, sections, subsections, and subsubsections using \title{...}, \section{...}, \subsection{...}, \subsubsection{...}. - Tables in \begin{table}...\end{table} environments with tabular content - Figures in \begin{figure}...\end{figure} environments with \includegraphics. @@ -175,7 +175,7 @@ def _extract_structural_elements(content: str) -> list[dict[str, str | int]]: "figure": r"\\begin\{figure\}", "itemize": r"\\begin\{itemize\}", "enumerate": r"\\begin\{enumerate\}", - "equation": r"\\begin\{equation\}", + "equation": r"\\begin\{(multline|equation)\}", "newline": r"\\newline", } @@ -437,14 +437,24 @@ def _add_equation(self, doc: DocumentObject, content: str) -> None: equation_content = re.sub(r"\s*\\tag\{[^}]+\}", "", content) equation_content = re.sub(r"\\notag", "", equation_content) - p = doc.add_paragraph() - p.style = "No Spacing" - p.paragraph_format.space_before = Pt(6) - p.paragraph_format.space_after = Pt(6) - p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER - p._p.append(self._formula(equation_content)) # noqa: SLF001 - if tag: - p.add_run(f" ({tag})") + # Split equation content on line breaks (\\) + lines = ( + equation_content.replace("\\begin{multline}", "") + .replace("\\end{multline}", "") + .replace("\\begin{equation}", "") + .replace("\\end{equation}", "") + .split(r"\\") + ) + for idx, line in enumerate(lines): + p = doc.add_paragraph() + p.style = "No Spacing" + p.paragraph_format.space_before = Pt(6) + p.paragraph_format.space_after = Pt(6) + p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + p._p.append(self._formula(line)) # noqa: SLF001 + # Only add tag to the last line + if tag and idx == len(lines) - 1: + p.add_run(f" ({tag})") def _add_table_to_doc(self, doc: DocumentObject, table_latex: str) -> None: """Parse LaTeX table and add it to the Word document. diff --git a/blueprints/utils/report.py b/blueprints/utils/report.py index 3072102fe..c989502e6 100644 --- a/blueprints/utils/report.py +++ b/blueprints/utils/report.py @@ -119,6 +119,7 @@ def add_equation( equation: str, tag: str | None = None, inline: bool = False, + split_after: list[tuple[int, str]] | None = None, ) -> Self: r"""Add an equation to the report. For adding Blueprints formulas, use add_formula instead. @@ -130,6 +131,10 @@ def add_equation( Tag to label the equation (e.g., "6.83", "EN 1992-1-1:2004 6.6n", etc.). inline : bool, optional Whether to add the equation inline (meaning within text) or as a separate equation block. Default is False. + split_after: list[tuple[int, str]], optional + List of characters to split the equation line on for better readability. + e.g. a = b + c = 2 + 3 = 5 with split_after=[(2, "="), (2, "+")] will split after second "=" and second "+" + to give: a = b + c = \\ 2 + \\ 3 = 5. Default is None. Returns ------- @@ -145,12 +150,35 @@ def add_equation( >>> report.add_equation(r"\\frac{a}{b}", inline=True) """ + + def _split_equation(eq: str, split_after: list[tuple[int, str]] | None) -> str: + if not split_after: + return eq + eq_mod = eq + # Sort by decreasing index so insertion doesn't affect later positions + for n, char in sorted(split_after, reverse=True): + # Find nth occurrence of char + idx = -1 + count = 0 + for i, c in enumerate(eq_mod): + if c == char: + count += 1 + if count == n: + idx = i + break + if idx != -1: + eq_mod = eq_mod[: idx + 1] + r" \\" + eq_mod[idx + 1 :] + return eq_mod + + eq_to_use = _split_equation(equation, split_after) + multline_vs_equation = "multline" if split_after else "equation" + if inline: - self.content += r"\txt{ " + rf"${equation}$" + f"{f' ({tag})' if tag else ''}" + r" }" + self.content += r"\txt{ " + rf"${eq_to_use}$" + f"{f' ({tag})' if tag else ''}" + r" }" elif tag: - self.content += rf"\begin{{equation}} {equation} \tag{{{tag}}} \end{{equation}}" + self.content += rf"\begin{{{multline_vs_equation}}} {eq_to_use} \tag{{{tag}}} \end{{{multline_vs_equation}}}" else: - self.content += rf"\begin{{equation}} {equation} \notag \end{{equation}}" + self.content += rf"\begin{{{multline_vs_equation}}} {eq_to_use} \notag \end{{{multline_vs_equation}}}" # Add a newline for visual separation self.content += "\n" @@ -165,6 +193,7 @@ def add_formula( include_source: bool = True, include_formula_number: bool = True, inline: bool = False, + split_after: list[tuple[int, str]] | None = None, ) -> Self: r"""Add a Blueprints formula to the report, for generic equations, use add_equation. @@ -187,6 +216,10 @@ def add_formula( For example: "6.5" or "6.6n". inline : bool, optional Whether to add the formula inline (meaning within text) or as a separate equation block (default). + split_after: list[tuple[int, str]], optional + List of characters to split the equation line on for better readability. + e.g. a = b + c = 2 + 3 = 5 with split_after=[(2, "="), (2, "+")] will split after second "=" and second "+" + to give: a = b + c = \\ 2 + \\ 3 = 5. Default is None. Returns ------- @@ -227,7 +260,7 @@ def add_formula( tag_parts.append(formula.label) tag_str = " ".join(tag_parts).strip() - return self.add_equation(equation=equation_str, inline=inline, tag=tag_str or None) + return self.add_equation(equation=equation_str, inline=inline, tag=tag_str or None, split_after=split_after) def add_heading(self, text: str, level: int = 1) -> Self: """Add a heading to the report. @@ -506,7 +539,7 @@ def __repr__(self) -> str: """Return a concise representation showing report structure and content summary.""" sections = self.content.count(r"\section{") subsections = self.content.count(r"\subsection{") - equations = self.content.count(r"\begin{equation}") + equations = self.content.count(r"\begin{multline}") + self.content.count(r"\begin{equation}") tables = self.content.count(r"\begin{table}") figures = self.content.count(r"\begin{figure}") lists = self.content.count(r"\begin{itemize}") + self.content.count(r"\begin{enumerate}") @@ -524,7 +557,7 @@ def __str__(self) -> str: """Return a human-readable representation of the report structure and content.""" sections = self.content.count(r"\section{") subsections = self.content.count(r"\subsection{") - equations = self.content.count(r"\begin{equation}") + equations = self.content.count(r"\begin{multline}") + self.content.count(r"\begin{equation}") tables = self.content.count(r"\begin{table}") figures = self.content.count(r"\begin{figure}") lists = self.content.count(r"\begin{itemize}") + self.content.count(r"\begin{enumerate}") diff --git a/tests/checks/eurocode/en_1992_1_1_2004/__init__.py b/tests/checks/eurocode/concrete/__init__.py similarity index 100% rename from tests/checks/eurocode/en_1992_1_1_2004/__init__.py rename to tests/checks/eurocode/concrete/__init__.py diff --git a/tests/checks/eurocode/en_1992_1_1_2004/nominal_concrete_cover/test_nominal_concrete_cover.py b/tests/checks/eurocode/concrete/test_nominal_concrete_cover.py similarity index 100% rename from tests/checks/eurocode/en_1992_1_1_2004/nominal_concrete_cover/test_nominal_concrete_cover.py rename to tests/checks/eurocode/concrete/test_nominal_concrete_cover.py diff --git a/tests/checks/eurocode/en_1992_1_1_2004/nominal_concrete_cover/__init__.py b/tests/checks/eurocode/en_1992_1_1_2004/nominal_concrete_cover/__init__.py deleted file mode 100644 index 47f8bba62..000000000 --- a/tests/checks/eurocode/en_1992_1_1_2004/nominal_concrete_cover/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Contains tests for the nominal concrete cover check.""" diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/__init__.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/__init__.py deleted file mode 100644 index b8dbaabe5..000000000 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test steel ultimate limit state checks.""" diff --git a/tests/checks/eurocode/en_1993_1_1_2005/__init__.py b/tests/checks/eurocode/steel/__init__.py similarity index 100% rename from tests/checks/eurocode/en_1993_1_1_2005/__init__.py rename to tests/checks/eurocode/steel/__init__.py diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/conftest.py b/tests/checks/eurocode/steel/conftest.py similarity index 72% rename from tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/conftest.py rename to tests/checks/eurocode/steel/conftest.py index 32d218fb3..7d79e95ac 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/conftest.py +++ b/tests/checks/eurocode/steel/conftest.py @@ -6,6 +6,7 @@ from blueprints.materials.steel import SteelMaterial, SteelStrengthClass from blueprints.structural_sections.steel.standard_profiles.chs import CHS from blueprints.structural_sections.steel.standard_profiles.heb import HEB +from blueprints.structural_sections.steel.standard_profiles.unp import UNP from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection @@ -23,3 +24,11 @@ def chs_steel_cross_section() -> tuple[SteelCrossSection, SectionProperties]: steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) profile = CHS.CHS1016x12_5 return SteelCrossSection(profile=profile, material=steel_material), profile.section_properties() + + +@pytest.fixture(scope="class") +def unp_steel_cross_section() -> tuple[SteelCrossSection, SectionProperties]: + """Create a SteelCrossSection fixture with UNP80 profile and S355 steel material.""" + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + profile = UNP.UNP80 + return SteelCrossSection(profile=profile, material=steel_material), profile.section_properties() diff --git a/tests/checks/eurocode/steel/test_strength_bending.py b/tests/checks/eurocode/steel/test_strength_bending.py new file mode 100644 index 000000000..c080330fe --- /dev/null +++ b/tests/checks/eurocode/steel/test_strength_bending.py @@ -0,0 +1,181 @@ +"""Tests for bending moment strength according to Eurocode 3.""" + +import pytest +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.eurocode.steel.strength_bending import CheckStrengthBendingClass3, CheckStrengthBendingClass12 +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection + + +class TestCheckStrengthBendingClass12: + """Tests for CheckStrengthBendingClass12.""" + + def test_result_none(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no bending moment.""" + cross_section, section_properties = heb_steel_cross_section + calc = CheckStrengthBendingClass12(cross_section, 0, axis="My", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + calc_without_section_props = CheckStrengthBendingClass12(cross_section, 0, axis="My", gamma_m0=1.0) + assert pytest.approx(result.unity_check) == calc_without_section_props.result().unity_check + + def test_result_my_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok bending moment about y-axis.""" + cross_section, section_properties = heb_steel_cross_section + m = 355 * 1.869 * 0.99 + calc = CheckStrengthBendingClass12(cross_section, m, axis="My", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + def test_result_my_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok bending moment about y-axis.""" + cross_section, section_properties = heb_steel_cross_section + m = 355 * 1.869 * 1.01 + calc = CheckStrengthBendingClass12(cross_section, m, axis="My", section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() + + def test_result_mz_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok bending moment about z-axis.""" + cross_section, section_properties = heb_steel_cross_section + m = 355 * 0.870 * 0.99 + calc = CheckStrengthBendingClass12(cross_section, m, axis="Mz", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + def test_result_mz_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok bending moment about z-axis.""" + cross_section, section_properties = heb_steel_cross_section + m = 355 * 0.870 * 1.01 + calc = CheckStrengthBendingClass12(cross_section, m, axis="Mz", section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() + + def test_negative_moment(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for negative bending moments (should be treated as positive).""" + cross_section, section_properties = heb_steel_cross_section + m = -355 * 1.869 * 0.99 + calc = CheckStrengthBendingClass12(cross_section, m, axis="My", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + m = -355 * 0.870 * 0.99 + calc = CheckStrengthBendingClass12(cross_section, m, axis="Mz", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + + def test_invalid_axis(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test ValueError is raised for invalid axis input.""" + cross_section, section_properties = heb_steel_cross_section + with pytest.raises(ValueError): + CheckStrengthBendingClass12(cross_section, 100, axis="Mx", section_properties=section_properties).calculation_formula() + + def test_report(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test report output for bending moment check.""" + cross_section, section_properties = heb_steel_cross_section + m = 100 + calc = CheckStrengthBendingClass12(cross_section, m, axis="My", section_properties=section_properties) + assert calc.report() + + +class TestCheckStrengthBendingClass3: + """Tests for CheckStrengthBendingClass3.""" + + def test_result_none(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no bending moment.""" + cross_section, section_properties = heb_steel_cross_section + calc = CheckStrengthBendingClass3(cross_section, 0, axis="My", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + calc_without_section_props = CheckStrengthBendingClass3(cross_section, 0, axis="My", gamma_m0=1.0) + assert pytest.approx(result.unity_check) == calc_without_section_props.result().unity_check + + def test_result_my_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok bending moment about y-axis.""" + cross_section, section_properties = heb_steel_cross_section + m = 355 * 1.678 * 0.99 + calc = CheckStrengthBendingClass3(cross_section, m, axis="My", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + def test_result_my_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok bending moment about y-axis.""" + cross_section, section_properties = heb_steel_cross_section + m = 355 * 1.678 * 1.01 + calc = CheckStrengthBendingClass3(cross_section, m, axis="My", section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() + + def test_result_mz_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok bending moment about z-axis.""" + cross_section, section_properties = heb_steel_cross_section + m = 355 * 0.571 * 0.99 + calc = CheckStrengthBendingClass3(cross_section, m, axis="Mz", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + def test_result_mz_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok bending moment about z-axis.""" + cross_section, section_properties = heb_steel_cross_section + m = 355 * 0.571 * 1.01 + calc = CheckStrengthBendingClass3(cross_section, m, axis="Mz", section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() + + def test_negative_moment(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for negative bending moments (should be treated as positive).""" + cross_section, section_properties = heb_steel_cross_section + m = -355 * 1.678 * 0.99 + calc = CheckStrengthBendingClass3(cross_section, m, axis="My", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + m = -355 * 0.571 * 0.99 + calc = CheckStrengthBendingClass3(cross_section, m, axis="Mz", section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + + def test_invalid_axis(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test ValueError is raised for invalid axis input.""" + cross_section, section_properties = heb_steel_cross_section + with pytest.raises(ValueError): + CheckStrengthBendingClass3(cross_section, 100, axis="Mx", section_properties=section_properties).calculation_formula() + + def test_report(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test report output for bending moment check.""" + cross_section, section_properties = heb_steel_cross_section + m = 100 + calc = CheckStrengthBendingClass3(cross_section, m, axis="My", section_properties=section_properties) + assert calc.report() diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_strength_compression.py b/tests/checks/eurocode/steel/test_strength_compression.py similarity index 100% rename from tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_strength_compression.py rename to tests/checks/eurocode/steel/test_strength_compression.py diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_strength_i_profile.py b/tests/checks/eurocode/steel/test_strength_i_profile.py similarity index 100% rename from tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_strength_i_profile.py rename to tests/checks/eurocode/steel/test_strength_i_profile.py diff --git a/tests/checks/eurocode/steel/test_strength_shear.py b/tests/checks/eurocode/steel/test_strength_shear.py new file mode 100644 index 000000000..6c8002924 --- /dev/null +++ b/tests/checks/eurocode/steel/test_strength_shear.py @@ -0,0 +1,155 @@ +"""Tests for shear strength checks according to Eurocode 3.""" + +import pytest +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.eurocode.steel.strength_shear import CheckStrengthShearClass12IProfile, CheckStrengthShearClass34 +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection + + +class TestCheckStrengthShearClass12IProfile: + """Tests for CheckStrengthShearClass12IProfile.""" + + def test_result_none(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 0 + calc = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + calc_without_section_props = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vz", gamma_m0=1.0) + assert calc == calc_without_section_props + + def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 355 * 4.74 / 1.732 * 0.99 + calc = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + v = -v + calc = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + v = 355 * 12.03 / 1.732 * 0.99 + calc = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + v = 355 * 2.882 / 1.732 * 0.99 + object.__setattr__(cross_section, "fabrication_method", "welded") + calc = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok shear force.""" + cross_section, section_properties = heb_steel_cross_section + object.__setattr__(cross_section, "fabrication_method", "hot-rolled") + v = 355 * 4.74 / 1.732 * 1.01 + calc = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() + + v = 355 * 12.03 / 1.732 * 1.01 + calc = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() + + v = 355 * 2.882 / 1.732 * 1.01 + object.__setattr__(cross_section, "fabrication_method", "welded") + calc = CheckStrengthShearClass12IProfile(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + + def test_report_shear(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test report output for shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 1 + calc = CheckStrengthShearClass12IProfile(cross_section, v, gamma_m0=1.0, section_properties=section_properties) + assert calc.report() + + def test_check_wrong_profile(self, chs_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test check() raises TypeError for non-I-profile.""" + cross_section, section_properties = chs_steel_cross_section + v = 1 + with pytest.raises(TypeError, match="The provided profile is not an I-profile"): + CheckStrengthShearClass12IProfile(cross_section, v, gamma_m0=1.0, section_properties=section_properties) + + +class TestCheckStrengthShearClass34: + """Tests for CheckStrengthShearClass34.""" + + def test_result_none(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test report output for shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 0 + calc = CheckStrengthShearClass34(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + calc_without_section_props = CheckStrengthShearClass34(cross_section, v, axis="Vz", gamma_m0=1.0) + assert calc == calc_without_section_props + + def test_result_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok shear force in Vz direction.""" + cross_section, section_properties = heb_steel_cross_section + v = 1379 * 0.99 + calc = CheckStrengthShearClass34(cross_section, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + assert calc.report() + + v = 607 * 0.99 + calc = CheckStrengthShearClass34(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + def test_result_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 1379 * 1.01 + calc = CheckStrengthShearClass34(cross_section, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() + + v = 607 * 1.01 + calc = CheckStrengthShearClass34(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_strength_tension.py b/tests/checks/eurocode/steel/test_strength_tension.py similarity index 100% rename from tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_strength_tension.py rename to tests/checks/eurocode/steel/test_strength_tension.py diff --git a/tests/checks/eurocode/steel/test_strength_torsion.py b/tests/checks/eurocode/steel/test_strength_torsion.py new file mode 100644 index 000000000..64ea95e45 --- /dev/null +++ b/tests/checks/eurocode/steel/test_strength_torsion.py @@ -0,0 +1,48 @@ +"""Tests for CheckStrengthStVenantTorsionClass1234 according to Eurocode 3.""" + +import pytest +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.eurocode.steel.strength_torsion import CheckStrengthStVenantTorsionClass1234 +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection + + +class TestCheckStrengthStVenantTorsionClass1234: + """Tests for CheckStrengthStVenantTorsionClass1234.""" + + def test_result_none(self, unp_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no torsion.""" + m_x = 0 + cross_section, section_properties = unp_steel_cross_section + calc = CheckStrengthStVenantTorsionClass1234(cross_section, m_x, gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0.0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + calc_without_section_props = CheckStrengthStVenantTorsionClass1234(cross_section, m_x, gamma_m0=1.0) + assert calc == calc_without_section_props + + def test_result_tension_ok(self, unp_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok tension load.""" + m_x = -0.3896 * 0.99 + cross_section, section_properties = unp_steel_cross_section + calc = CheckStrengthStVenantTorsionClass1234(cross_section, m_x, gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + assert calc.report() + + def test_result_tension_not_ok(self, unp_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok tension load.""" + m_x = 0.3896 * 1.01 + cross_section, section_properties = unp_steel_cross_section + calc = CheckStrengthStVenantTorsionClass1234(cross_section, m_x, gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() diff --git a/tests/checks/eurocode/steel/test_strength_torsion_shear.py b/tests/checks/eurocode/steel/test_strength_torsion_shear.py new file mode 100644 index 000000000..3ba32c961 --- /dev/null +++ b/tests/checks/eurocode/steel/test_strength_torsion_shear.py @@ -0,0 +1,165 @@ +"""Tests for torsion with shear strength according to Eurocode 3.""" + +import pytest +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.eurocode.steel.strength_torsion_shear import CheckStrengthTorsionShearClass12IProfile, CheckStrengthTorsionShearClass34 +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection + + +class TestCheckStrengthTorsionShearClass12IProfile: + """Tests for CheckStrengthTorsionShearClass12IProfile, using St. Venant torsion, for class 1 and 2 I-profiles.""" + + def test_result_none_v(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 0 + m_x = 1 + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + calc_without_section_props = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vz", gamma_m0=1.0) + assert calc == calc_without_section_props + + def test_result_none_m_x(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no torsional moment.""" + cross_section, section_properties = heb_steel_cross_section + m_x = 0 + v = 1 + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 585.023 * 0.99 + m_x = 10 + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + assert calc.report() + + v = -v + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + v = 1482.833 * 0.99 + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + v = 355.277 * 0.99 + object.__setattr__(cross_section, "fabrication_method", "welded") + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + + def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok shear force.""" + cross_section, section_properties = heb_steel_cross_section + object.__setattr__(cross_section, "fabrication_method", "hot-rolled") + v = 585.023 * 1.01 + m_x = 10 + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() + + v = 1482.833 * 1.01 + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + + v = 355.277 * 1.01 + object.__setattr__(cross_section, "fabrication_method", "welded") + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + + def test_check_wrong_profile(self, chs_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test check() raises TypeError for non-I-profile.""" + cross_section, section_properties = chs_steel_cross_section + with pytest.raises(TypeError, match="The provided profile is not an I-profile"): + CheckStrengthTorsionShearClass12IProfile(cross_section, m_x=10, v=1, gamma_m0=1.0, section_properties=section_properties) + + +class TestCheckStrengthTorsionShearClass34: + """Tests for TestCheckStrengthTorsionShearClass34, using St. Venant torsion, for class 3 and 4.""" + + def test_result_none_v(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 0 + m_x = 1 + calc = CheckStrengthTorsionShearClass34(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + calc_without_section_props = CheckStrengthTorsionShearClass34(cross_section, m_x, v, axis="Vz", gamma_m0=1.0) + assert calc == calc_without_section_props + + def test_result_none_m_x(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no torsional moment.""" + cross_section, section_properties = heb_steel_cross_section + m_x = 0 + v = 1 + calc = CheckStrengthTorsionShearClass34(cross_section, m_x, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert result.unity_check == 0 + assert result.factor_of_safety == float("inf") + assert result.provided == 0.0 + assert calc.report() + + def test_result_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for ok shear force in Vz direction.""" + cross_section, section_properties = heb_steel_cross_section + v = 690 * 0.99 + m_x = 7.66 * 0.99 + calc = CheckStrengthTorsionShearClass34(cross_section, m_x, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is True + assert pytest.approx(result.unity_check, 0.005) == 0.99 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 + assert calc.report() + + def test_result_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() for not ok shear force.""" + cross_section, section_properties = heb_steel_cross_section + v = 690 * 1.01 + m_x = 7.66 * 1.01 + calc = CheckStrengthTorsionShearClass34(cross_section, m_x, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + result = calc.result() + assert result.is_ok is False + assert pytest.approx(result.unity_check, 0.005) == 1.01 + assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 + assert calc.report() diff --git a/tests/codes/eurocode/en_1993_1_1_2005/chapter_6_ultimate_limit_state/test_formula_6_18_sub_av.py b/tests/codes/eurocode/en_1993_1_1_2005/chapter_6_ultimate_limit_state/test_formula_6_18_sub_av.py index 0cb31bb07..91672a6df 100644 --- a/tests/codes/eurocode/en_1993_1_1_2005/chapter_6_ultimate_limit_state/test_formula_6_18_sub_av.py +++ b/tests/codes/eurocode/en_1993_1_1_2005/chapter_6_ultimate_limit_state/test_formula_6_18_sub_av.py @@ -22,43 +22,70 @@ class TestForm6Dot18SubARolledIandHSection: def test_evaluation(self) -> None: """Tests the evaluation of the result.""" a = 10000.0 - b = 200.0 + b1 = 200.0 + b2 = 200.0 hw = 250.0 - r = 10.0 - tf = 15.0 + r1 = 10.0 + r2 = 10.0 + tf1 = 15.0 + tf2 = 15.0 tw = 8.0 eta = 1.0 - formula = Form6Dot18SubARolledIandHSection(a=a, b=b, hw=hw, r=r, tf=tf, tw=tw, eta=eta) + formula = Form6Dot18SubARolledIandHSection(a=a, b1=b1, b2=b2, hw=hw, r1=r1, r2=r2, tf1=tf1, tf2=tf2, tw=tw, eta=eta) manually_calculated_result = 4420.0 # mm^2 assert formula == pytest.approx(expected=manually_calculated_result, rel=1e-4) + def test_evaluation_non_symmetric(self) -> None: + """Tests the evaluation of the result for a non-symmetric profile.""" + a = 10000.0 + b1 = 180.0 + b2 = 200.0 + hw = 250.0 + r1 = 10.0 + r2 = 10.0 + tf1 = 15.0 + tf2 = 15.0 + tw = 8.0 + eta = 1.0 + + formula = Form6Dot18SubARolledIandHSection(a=a, b1=b1, b2=b2, hw=hw, r1=r1, r2=r2, tf1=tf1, tf2=tf2, tw=tw, eta=eta) + manually_calculated_result = 4720.0 # mm^2 + + assert formula == pytest.approx(expected=manually_calculated_result, rel=1e-4) + @pytest.mark.parametrize( - ("a", "b", "hw", "r", "tf", "tw", "eta"), + ("a", "b1", "b2", "hw", "r1", "r2", "tf1", "tf2", "tw", "eta"), [ - (-10000.0, 200.0, 250.0, 10.0, 15.0, 8.0, 1.0), # a is negative - (10000.0, -200.0, 250.0, 10.0, 15.0, 8.0, 1.0), # b is negative - (10000.0, 200.0, -250.0, 10.0, 15.0, 8.0, 1.0), # hw is negative - (10000.0, 200.0, 250.0, -10.0, 15.0, 8.0, 1.0), # r is negative - (10000.0, 200.0, 250.0, 10.0, -15.0, 8.0, 1.0), # tf is negative - (10000.0, 200.0, 250.0, 10.0, 15.0, -8.0, 1.0), # tw is negative - (10000.0, 200.0, 250.0, 10.0, 15.0, 8.0, -1.0), # eta is negative + (-10000.0, 200.0, 200.0, 250.0, 10.0, 10.0, 15.0, 15.0, 8.0, 1.0), # a is negative + (10000.0, -200.0, 200.0, 250.0, 10.0, 10.0, 15.0, 15.0, 8.0, 1.0), # b1 is negative + (10000.0, 200.0, -200.0, 250.0, 10.0, 10.0, 15.0, 15.0, 8.0, 1.0), # b2 is negative + (10000.0, 200.0, 200.0, -250.0, 10.0, 10.0, 15.0, 15.0, 8.0, 1.0), # hw is negative + (10000.0, 200.0, 200.0, 250.0, -10.0, 10.0, 15.0, 15.0, 8.0, 1.0), # r1 is negative + (10000.0, 200.0, 200.0, 250.0, 10.0, -10.0, 15.0, 15.0, 8.0, 1.0), # r2 is negative + (10000.0, 200.0, 200.0, 250.0, 10.0, 10.0, -15.0, 15.0, 8.0, 1.0), # tf1 is negative + (10000.0, 200.0, 200.0, 250.0, 10.0, 10.0, 15.0, -15.0, 8.0, 1.0), # tf2 is negative + (10000.0, 200.0, 200.0, 250.0, 10.0, 10.0, 15.0, 15.0, -8.0, 1.0), # tw is negative + (10000.0, 200.0, 200.0, 250.0, 10.0, 10.0, 15.0, 15.0, 8.0, -1.0), # eta is negative ], ) - def test_raise_error_when_invalid_values_are_given(self, a: float, b: float, hw: float, r: float, tf: float, tw: float, eta: float) -> None: + def test_raise_error_when_invalid_values_are_given( + self, a: float, b1: float, b2: float, hw: float, r1: float, r2: float, tf1: float, tf2: float, tw: float, eta: float + ) -> None: """Test invalid values.""" with pytest.raises(NegativeValueError): - Form6Dot18SubARolledIandHSection(a=a, b=b, hw=hw, r=r, tf=tf, tw=tw, eta=eta) + Form6Dot18SubARolledIandHSection(a=a, b1=b1, b2=b2, hw=hw, r1=r1, r2=r2, tf1=tf1, tf2=tf2, tw=tw, eta=eta) @pytest.mark.parametrize( ("representation", "expected"), [ ( "complete", - r"A_v = max(A - 2 \cdot b \cdot t_f + (t_w + 2 \cdot r) \cdot t_f; \eta \cdot h_w \cdot t_w) = " - r"max(10000.000 - 2 \cdot 200.000 \cdot 15.000 + (8.000 + 2 \cdot 10.000) \cdot 15.000; 1.000 \cdot 250.000 \cdot 8.000) = " - r"4420.000 \ mm^2", + r"A_v = \max(A - b_1 \cdot t_{f1} - b_2 \cdot t_{f2} + (t_w + 2 \cdot r_1) \cdot \frac{t_{f1}}{2} + " + r"(t_w + 2 \cdot r_2) \cdot \frac{t_{f2}}{2}; \eta \cdot h_w \cdot t_w) = " + r"\max(10000.000 - 200.000 \cdot 15.000 - 200.000 \cdot 15.000 + (8.000 + 2 \cdot 10.000) " + r"\cdot \frac{15.000}{2} + (8.000 + 2 \cdot 10.000) \cdot \frac{15.000}{2}; 1.000 \cdot 250.000 \cdot 8.000) = 4420.000 \ mm^2", ), ("short", r"A_v = 4420.000 \ mm^2"), ], @@ -66,14 +93,17 @@ def test_raise_error_when_invalid_values_are_given(self, a: float, b: float, hw: def test_latex(self, representation: str, expected: str) -> None: """Test the latex representation of the formula.""" a = 10000.0 - b = 200.0 + b1 = 200.0 + b2 = 200.0 hw = 250.0 - r = 10.0 - tf = 15.0 + r1 = 10.0 + r2 = 10.0 + tf1 = 15.0 + tf2 = 15.0 tw = 8.0 eta = 1.0 - latex = Form6Dot18SubARolledIandHSection(a=a, b=b, hw=hw, r=r, tf=tf, tw=tw, eta=eta).latex() + latex = Form6Dot18SubARolledIandHSection(a=a, b1=b1, b2=b2, hw=hw, r1=r1, r2=r2, tf1=tf1, tf2=tf2, tw=tw, eta=eta).latex() actual = { "complete": latex.complete, diff --git a/tests/utils/test_report.py b/tests/utils/test_report.py index 398b2bebc..2f2f10d83 100644 --- a/tests/utils/test_report.py +++ b/tests/utils/test_report.py @@ -103,7 +103,7 @@ def test_add_text_bold_and_italic(self, fixture_report: Report) -> None: expected = r"\textbf{\textit{This is bold and italic}}" + "\n" assert fixture_report.content == expected - def test_multiline_add_text_calls(self, fixture_report: Report) -> None: + def test_multline_add_text_calls(self, fixture_report: Report) -> None: """Test multline add_text calls.""" fixture_report.add_paragraph("First line.Second line.") expected = r"\txt{First line.Second line.}" + "\n" @@ -128,6 +128,19 @@ def test_add_equation_with_tag(self, fixture_report: Report) -> None: expected = r"\begin{equation} a^2+b^2=c^2 \tag{6.83} \end{equation}" + "\n" assert fixture_report.content == expected + def test_very_long_equation_splitting(self, fixture_report: Report) -> None: + """Test adding a very long equation that requires splitting.""" + long_equation = "a = b + c + d + e + f + g + h + i + j + k + l + m + n + o + p + q + r + s + t + u + v + w + x + y + z" + fixture_report.add_equation(long_equation, split_after=[(3, "+"), (6, "+"), (9, "+")]) + expected_parts = [ + r"\begin{multline} a = b + c + d + \\", + r"e + f + g + \\", + r"h + i + j + \\", + r"k + l + m + n + o + p + q + r + s + t + u + v + w + x + y + z \notag \end{multline}", + ] + for part in expected_parts: + assert part in fixture_report.content + def test_add_equation_method_chaining(self, fixture_report: Report) -> None: """Test that add_equation returns self for method chaining.""" result = fixture_report.add_equation("E=mc^2") diff --git a/tests/utils/test_report_to_word.py b/tests/utils/test_report_to_word.py index d3b4c59ed..28b1a7ab6 100644 --- a/tests/utils/test_report_to_word.py +++ b/tests/utils/test_report_to_word.py @@ -30,6 +30,12 @@ def test_complex_document_conversion(self) -> None: report.add_paragraph("test").add_equation("E=mc^2", inline=True).add_paragraph("more text.").add_newline() report.add_equation("E=mc^2", tag="4") report.add_equation(r"\int_a^b f(x)dx = F(b) - F(a)") + report.add_equation( + "A_v = max(A - 2 \\cdot b \\cdot t_f + (t_w + 2 \\cdot r) \\cdot t_f;" + " \\eta \\cdot h_w \\cdot t_w) = max(" + "12324.48 - 2 \\cdot 297.00 \\cdot 16.00 + (8.00 + 2 \\cdot 28.50) \\cdot 16.00;" + " 1.00 \\cdot 265.00 \\cdot 8.00) = 3860.48 \\ mm^2 " + ) report.add_list(["One", ["A", "B", "C"], "Two", ["A", ["I", "II", ["A", "B"], "III"]]], style="numbered") report.add_list(["First", "Second", ["Subfirst", "Subsecond"], "Third"], style="bulleted") report.add_paragraph("Here is a table:")