From 7a37a7c8228dfb7816e6367d3a37d3dd3177a3e9 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 31 Jan 2026 10:08:33 +0100 Subject: [PATCH 01/46] feat: add PlasticShearStrengthIProfileCheck class for shear force resistance checks and enhance Report class with equation splitting functionality --- .../checks/eurocode/steel/shear_strength.py | 190 ++++++++++++++++++ blueprints/utils/report.py | 36 +++- 2 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 blueprints/checks/eurocode/steel/shear_strength.py diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py new file mode 100644 index 000000000..2f735e3ef --- /dev/null +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -0,0 +1,190 @@ +"""Module for checking plastic shear force resistance of steel(Eurocode 3).""" + +from dataclasses import dataclass, field +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_17, + formula_6_18, + formula_6_18_sub_av, +) +from blueprints.codes.formula import Formula +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 PlasticShearStrengthIProfileCheck: + """Class to perform plastic shear force resistance check for steel I-profiles (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 (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.shear_strength import PlasticShearStrengthIProfileCheck + 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 = PlasticShearStrengthIProfileCheck(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] + _profile: IProfile = field(init=False, repr=False) + + 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.") + object.__setattr__(self, "_profile", self.steel_cross_section.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. + """ + if self.v == 0: + return {} + + # Get parameters from profile, average top and bottom flange properties + a = self.section_properties.area + b = (self.steel_cross_section.profile.top_flange_width + self.steel_cross_section.profile.bottom_flange_width) / 2 + tf = (self.steel_cross_section.profile.top_flange_thickness + self.steel_cross_section.profile.bottom_flange_thickness) / 2 + tw = self.steel_cross_section.profile.web_thickness + hw = self.steel_cross_section.profile.total_height - ( + self.steel_cross_section.profile.top_flange_thickness + self.steel_cross_section.profile.bottom_flange_thickness + ) + r = (self.steel_cross_section.profile.top_radius + self.steel_cross_section.profile.bottom_radius) / 2 + + if self.axis == "Vz" and self.steel_cross_section.fabrication_method == "rolled": + av = formula_6_18_sub_av.Form6Dot18SubARolledIandHSection(a=a, b=b, hw=hw, r=r, tf=tf, 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, + "shear_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() + if not steps: + return CheckResult(is_ok=True, unity_check=0.0) + provided = abs(self.v) * KN_TO_N + required = float(steps["shear_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: plastic shear force steel I-profile") + 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_on=[[2, "="], [3, "="]]) + report.add_paragraph("The shear resistance is calculated as follows:") + report.add_formula(formulas["shear_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 + + +if __name__ == "__main__": + from blueprints.checks.eurocode.steel.shear_strength import PlasticShearStrengthIProfileCheck + 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 = PlasticShearStrengthIProfileCheck(heb_300_s355, v, axis="Vz", gamma_m0=1.0) + calc.report().to_pdf("shear_strength.pdf", language="en") + import os + + os.startfile("shear_strength.pdf") diff --git a/blueprints/utils/report.py b/blueprints/utils/report.py index 3072102fe..48630081d 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_on: 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_on: 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_on=[(2, "="), (2, "+")] will split after second "=" and second "+" + to give: a = b + c = \\ 2 + \\ 3 = 5. Default is None. Returns ------- @@ -145,12 +150,34 @@ def add_equation( >>> report.add_equation(r"\\frac{a}{b}", inline=True) """ + + def _split_equation(eq: str, split_on: list[tuple[int, str]] | None) -> str: + if not split_on: + return eq + eq_mod = eq + # Sort by decreasing index so insertion doesn't affect later positions + for n, char in sorted(split_on, 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_on) + 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}} {eq_to_use} \tag{{{tag}}} \end{{multline}}" else: - self.content += rf"\begin{{equation}} {equation} \notag \end{{equation}}" + self.content += rf"\begin{{multline}} {eq_to_use} \notag \end{{multline}}" # Add a newline for visual separation self.content += "\n" @@ -165,6 +192,7 @@ def add_formula( include_source: bool = True, include_formula_number: bool = True, inline: bool = False, + split_on: list[tuple[int, str]] | None = None, ) -> Self: r"""Add a Blueprints formula to the report, for generic equations, use add_equation. @@ -227,7 +255,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_on=split_on) def add_heading(self, text: str, level: int = 1) -> Self: """Add a heading to the report. From ba289240d17f687a256e3af52d0101436919d51f Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 31 Jan 2026 11:07:20 +0100 Subject: [PATCH 02/46] feat: update equation handling in Report class to support multline environments and rename split_on to split_after for clarity --- .../checks/eurocode/steel/shear_strength.py | 6 ++--- blueprints/utils/_report_to_word.py | 24 +++++++++++-------- blueprints/utils/report.py | 22 ++++++++++------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index 2f735e3ef..1d7ec257c 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -161,7 +161,7 @@ def report(self, n: int = 2) -> Report: rf"The shear area $A_v$ is calculated as follows:" ) formulas = self.calculation_formula() - report.add_formula(formulas["shear_area"], n=n, split_on=[[2, "="], [3, "="]]) + report.add_formula(formulas["shear_area"], n=n, split_after=[[2, "="], [3, "="]]) report.add_paragraph("The shear resistance is calculated as follows:") report.add_formula(formulas["shear_resistance"], n=n) report.add_paragraph("The unity check is calculated as follows:") @@ -184,7 +184,7 @@ def report(self, n: int = 2) -> Report: heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) calc = PlasticShearStrengthIProfileCheck(heb_300_s355, v, axis="Vz", gamma_m0=1.0) - calc.report().to_pdf("shear_strength.pdf", language="en") + calc.report().to_word("shear_strength.docx", language="en") import os - os.startfile("shear_strength.pdf") + os.startfile("shear_strength.docx") diff --git a/blueprints/utils/_report_to_word.py b/blueprints/utils/_report_to_word.py index 2b84fcf21..e7fd14df0 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} 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\}", "newline": r"\\newline", } @@ -437,14 +437,18 @@ 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}", "").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 48630081d..2a082681d 100644 --- a/blueprints/utils/report.py +++ b/blueprints/utils/report.py @@ -119,7 +119,7 @@ def add_equation( equation: str, tag: str | None = None, inline: bool = False, - split_on: list[tuple[int, str]] | None = None, + split_after: list[tuple[int, str]] | None = None, ) -> Self: r"""Add an equation to the report. For adding Blueprints formulas, use add_formula instead. @@ -131,9 +131,9 @@ 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_on: list[tuple[int, str]], optional + 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_on=[(2, "="), (2, "+")] will split after second "=" and second "+" + 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 @@ -151,12 +151,12 @@ def add_equation( """ - def _split_equation(eq: str, split_on: list[tuple[int, str]] | None) -> str: - if not split_on: + 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_on, reverse=True): + for n, char in sorted(split_after, reverse=True): # Find nth occurrence of char idx = -1 count = 0 @@ -170,7 +170,7 @@ def _split_equation(eq: str, split_on: list[tuple[int, str]] | None) -> str: eq_mod = eq_mod[: idx + 1] + r" \\" + eq_mod[idx + 1 :] return eq_mod - eq_to_use = _split_equation(equation, split_on) + eq_to_use = _split_equation(equation, split_after) if inline: self.content += r"\txt{ " + rf"${eq_to_use}$" + f"{f' ({tag})' if tag else ''}" + r" }" @@ -192,7 +192,7 @@ def add_formula( include_source: bool = True, include_formula_number: bool = True, inline: bool = False, - split_on: list[tuple[int, str]] | None = None, + split_after: list[tuple[int, str]] | None = None, ) -> Self: r"""Add a Blueprints formula to the report, for generic equations, use add_equation. @@ -215,6 +215,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 ------- @@ -255,7 +259,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, split_on=split_on) + 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. From 84bd641d68f18ba33879a22c17492f814afb06e9 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 31 Jan 2026 11:16:00 +0100 Subject: [PATCH 03/46] feat: update Report class to support multline equations and enhance tests for equation handling --- blueprints/utils/report.py | 4 ++-- tests/utils/test_report.py | 27 ++++++++++++++++++++------- tests/utils/test_report_to_word.py | 6 ++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/blueprints/utils/report.py b/blueprints/utils/report.py index 2a082681d..aa1405814 100644 --- a/blueprints/utils/report.py +++ b/blueprints/utils/report.py @@ -538,7 +538,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}") 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}") @@ -556,7 +556,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}") 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/utils/test_report.py b/tests/utils/test_report.py index 398b2bebc..199f7f642 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" @@ -119,15 +119,28 @@ def test_add_text_method_chaining(self, fixture_report: Report) -> None: def test_add_equation_without_tag(self, fixture_report: Report) -> None: """Test adding equation without tag.""" fixture_report.add_equation("a^2+b^2=c^2") - expected = r"\begin{equation} a^2+b^2=c^2 \notag \end{equation}" + "\n" + expected = r"\begin{multline} a^2+b^2=c^2 \notag \end{multline}" + "\n" assert fixture_report.content == expected def test_add_equation_with_tag(self, fixture_report: Report) -> None: """Test adding equation with tag.""" fixture_report.add_equation("a^2+b^2=c^2", tag="6.83") - expected = r"\begin{equation} a^2+b^2=c^2 \tag{6.83} \end{equation}" + "\n" + expected = r"\begin{multline} a^2+b^2=c^2 \tag{6.83} \end{multline}" + "\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") @@ -147,7 +160,7 @@ def test_add_equation_inline_method_chaining(self, fixture_report: Report) -> No def test_add_formula(self, fixture_report: Report) -> None: """Test adding formula.""" fixture_report.add_formula(formula_6_5.Form6Dot5UnityCheckTensileStrength(n_ed=150000, n_t_rd=200000), options="complete") - expected = r"\begin{equation} CHECK" + expected = r"\begin{multline} CHECK" assert expected in fixture_report.content def test_add_formula_inline(self, fixture_report: Report) -> None: @@ -159,7 +172,7 @@ def test_add_formula_inline(self, fixture_report: Report) -> None: def test_add_formula_complete_with_units(self, fixture_report: Report) -> None: """Test adding formula with complete_with_units option.""" fixture_report.add_formula(formula_6_5.Form6Dot5UnityCheckTensileStrength(n_ed=150000, n_t_rd=200000), options="complete_with_units") - expected = r"\begin{equation} CHECK" + expected = r"\begin{multline} CHECK" assert expected in fixture_report.content def test_add_section(self, fixture_report: Report) -> None: @@ -386,8 +399,8 @@ def test_comprehensive_example_from_docstring(self) -> None: assert r"\textbf{This is bold text with newline after.}" in latex_document assert r"\textit{This is italic text with 4 newlines after.}" in latex_document assert r"\textbf{\textit{This is bold and italic text.}}" in latex_document - assert r"\begin{equation} E=mc^2 \tag{3.14} \end{equation}" in latex_document - assert r"\begin{equation} CHECK" in latex_document + assert r"\begin{multline} E=mc^2 \tag{3.14} \end{multline}" in latex_document + assert r"\begin{multline} CHECK" in latex_document assert r"$\frac{a}{b}$" in latex_document assert r"Parameter & Value & Unit" in latex_document assert r"\text{Length} & 10 & \text{m}" in latex_document 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:") From a7a61a55484b6ebfd1738fa2fa808c429430fbef Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 31 Jan 2026 11:18:50 +0100 Subject: [PATCH 04/46] feat: update report title for plastic shear strength check and modify output format to PDF --- .../checks/eurocode/steel/shear_strength.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index 1d7ec257c..31df7af94 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -150,7 +150,7 @@ def report(self, n: int = 2) -> Report: Report Report of the plastic shear force check. """ - report = Report("Check: plastic shear force steel I-profile") + 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 @@ -171,20 +171,3 @@ def report(self, n: int = 2) -> Report: else: report.add_paragraph("The check for plastic shear force does NOT satisfy the requirements.") return report - - -if __name__ == "__main__": - from blueprints.checks.eurocode.steel.shear_strength import PlasticShearStrengthIProfileCheck - 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 = PlasticShearStrengthIProfileCheck(heb_300_s355, v, axis="Vz", gamma_m0=1.0) - calc.report().to_word("shear_strength.docx", language="en") - import os - - os.startfile("shear_strength.docx") From e9d6768088c59ca386e37a2029f6bb56619480f2 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 16:57:37 +0100 Subject: [PATCH 05/46] feat: modify Form6Dot18SubARolledIandHSection to split flange effects and update tests accordingly --- .../formula_6_18_sub_av.py | 68 +++++++++++++------ .../test_formula_6_18_sub_av.py | 54 +++++++++------ 2 files changed, 79 insertions(+), 43 deletions(-) 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..15359693c 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.b_1:.{n}f}", + r"b_2": f"{self.b_2:.{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.r_1:.{n}f}", + r"r_2": f"{self.r_2:.{n}f}", + r"t_{f1}": f"{self.tf_1:.{n}f}", + r"t_{f2}": f"{self.tf_2:.{n}f}", r"t_w": f"{self.tw:.{n}f}", r"\eta": f"{self.eta:.{n}f}", }, 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..6257aed3d 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,52 @@ 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) @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 +75,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 + b_1 = 200.0 + b_2 = 200.0 hw = 250.0 - r = 10.0 - tf = 15.0 + r_1 = 10.0 + r_2 = 10.0 + tf_1 = 15.0 + tf_2 = 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, b_1=b_1, b_2=b_2, hw=hw, r_1=r_1, r_2=r_2, tf_1=tf_1, tf_2=tf_2, tw=tw, eta=eta).latex() actual = { "complete": latex.complete, From b6485e5d9af11d94050641fba28a7e830c242bd8 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 17:02:58 +0100 Subject: [PATCH 06/46] fix: correct attribute names in latex output for Form6Dot18SubARolledIandHSection --- .../formula_6_18_sub_av.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 15359693c..2a9f405b1 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 @@ -101,13 +101,13 @@ def latex(self, n: int = 3) -> LatexFormula: _equation, { r"A": f"{self.a:.{n}f}", - r"b_1": f"{self.b_1:.{n}f}", - r"b_2": f"{self.b_2:.{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_1": f"{self.r_1:.{n}f}", - r"r_2": f"{self.r_2:.{n}f}", - r"t_{f1}": f"{self.tf_1:.{n}f}", - r"t_{f2}": f"{self.tf_2:.{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}", }, From 914fc0e05126db77a7c6b6628a145b899e8893dc Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 17:05:00 +0100 Subject: [PATCH 07/46] refactor: rename variables in test_latex method for consistency --- .../test_formula_6_18_sub_av.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 6257aed3d..c62c2ccaf 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 @@ -75,17 +75,17 @@ def test_raise_error_when_invalid_values_are_given( def test_latex(self, representation: str, expected: str) -> None: """Test the latex representation of the formula.""" a = 10000.0 - b_1 = 200.0 - b_2 = 200.0 + b1 = 200.0 + b2 = 200.0 hw = 250.0 - r_1 = 10.0 - r_2 = 10.0 - tf_1 = 15.0 - tf_2 = 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_1=b_1, b_2=b_2, hw=hw, r_1=r_1, r_2=r_2, tf_1=tf_1, tf_2=tf_2, 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, From 5ef3281dd163d4ef01c503aa732c4be7eeb94713 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 17:08:11 +0100 Subject: [PATCH 08/46] fix: correct latex equation formatting in Form6Dot18SubARolledIandHSection --- .../chapter_6_ultimate_limit_state/formula_6_18_sub_av.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2a9f405b1..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 @@ -94,7 +94,7 @@ def _evaluate( def latex(self, n: int = 3) -> LatexFormula: """Returns LatexFormula object for formula 6.18suba.""" _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"\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( From baea411908798f9e69b9af5e8b94a9ff531e5d84 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 17:10:10 +0100 Subject: [PATCH 09/46] feat: update PlasticShearStrengthIProfileCheck to use individual flange properties for calculations --- blueprints/checks/eurocode/steel/shear_strength.py | 13 +++++++++---- .../steel/steel_cross_section.py | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index 31df7af94..d024a69a4 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -97,16 +97,21 @@ def calculation_formula(self) -> dict[str, Formula]: # Get parameters from profile, average top and bottom flange properties a = self.section_properties.area - b = (self.steel_cross_section.profile.top_flange_width + self.steel_cross_section.profile.bottom_flange_width) / 2 - tf = (self.steel_cross_section.profile.top_flange_thickness + self.steel_cross_section.profile.bottom_flange_thickness) / 2 + b_1 = self.steel_cross_section.profile.top_flange_width + b_2 = self.steel_cross_section.profile.bottom_flange_width + tf_1 = self.steel_cross_section.profile.top_flange_thickness + tf_2 = self.steel_cross_section.profile.bottom_flange_thickness tw = self.steel_cross_section.profile.web_thickness hw = self.steel_cross_section.profile.total_height - ( self.steel_cross_section.profile.top_flange_thickness + self.steel_cross_section.profile.bottom_flange_thickness ) - r = (self.steel_cross_section.profile.top_radius + self.steel_cross_section.profile.bottom_radius) / 2 + r_1 = self.steel_cross_section.profile.top_radius + r_2 = self.steel_cross_section.profile.bottom_radius if self.axis == "Vz" and self.steel_cross_section.fabrication_method == "rolled": - av = formula_6_18_sub_av.Form6Dot18SubARolledIandHSection(a=a, b=b, hw=hw, r=r, tf=tf, tw=tw, eta=1.0) + av = formula_6_18_sub_av.Form6Dot18SubARolledIandHSection( + a=a, b_1=b_1, b_2=b_2, hw=hw, r_1=r_1, r_2=r_2, tf_1=tf_1, tf_2=tf_2, 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" diff --git a/blueprints/structural_sections/steel/steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_section.py index a3fbcddd7..89701c1de 100644 --- a/blueprints/structural_sections/steel/steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_section.py @@ -32,7 +32,6 @@ class SteelCrossSection: """The material type of the steel.""" fabrication_method: Literal["rolled", "welded"] = "rolled" - @property def yield_strength(self) -> MPA: """ From 79e6cc52d4bd4b1b5ec8ca000e5b3f31f7d7fa7e Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 17:39:48 +0100 Subject: [PATCH 10/46] feat: update PlasticShearStrengthIProfileCheck to use consistent variable naming for flange properties --- .../checks/eurocode/steel/shear_strength.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index d024a69a4..ecebf7b38 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -96,22 +96,20 @@ def calculation_formula(self) -> dict[str, Formula]: return {} # Get parameters from profile, average top and bottom flange properties - a = self.section_properties.area - b_1 = self.steel_cross_section.profile.top_flange_width - b_2 = self.steel_cross_section.profile.bottom_flange_width - tf_1 = self.steel_cross_section.profile.top_flange_thickness - tf_2 = self.steel_cross_section.profile.bottom_flange_thickness - tw = self.steel_cross_section.profile.web_thickness - hw = self.steel_cross_section.profile.total_height - ( - self.steel_cross_section.profile.top_flange_thickness + self.steel_cross_section.profile.bottom_flange_thickness + 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] ) - r_1 = self.steel_cross_section.profile.top_radius - r_2 = self.steel_cross_section.profile.bottom_radius + 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 == "rolled": - av = formula_6_18_sub_av.Form6Dot18SubARolledIandHSection( - a=a, b_1=b_1, b_2=b_2, hw=hw, r_1=r_1, r_2=r_2, tf_1=tf_1, tf_2=tf_2, tw=tw, eta=1.0 - ) + 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" @@ -166,7 +164,7 @@ def report(self, n: int = 2) -> Report: 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, "="], [3, "="]]) + report.add_formula(formulas["shear_area"], n=n, split_after=[(2, "="), (3, "=")]) report.add_paragraph("The shear resistance is calculated as follows:") report.add_formula(formulas["shear_resistance"], n=n) report.add_paragraph("The unity check is calculated as follows:") From f9b2d6ebe49eb3240893790cf79dff93467f25c7 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 18:38:52 +0100 Subject: [PATCH 11/46] feat: add evaluation test for non-symmetric profile in Form6Dot18SubARolledIandHSection --- .../formula_6_18_sub_av.py | 14 ++++++++++++++ .../test_formula_6_18_sub_av.py | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) 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 ba919f57e..e480dd056 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 @@ -641,3 +641,17 @@ def latex(self, n: int = 3) -> LatexFormula: comparison_operator_label="=", unit="mm^2", ) + +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) +print(formula) \ No newline at end of file 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 c62c2ccaf..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 @@ -37,6 +37,24 @@ def test_evaluation(self) -> None: 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", "b1", "b2", "hw", "r1", "r2", "tf1", "tf2", "tw", "eta"), [ From b3399903bb31779077fa1b4e798de6357146a88f Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:07:43 +0100 Subject: [PATCH 12/46] feat: implement plastic shear strength checks for I profiles with comprehensive test coverage --- .../checks/eurocode/steel/shear_strength.py | 12 +-- .../test_shear_strength.py | 94 +++++++++++++++++++ 2 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index ecebf7b38..cb85816aa 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -92,9 +92,7 @@ def calculation_formula(self) -> dict[str, Formula]: dict[str, Formula] Calculation results keyed by formula number. Returns an empty dict if no shear force is applied. """ - if self.v == 0: - return {} - + # 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] @@ -121,7 +119,7 @@ def calculation_formula(self) -> dict[str, Formula]: check_shear = formula_6_17.Form6Dot17CheckShearForce(v_ed=v_ed, v_c_rd=v_pl_rd) return { "shear_area": av, - "shear_resistance": v_pl_rd, + "resistance": v_pl_rd, "check": check_shear, } @@ -134,10 +132,8 @@ def result(self) -> CheckResult: True if the shear force check passes, False otherwise. """ steps = self.calculation_formula() - if not steps: - return CheckResult(is_ok=True, unity_check=0.0) provided = abs(self.v) * KN_TO_N - required = float(steps["shear_resistance"]) + required = steps["resistance"] return CheckResult.from_comparison(provided=provided, required=required) def report(self, n: int = 2) -> Report: @@ -166,7 +162,7 @@ def report(self, n: int = 2) -> Report: formulas = self.calculation_formula() report.add_formula(formulas["shear_area"], n=n, split_after=[(2, "="), (3, "=")]) report.add_paragraph("The shear resistance is calculated as follows:") - report.add_formula(formulas["shear_resistance"], n=n) + 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: diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py new file mode 100644 index 000000000..d2ca67d8b --- /dev/null +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py @@ -0,0 +1,94 @@ +"""Tests for PlasticShearStrengthIProfileCheck according to Eurocode 3.""" + +import pytest +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.eurocode.steel.shear_strength import PlasticShearStrengthIProfileCheck +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection + + +class TestPlasticShearStrengthIProfileCheck: + """Tests for PlasticShearStrengthIProfileCheck.""" + + 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 = PlasticShearStrengthIProfileCheck(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 = PlasticShearStrengthIProfileCheck(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 * 2.73 * 0.99 + calc = PlasticShearStrengthIProfileCheck(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 = PlasticShearStrengthIProfileCheck(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 * 6.95 * 0.99 + calc = PlasticShearStrengthIProfileCheck(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 * 1.67 * 0.99 + object.__setattr__(cross_section, "fabrication_method", "welded") + calc = PlasticShearStrengthIProfileCheck(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", "rolled") + v = 355 * 2.73 * 1.01 + calc = PlasticShearStrengthIProfileCheck(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 * 6.95 * 1.01 + calc = PlasticShearStrengthIProfileCheck(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 * 1.67 * 1.01 + object.__setattr__(cross_section, "fabrication_method", "welded") + calc = PlasticShearStrengthIProfileCheck(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 = PlasticShearStrengthIProfileCheck(cross_section, v, gamma_m0=1.0, section_properties=section_properties) + assert calc.report() From f54f0e536b8966fe40260551f5b49e3af432b6e8 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:10:52 +0100 Subject: [PATCH 13/46] feat: add test for TypeError when checking non-I-profile in PlasticShearStrengthIProfileCheck --- .../ultimate_limit_states/test_shear_strength.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py index d2ca67d8b..17a25c9fa 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py @@ -92,3 +92,10 @@ def test_report_shear(self, heb_steel_cross_section: tuple[SteelCrossSection, Se v = 1 calc = PlasticShearStrengthIProfileCheck(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"): + PlasticShearStrengthIProfileCheck(cross_section, v, gamma_m0=1.0, section_properties=section_properties) \ No newline at end of file From c8797930aeea14df255db291498fbcf22d5a16c5 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:17:17 +0100 Subject: [PATCH 14/46] feat: update shear force calculations in PlasticShearStrengthIProfileCheck tests for accuracy --- .../ultimate_limit_states/test_shear_strength.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py index 17a25c9fa..17844bc83 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py @@ -1,6 +1,7 @@ """Tests for PlasticShearStrengthIProfileCheck according to Eurocode 3.""" import pytest +import numpy as np from sectionproperties.post.post import SectionProperties from blueprints.checks.eurocode.steel.shear_strength import PlasticShearStrengthIProfileCheck @@ -28,7 +29,7 @@ def test_result_none(self, heb_steel_cross_section: tuple[SteelCrossSection, Sec 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 * 2.73 * 0.99 + v = 355 * 4.74 / 1.732 * 0.99 calc = PlasticShearStrengthIProfileCheck(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) result = calc.result() assert result.is_ok is True @@ -42,14 +43,14 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, assert pytest.approx(result.unity_check, 0.005) == 0.99 assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 - v = 355 * 6.95 * 0.99 + v = 355 * 11.4 / 1.732 * 0.99 calc = PlasticShearStrengthIProfileCheck(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 * 1.67 * 0.99 + v = 355 * 2.89 / 1.732 * 0.99 object.__setattr__(cross_section, "fabrication_method", "welded") calc = PlasticShearStrengthIProfileCheck(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) result = calc.result() @@ -62,7 +63,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect """Test result() for not ok shear force.""" cross_section, section_properties = heb_steel_cross_section object.__setattr__(cross_section, "fabrication_method", "rolled") - v = 355 * 2.73 * 1.01 + v = 355 * 4.74 / 1.732 * 1.01 calc = PlasticShearStrengthIProfileCheck(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) result = calc.result() assert result.is_ok is False @@ -70,7 +71,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 assert calc.report() - v = 355 * 6.95 * 1.01 + v = 355 * 11.4 / 1.732 * 1.01 calc = PlasticShearStrengthIProfileCheck(cross_section, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) result = calc.result() assert result.is_ok is False @@ -78,7 +79,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 assert calc.report() - v = 355 * 1.67 * 1.01 + v = 355 * 1.89 / 1.732 * 1.01 object.__setattr__(cross_section, "fabrication_method", "welded") calc = PlasticShearStrengthIProfileCheck(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) result = calc.result() From 88334accbfdafc661b14100279ca35e44b42b0e7 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:27:45 +0100 Subject: [PATCH 15/46] feat: enhance plastic shear strength calculations and add main execution block for HEB profile --- .../checks/eurocode/steel/shear_strength.py | 15 +++++++++++++++ .../ultimate_limit_states/test_shear_strength.py | 8 ++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index cb85816aa..d9df32323 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -170,3 +170,18 @@ def report(self, n: int = 2) -> Report: else: report.add_paragraph("The check for plastic shear force does NOT satisfy the requirements.") return report + +if __name__ == "__main__": + 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 + v = 100 # Applied shear force in kN + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + object.__setattr__(heb_300_s355, "fabrication_method", "welded") + calc = PlasticShearStrengthIProfileCheck(heb_300_s355, v, axis="Vz", gamma_m0=1.0) + print(calc._profile.area) + print(calc._profile.area - 11 * 260) + print(calc.calculation_formula()) \ No newline at end of file diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py index 17844bc83..4195698af 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py @@ -43,14 +43,14 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, assert pytest.approx(result.unity_check, 0.005) == 0.99 assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 0.99 - v = 355 * 11.4 / 1.732 * 0.99 + v = 355 * 12.03 / 1.732 * 0.99 calc = PlasticShearStrengthIProfileCheck(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.89 / 1.732 * 0.99 + v = 355 * 2.882 / 1.732 * 0.99 object.__setattr__(cross_section, "fabrication_method", "welded") calc = PlasticShearStrengthIProfileCheck(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) result = calc.result() @@ -71,7 +71,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 assert calc.report() - v = 355 * 11.4 / 1.732 * 1.01 + v = 355 * 12.03 / 1.732 * 1.01 calc = PlasticShearStrengthIProfileCheck(cross_section, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) result = calc.result() assert result.is_ok is False @@ -79,7 +79,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect assert pytest.approx(result.factor_of_safety, 0.005) == 1 / 1.01 assert calc.report() - v = 355 * 1.89 / 1.732 * 1.01 + v = 355 * 2.882 / 1.732 * 1.01 object.__setattr__(cross_section, "fabrication_method", "welded") calc = PlasticShearStrengthIProfileCheck(cross_section, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) result = calc.result() From ffeb259535aab5a9cbc1388a98556190f74aa8f2 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:31:39 +0100 Subject: [PATCH 16/46] feat: remove main execution block from PlasticShearStrengthIProfileCheck module --- .../checks/eurocode/steel/shear_strength.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index d9df32323..cb85816aa 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -170,18 +170,3 @@ def report(self, n: int = 2) -> Report: else: report.add_paragraph("The check for plastic shear force does NOT satisfy the requirements.") return report - -if __name__ == "__main__": - 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 - v = 100 # Applied shear force in kN - - heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) - object.__setattr__(heb_300_s355, "fabrication_method", "welded") - calc = PlasticShearStrengthIProfileCheck(heb_300_s355, v, axis="Vz", gamma_m0=1.0) - print(calc._profile.area) - print(calc._profile.area - 11 * 260) - print(calc.calculation_formula()) \ No newline at end of file From 9137a1673c5f9bee7ac240f491fb80d576c07f84 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:36:49 +0100 Subject: [PATCH 17/46] feat: update shear area formula in report generation for PlasticShearStrengthIProfileCheck --- .../checks/eurocode/steel/shear_strength.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index cb85816aa..f73e680cb 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -160,7 +160,7 @@ def report(self, n: int = 2) -> Report: 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, "="), (3, "=")]) + report.add_formula(formulas["shear_area"], n=n, split_after=[(2, "="), (7, "+"), (4, "=")]) 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:") @@ -170,3 +170,19 @@ def report(self, n: int = 2) -> Report: else: report.add_paragraph("The check for plastic shear force does NOT satisfy the requirements.") return report + +if __name__ == "__main__": + 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 + v = 100 # Applied shear force in kN + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = PlasticShearStrengthIProfileCheck(heb_300_s355, v, axis="Vz", gamma_m0=1.0) + calc.report().to_pdf("shear_strength.pdf", language="nl") + calc.report().to_word("shear_strength.docx", language="nl") + import os + os.startfile("shear_strength.pdf") + os.startfile("shear_strength.docx") \ No newline at end of file From 72a68f91b6b579542136c1cc2d9bcfe948001320 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:37:07 +0100 Subject: [PATCH 18/46] fix: correct split index for shear area formula in report generation --- blueprints/checks/eurocode/steel/shear_strength.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index f73e680cb..623b3db80 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -160,7 +160,7 @@ def report(self, n: int = 2) -> Report: 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, "+"), (4, "=")]) + 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:") From fca4809637176bf0b7878d7c8188eba4cee0021e Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:39:16 +0100 Subject: [PATCH 19/46] feat: remove main execution block from PlasticShearStrengthIProfileCheck module --- .../checks/eurocode/steel/shear_strength.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index 623b3db80..86b9a656e 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -170,19 +170,3 @@ def report(self, n: int = 2) -> Report: else: report.add_paragraph("The check for plastic shear force does NOT satisfy the requirements.") return report - -if __name__ == "__main__": - 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 - v = 100 # Applied shear force in kN - - heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) - calc = PlasticShearStrengthIProfileCheck(heb_300_s355, v, axis="Vz", gamma_m0=1.0) - calc.report().to_pdf("shear_strength.pdf", language="nl") - calc.report().to_word("shear_strength.docx", language="nl") - import os - os.startfile("shear_strength.pdf") - os.startfile("shear_strength.docx") \ No newline at end of file From d0c6439d30131820abcb833c4d0bbceaa575f069 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:44:43 +0100 Subject: [PATCH 20/46] refactor: remove unused variables and print statement from formula_6_18_sub_av.py --- blueprints/checks/eurocode/steel/shear_strength.py | 1 - .../formula_6_18_sub_av.py | 14 -------------- .../ultimate_limit_states/test_shear_strength.py | 5 ++--- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index 86b9a656e..a23f62402 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -92,7 +92,6 @@ def calculation_formula(self) -> dict[str, Formula]: 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] 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 e480dd056..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 @@ -641,17 +641,3 @@ def latex(self, n: int = 3) -> LatexFormula: comparison_operator_label="=", unit="mm^2", ) - -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) -print(formula) \ No newline at end of file diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py index 4195698af..6aaa92084 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py @@ -1,7 +1,6 @@ """Tests for PlasticShearStrengthIProfileCheck according to Eurocode 3.""" import pytest -import numpy as np from sectionproperties.post.post import SectionProperties from blueprints.checks.eurocode.steel.shear_strength import PlasticShearStrengthIProfileCheck @@ -57,7 +56,7 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, 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.""" @@ -99,4 +98,4 @@ def test_check_wrong_profile(self, chs_steel_cross_section: tuple[SteelCrossSect cross_section, section_properties = chs_steel_cross_section v = 1 with pytest.raises(TypeError, match="The provided profile is not an I-profile"): - PlasticShearStrengthIProfileCheck(cross_section, v, gamma_m0=1.0, section_properties=section_properties) \ No newline at end of file + PlasticShearStrengthIProfileCheck(cross_section, v, gamma_m0=1.0, section_properties=section_properties) From ba970a7545c61c38892b92118a59ff7896f940a0 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 1 Feb 2026 19:46:23 +0100 Subject: [PATCH 21/46] fix: remove unnecessary blank line in test_shear_strength.py --- .../ultimate_limit_states/test_shear_strength.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py index 6aaa92084..a285f9416 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_shear_strength.py @@ -57,7 +57,6 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, 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 From e2f536ec03a84abd2692bad00fb6b1116eec0710 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Thu, 5 Feb 2026 20:01:01 +0100 Subject: [PATCH 22/46] feat: add stress calculation method to Profile class --- blueprints/structural_sections/_profile.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/blueprints/structural_sections/_profile.py b/blueprints/structural_sections/_profile.py index 146ad3a19..a15d53477 100644 --- a/blueprints/structural_sections/_profile.py +++ b/blueprints/structural_sections/_profile.py @@ -13,6 +13,7 @@ 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 @@ -185,6 +186,34 @@ 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) -> Callable[..., SectionProperties]: + """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[..., SectionProperties] + 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 + return section.calculate_stress( + n=-float(result_internal_force_1d.n) * 1e3, + vx=-float(result_internal_force_1d.vy) * 1e3, + vy=float(result_internal_force_1d.vz) * 1e3, + mxx=-float(result_internal_force_1d.my) * 1e6, + myy=float(result_internal_force_1d.mz) * 1e6, + mzz=-float(result_internal_force_1d.mx) * 1e6, + ) + def plot(self, plotter: Callable[[Any], plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: """Plot the profile. Making use of the standard plotter. From 5849cecd559e0b087900dc8d54e0cdfce40bfce2 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Thu, 5 Feb 2026 21:14:59 +0100 Subject: [PATCH 23/46] feat: add TorsionStrengthCheck module for torsional shear stress resistance --- .../checks/eurocode/steel/torsion_strength.py | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 blueprints/checks/eurocode/steel/torsion_strength.py diff --git a/blueprints/checks/eurocode/steel/torsion_strength.py b/blueprints/checks/eurocode/steel/torsion_strength.py new file mode 100644 index 000000000..80fe32b52 --- /dev/null +++ b/blueprints/checks/eurocode/steel/torsion_strength.py @@ -0,0 +1,182 @@ +"""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, NMM_TO_KNM +from blueprints.utils.report import Report + + +@dataclass(frozen=True) +class TorsionStrengthCheck: + """Class to perform torsion resistance check (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. + mx : KNM + The applied shear force (positive value, in kN). + 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) + mx = 10 # Applied torsional moment in kNm + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = TorsionStrengthCheck(heb_300_s355, mx, gamma_m0=1.0) + calc.report().to_word("torsion_strength.docx", language="nl") + + """ + + steel_cross_section: SteelCrossSection + mx: KNM = 0 + gamma_m0: DIMENSIONLESS = 1.0 + section_properties: SectionProperties | None = None + name: str = "Torsion 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.""" + 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 torsion force resistance check. + + Returns + ------- + dict[str, Formula] + 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 + ) + + stress = self.steel_cross_section.profile.calculate_stress(rif1d) + sig_zx_mzz = stress.get_stress()[0]["sig_zx_mzz"] + sig_zy_mzz = stress.get_stress()[0]["sig_zy_mzz"] + max_mzz_zxy = max((sig_zx_mzz**2 + sig_zy_mzz**2) ** 0.5) + + t_rd = self.steel_cross_section.yield_strength / self.gamma_m0 / np.sqrt(3) / max_mzz_zxy * KNM_TO_NMM + + check_torsion = Form6Dot23CheckTorsionalMoment(t_ed=self.mx, t_rd=t_rd * NMM_TO_KNM) + + return { + "kNm_unit_stress": max_mzz_zxy, + "resistance": t_rd * NMM_TO_KNM, + "check": check_torsion, + } + + def result(self) -> CheckResult: + """Calculate result of torsion force resistance. + + Returns + ------- + CheckResult + True if the torsion force check passes, False otherwise. + """ + steps = self.calculation_formula() + provided = self.mx + 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 force check. + + Parameters + ---------- + n : int, optional + Number of decimal places for numerical values in the report (default is 2). + + Returns + ------- + Report + Report of the torsion force check. + """ + report = Report("Check: torsion force steel beam") + if self.mx == 0: + report.add_paragraph("No torsion force was applied; therefore, no torsion force check is necessary.") + return report + profile_name = self.steel_cross_section.profile.name + steel_quality = self.steel_cross_section.material.steel_class.name + mx_val = f"{self.mx:.{n}f}" + unit_stress_val = f"{self.calculation_formula()['kNm_unit_stress']:.{n}f}" + report.add_paragraph( + rf"Profile {profile_name} with steel quality {steel_quality} " + rf"is loaded with a torsion force of {mx_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:" + ) + + fy = self.steel_cross_section.yield_strength + gamma_m0 = self.gamma_m0 + unit_stress = self.calculation_formula()["kNm_unit_stress"] + result = self.calculation_formula()["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:") + report.add_formula(self.calculation_formula()["check"], n=n) + if self.result().is_ok: + report.add_paragraph("The check for torsion force satisfies the requirements.") + else: + report.add_paragraph("The check for torsion force does NOT satisfy the requirements.") + return report + + +if __name__ == "__main__": + 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) + mx = 10 # Applied torsional moment in kNm + + heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) + calc = TorsionStrengthCheck(heb_300_s355, mx, gamma_m0=1.0) + calc.report().to_pdf("torsion_strength.pdf") + calc.report().to_word("torsion_strength.docx", language="nl") + import os + + os.startfile("torsion_strength.pdf") + os.startfile("torsion_strength.docx") From 4bd30b1ba3371860321d6047844e92dcc5d338a8 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Fri, 6 Feb 2026 16:44:19 +0100 Subject: [PATCH 24/46] feat: enhance TorsionStrengthCheck with additional calculations and tests --- .../checks/eurocode/steel/torsion_strength.py | 28 ++--------- .../ultimate_limit_states/conftest.py | 9 ++++ .../test_torsion_strength.py | 48 +++++++++++++++++++ 3 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_strength.py diff --git a/blueprints/checks/eurocode/steel/torsion_strength.py b/blueprints/checks/eurocode/steel/torsion_strength.py index 80fe32b52..310aba87c 100644 --- a/blueprints/checks/eurocode/steel/torsion_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_strength.py @@ -13,7 +13,6 @@ 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, NMM_TO_KNM from blueprints.utils.report import Report @@ -95,13 +94,14 @@ def calculation_formula(self) -> dict[str, Formula]: sig_zy_mzz = stress.get_stress()[0]["sig_zy_mzz"] max_mzz_zxy = max((sig_zx_mzz**2 + sig_zy_mzz**2) ** 0.5) - t_rd = self.steel_cross_section.yield_strength / self.gamma_m0 / np.sqrt(3) / max_mzz_zxy * KNM_TO_NMM + t_rd = self.steel_cross_section.yield_strength / self.gamma_m0 / np.sqrt(3) / max_mzz_zxy + t_ed = abs(self.mx) - check_torsion = Form6Dot23CheckTorsionalMoment(t_ed=self.mx, t_rd=t_rd * NMM_TO_KNM) + check_torsion = Form6Dot23CheckTorsionalMoment(t_ed=t_ed, t_rd=t_rd) return { "kNm_unit_stress": max_mzz_zxy, - "resistance": t_rd * NMM_TO_KNM, + "resistance": t_rd, "check": check_torsion, } @@ -114,7 +114,7 @@ def result(self) -> CheckResult: True if the torsion force check passes, False otherwise. """ steps = self.calculation_formula() - provided = self.mx + provided = abs(self.mx) required = steps["resistance"] return CheckResult.from_comparison(provided=provided, required=float(required)) @@ -162,21 +162,3 @@ def report(self, n: int = 2) -> Report: else: report.add_paragraph("The check for torsion force does NOT satisfy the requirements.") return report - - -if __name__ == "__main__": - 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) - mx = 10 # Applied torsional moment in kNm - - heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) - calc = TorsionStrengthCheck(heb_300_s355, mx, gamma_m0=1.0) - calc.report().to_pdf("torsion_strength.pdf") - calc.report().to_word("torsion_strength.docx", language="nl") - import os - - os.startfile("torsion_strength.pdf") - os.startfile("torsion_strength.docx") diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/conftest.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/conftest.py index 32d218fb3..7d79e95ac 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/conftest.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/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/en_1993_1_1_2005/ultimate_limit_states/test_torsion_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_strength.py new file mode 100644 index 000000000..9d15cacd0 --- /dev/null +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_strength.py @@ -0,0 +1,48 @@ +"""Tests for TorsionStrengthCheck according to Eurocode 3.""" + +import pytest +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.eurocode.steel.torsion_strength import TorsionStrengthCheck +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection + + +class TestTorsionStrengthCheck: + """Tests for TorsionStrengthCheck.""" + + def test_result_none(self, unp_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + """Test result() returns True for no torsion.""" + mx = 0 + cross_section, section_properties = unp_steel_cross_section + calc = TorsionStrengthCheck(cross_section, mx, 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 = TorsionStrengthCheck(cross_section, mx, 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.""" + mx = -0.3896 * 0.99 + cross_section, section_properties = unp_steel_cross_section + calc = TorsionStrengthCheck(cross_section, mx, 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.""" + mx = 0.3896 * 1.01 + cross_section, section_properties = unp_steel_cross_section + calc = TorsionStrengthCheck(cross_section, mx, 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() From 86cb3514320b373e4a9178b2248f2d42ca21e87d Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Fri, 6 Feb 2026 19:45:28 +0100 Subject: [PATCH 25/46] feat: optimize torsion strength calculations and improve report clarity --- .../eurocode/steel/compression_strength.py | 8 ++- .../checks/eurocode/steel/tension_strength.py | 8 ++- .../checks/eurocode/steel/torsion_strength.py | 53 +++++++++++-------- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/blueprints/checks/eurocode/steel/compression_strength.py b/blueprints/checks/eurocode/steel/compression_strength.py index 2cca3519f..d98dab82f 100644 --- a/blueprints/checks/eurocode/steel/compression_strength.py +++ b/blueprints/checks/eurocode/steel/compression_strength.py @@ -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/tension_strength.py b/blueprints/checks/eurocode/steel/tension_strength.py index ea62623c2..e2073a377 100644 --- a/blueprints/checks/eurocode/steel/tension_strength.py +++ b/blueprints/checks/eurocode/steel/tension_strength.py @@ -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/torsion_strength.py b/blueprints/checks/eurocode/steel/torsion_strength.py index 310aba87c..16f232faf 100644 --- a/blueprints/checks/eurocode/steel/torsion_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_strength.py @@ -13,6 +13,7 @@ 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 @@ -74,7 +75,7 @@ def __post_init__(self) -> None: object.__setattr__(self, "section_properties", section_properties) def calculation_formula(self) -> dict[str, Formula]: - """Calculate torsion force resistance check. + """Calculate torsion resistance check. Returns ------- @@ -89,37 +90,37 @@ def calculation_formula(self) -> dict[str, Formula]: mx=1, # 1 kNm ) - stress = self.steel_cross_section.profile.calculate_stress(rif1d) - sig_zx_mzz = stress.get_stress()[0]["sig_zx_mzz"] - sig_zy_mzz = stress.get_stress()[0]["sig_zy_mzz"] - max_mzz_zxy = max((sig_zx_mzz**2 + sig_zy_mzz**2) ** 0.5) + unit_stress = self.steel_cross_section.profile.calculate_stress(rif1d) + unit_sig_zx_mzz = unit_stress.get_stress()[0]["sig_zx_mzz"] + unit_sig_zy_mzz = unit_stress.get_stress()[0]["sig_zy_mzz"] + unit_max_mzz_zxy = max((unit_sig_zx_mzz**2 + unit_sig_zy_mzz**2) ** 0.5) - t_rd = self.steel_cross_section.yield_strength / self.gamma_m0 / np.sqrt(3) / max_mzz_zxy + t_rd = self.steel_cross_section.yield_strength / self.gamma_m0 / np.sqrt(3) / unit_max_mzz_zxy t_ed = abs(self.mx) check_torsion = Form6Dot23CheckTorsionalMoment(t_ed=t_ed, t_rd=t_rd) return { - "kNm_unit_stress": max_mzz_zxy, + "unit_shear_stress": unit_max_mzz_zxy, "resistance": t_rd, "check": check_torsion, } def result(self) -> CheckResult: - """Calculate result of torsion force resistance. + """Calculate result of torsion resistance. Returns ------- CheckResult - True if the torsion force check passes, False otherwise. + True if the torsion check passes, False otherwise. """ steps = self.calculation_formula() - provided = abs(self.mx) - required = steps["resistance"] + provided = abs(self.mx) * 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 force check. + """Returns the report for the torsion check. Parameters ---------- @@ -129,36 +130,44 @@ def report(self, n: int = 2) -> Report: Returns ------- Report - Report of the torsion force check. + Report of the torsion check. """ - report = Report("Check: torsion force steel beam") + report = Report("Check: torsion steel beam") if self.mx == 0: - report.add_paragraph("No torsion force was applied; therefore, no torsion force check is necessary.") + 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 mx_val = f"{self.mx:.{n}f}" - unit_stress_val = f"{self.calculation_formula()['kNm_unit_stress']:.{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 force of {mx_val} kNm. " + rf"is loaded with a torsion of {mx_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 = self.calculation_formula()["kNm_unit_stress"] - result = self.calculation_formula()["resistance"] + 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:") - 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 torsion force satisfies the requirements.") + report.add_paragraph("The check for torsion satisfies the requirements.") else: - report.add_paragraph("The check for torsion force does NOT satisfy the requirements.") + report.add_paragraph("The check for torsion does NOT satisfy the requirements.") return report From 665e2becdf5279c4e5aa615e8965ada163a4c4d8 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Fri, 6 Feb 2026 20:08:36 +0100 Subject: [PATCH 26/46] feat: add TorsionWithShearStrengthIProfileCheck module and corresponding tests --- .../steel/torsion_with_shear_strength.py | 204 ++++++++++++++++++ .../test_torsion_with_shear_strength.py | 108 ++++++++++ 2 files changed, 312 insertions(+) create mode 100644 blueprints/checks/eurocode/steel/torsion_with_shear_strength.py create mode 100644 tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py diff --git a/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py b/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py new file mode 100644 index 000000000..318743c4c --- /dev/null +++ b/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py @@ -0,0 +1,204 @@ +"""Module for checking torsional shear stress resistance with shear force present (Eurocode 2, formula 6.23).""" + +from dataclasses import dataclass, field +from typing import ClassVar, Literal + +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.check_result import CheckResult +from blueprints.checks.eurocode.steel.shear_strength import PlasticShearStrengthIProfileCheck +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_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 TorsionWithShearStrengthIProfileCheck: + """Class to perform torsion resistance check with extra shear force for I profiles (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. + mx : KNM + The applied shear force (positive value, in kN). + 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.torsion_with_shear_strength import TorsionWithShearStrengthIProfileCheck + 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) + mx = 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 = TorsionWithShearStrengthIProfileCheck(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 + mx: 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" + source_docs: ClassVar[list] = [EN_1993_1_1_2005] + _profile: IProfile = field(init=False, repr=False) + + 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.") + object.__setattr__(self, "_profile", self.steel_cross_section.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 torsion resistance check. + + Returns + ------- + dict[str, Formula] + Calculation results keyed by formula number. Returns an empty dict if no torsion is applied. + """ + shear_calculation = PlasticShearStrengthIProfileCheck( + 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_zx_mzz = unit_stress.get_stress()[0]["sig_zx_mzz"] + unit_sig_zy_mzz = unit_stress.get_stress()[0]["sig_zy_mzz"] + unit_max_mzz_zxy = max((unit_sig_zx_mzz**2 + unit_sig_zy_mzz**2) ** 0.5) + + tau_t_ed = abs(self.mx) * unit_max_mzz_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_mzz_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.mx == 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.mx == 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 + mx_val = f"{self.mx:.{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.mx:.{n}f}" + + report.add_paragraph( + rf"Profile {profile_name} with steel quality {steel_quality} " + rf"is loaded with a torsion of {mx_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:" + ) + + report.add_formula(formulas["shear_area"], n=n, split_after=[(2, "="), (7, "+"), (3, "=")]) + report.add_formula(formulas["raw_shear_resistance"], n=n) + report.add_paragraph("Next, the combined torsion and 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 torsion satisfies the requirements.") + else: + report.add_paragraph("The check for torsion does NOT satisfy the requirements.") + return report diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py new file mode 100644 index 000000000..0d3c15086 --- /dev/null +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py @@ -0,0 +1,108 @@ +"""Tests for torsion with shear strength according to Eurocode 3.""" + +import pytest +from sectionproperties.post.post import SectionProperties + +from blueprints.checks.eurocode.steel.torsion_with_shear_strength import TorsionWithShearStrengthIProfileCheck +from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection + + +class TestTorsionWithShearStrengthIProfileCheck: + """Tests for TorsionWithShearStrengthIProfileCheck.""" + + 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 + mx = 1 + calc = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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 = TorsionWithShearStrengthIProfileCheck(cross_section, mx, v, axis="Vz", gamma_m0=1.0) + assert calc == calc_without_section_props + + def test_result_none_mx(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 + mx = 0 + v = 1 + calc = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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 + mx = 10 + calc = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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 = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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 = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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 = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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", "rolled") + v = 585.023 * 1.01 + mx = 10 + calc = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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 = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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.277 * 1.01 + object.__setattr__(cross_section, "fabrication_method", "welded") + calc = TorsionWithShearStrengthIProfileCheck(cross_section, mx, 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"): + TorsionWithShearStrengthIProfileCheck(cross_section, mx=10, v=1, gamma_m0=1.0, section_properties=section_properties) From 4617877484942a64ec98c18aaafe79a53b37fc03 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Fri, 6 Feb 2026 20:31:21 +0100 Subject: [PATCH 27/46] feat: rename TorsionStrengthCheck to StVenantTorsionStrengthCheck and update related tests --- .../checks/eurocode/steel/torsion_strength.py | 2 +- .../steel/torsion_with_shear_strength.py | 2 +- .../test_torsion_strength.py | 16 ++++++++-------- .../test_torsion_with_shear_strength.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/blueprints/checks/eurocode/steel/torsion_strength.py b/blueprints/checks/eurocode/steel/torsion_strength.py index 16f232faf..001126267 100644 --- a/blueprints/checks/eurocode/steel/torsion_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_strength.py @@ -18,7 +18,7 @@ @dataclass(frozen=True) -class TorsionStrengthCheck: +class StVenantTorsionStrengthCheck: """Class to perform torsion resistance check (Eurocode 3). Coordinate System: diff --git a/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py b/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py index 318743c4c..7d82fac49 100644 --- a/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py @@ -21,7 +21,7 @@ @dataclass(frozen=True) class TorsionWithShearStrengthIProfileCheck: - """Class to perform torsion resistance check with extra shear force for I profiles (Eurocode 3). + """Class to perform torsion resistance check with extra shear force for I profiles (Eurocode 3), using St. Venant torsion. Coordinate System: diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_strength.py index 9d15cacd0..df74eee4a 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_strength.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_strength.py @@ -1,20 +1,20 @@ -"""Tests for TorsionStrengthCheck according to Eurocode 3.""" +"""Tests for StVenantTorsionStrengthCheck according to Eurocode 3.""" import pytest from sectionproperties.post.post import SectionProperties -from blueprints.checks.eurocode.steel.torsion_strength import TorsionStrengthCheck +from blueprints.checks.eurocode.steel.torsion_strength import StVenantTorsionStrengthCheck from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection -class TestTorsionStrengthCheck: - """Tests for TorsionStrengthCheck.""" +class TestStVenantTorsionStrengthCheck: + """Tests for StVenantTorsionStrengthCheck.""" def test_result_none(self, unp_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: """Test result() returns True for no torsion.""" mx = 0 cross_section, section_properties = unp_steel_cross_section - calc = TorsionStrengthCheck(cross_section, mx, gamma_m0=1.0, section_properties=section_properties) + calc = StVenantTorsionStrengthCheck(cross_section, mx, gamma_m0=1.0, section_properties=section_properties) result = calc.result() assert result.is_ok is True assert result.unity_check == 0.0 @@ -22,14 +22,14 @@ def test_result_none(self, unp_steel_cross_section: tuple[SteelCrossSection, Sec assert result.provided == 0.0 assert calc.report() - calc_without_section_props = TorsionStrengthCheck(cross_section, mx, gamma_m0=1.0) + calc_without_section_props = StVenantTorsionStrengthCheck(cross_section, mx, 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.""" mx = -0.3896 * 0.99 cross_section, section_properties = unp_steel_cross_section - calc = TorsionStrengthCheck(cross_section, mx, gamma_m0=1.0, section_properties=section_properties) + calc = StVenantTorsionStrengthCheck(cross_section, mx, 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 @@ -40,7 +40,7 @@ def test_result_tension_not_ok(self, unp_steel_cross_section: tuple[SteelCrossSe """Test result() for not ok tension load.""" mx = 0.3896 * 1.01 cross_section, section_properties = unp_steel_cross_section - calc = TorsionStrengthCheck(cross_section, mx, gamma_m0=1.0, section_properties=section_properties) + calc = StVenantTorsionStrengthCheck(cross_section, mx, 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 diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py index 0d3c15086..e40376360 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py @@ -8,7 +8,7 @@ class TestTorsionWithShearStrengthIProfileCheck: - """Tests for TorsionWithShearStrengthIProfileCheck.""" + """Tests for TorsionWithShearStrengthIProfileCheck, using St. Venant torsion.""" def test_result_none_v(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: """Test result() returns True for no shear force.""" From 84fa85b882a2f40a38c7c5620e34dafcfa0fce03 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 08:41:07 +0100 Subject: [PATCH 28/46] test: add report assertion to shear strength test cases --- .../ultimate_limit_states/test_torsion_with_shear_strength.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py index e40376360..85469b258 100644 --- a/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py +++ b/tests/checks/eurocode/en_1993_1_1_2005/ultimate_limit_states/test_torsion_with_shear_strength.py @@ -49,6 +49,7 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, 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 = TorsionWithShearStrengthIProfileCheck(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) @@ -91,7 +92,6 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect 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.277 * 1.01 object.__setattr__(cross_section, "fabrication_method", "welded") From ca29d2c5b318dd6e4436b2cf13f95cb1ace26e00 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 08:50:54 +0100 Subject: [PATCH 29/46] feat: optimize stress calculation in torsion checks and update return type in Profile class --- blueprints/checks/eurocode/steel/torsion_strength.py | 2 +- .../checks/eurocode/steel/torsion_with_shear_strength.py | 3 ++- blueprints/structural_sections/_profile.py | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/blueprints/checks/eurocode/steel/torsion_strength.py b/blueprints/checks/eurocode/steel/torsion_strength.py index 001126267..63e3eab81 100644 --- a/blueprints/checks/eurocode/steel/torsion_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_strength.py @@ -93,7 +93,7 @@ def calculation_formula(self) -> dict[str, Formula]: unit_stress = self.steel_cross_section.profile.calculate_stress(rif1d) unit_sig_zx_mzz = unit_stress.get_stress()[0]["sig_zx_mzz"] unit_sig_zy_mzz = unit_stress.get_stress()[0]["sig_zy_mzz"] - unit_max_mzz_zxy = max((unit_sig_zx_mzz**2 + unit_sig_zy_mzz**2) ** 0.5) + unit_max_mzz_zxy = np.max(np.sqrt(np.array(unit_sig_zx_mzz) ** 2 + np.array(unit_sig_zy_mzz) ** 2)) t_rd = self.steel_cross_section.yield_strength / self.gamma_m0 / np.sqrt(3) / unit_max_mzz_zxy t_ed = abs(self.mx) diff --git a/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py b/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py index 7d82fac49..8c9742f80 100644 --- a/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from typing import ClassVar, Literal +import numpy as np from sectionproperties.post.post import SectionProperties from blueprints.checks.check_result import CheckResult @@ -119,7 +120,7 @@ def calculation_formula(self) -> dict[str, Formula]: unit_stress = self.steel_cross_section.profile.calculate_stress(rif1d) unit_sig_zx_mzz = unit_stress.get_stress()[0]["sig_zx_mzz"] unit_sig_zy_mzz = unit_stress.get_stress()[0]["sig_zy_mzz"] - unit_max_mzz_zxy = max((unit_sig_zx_mzz**2 + unit_sig_zy_mzz**2) ** 0.5) + unit_max_mzz_zxy = np.max(np.sqrt(np.array(unit_sig_zx_mzz) ** 2 + np.array(unit_sig_zy_mzz) ** 2)) tau_t_ed = abs(self.mx) * unit_max_mzz_zxy v_ed = abs(self.v) * KN_TO_N diff --git a/blueprints/structural_sections/_profile.py b/blueprints/structural_sections/_profile.py index a15d53477..d0fc8502f 100644 --- a/blueprints/structural_sections/_profile.py +++ b/blueprints/structural_sections/_profile.py @@ -9,6 +9,7 @@ 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 @@ -186,7 +187,7 @@ 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) -> Callable[..., SectionProperties]: + def calculate_stress(self, result_internal_force_1d: ResultInternalForce1D) -> StressPost: """Calculate the stress distribution for the profile given internal forces. Parameters @@ -196,7 +197,7 @@ def calculate_stress(self, result_internal_force_1d: ResultInternalForce1D) -> C Returns ------- - Callable[..., SectionProperties] + Callable[..., StressPost] A function that calculates the stress distribution when called. """ section = self._section() From 17def6cc69c66bf3b74a20a57489c85dad7b45fb Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 08:54:15 +0100 Subject: [PATCH 30/46] feat: simplify name of torsion strength check for clarity --- blueprints/checks/eurocode/steel/torsion_strength.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/checks/eurocode/steel/torsion_strength.py b/blueprints/checks/eurocode/steel/torsion_strength.py index 63e3eab81..035e7bd0a 100644 --- a/blueprints/checks/eurocode/steel/torsion_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_strength.py @@ -65,7 +65,7 @@ class StVenantTorsionStrengthCheck: mx: KNM = 0 gamma_m0: DIMENSIONLESS = 1.0 section_properties: SectionProperties | None = None - name: str = "Torsion strength check for steel I-profiles" + name: str = "Torsion strength check" source_docs: ClassVar[list] = [EN_1993_1_1_2005] def __post_init__(self) -> None: From cdc567601b55a9dd862670e3fe0f3b957bfb8b40 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 10:03:26 +0100 Subject: [PATCH 31/46] refactor: remove unused profile attribute in TorsionWithShearStrengthIProfileCheck --- .../checks/eurocode/steel/torsion_with_shear_strength.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py b/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py index 8c9742f80..da4d5c6bc 100644 --- a/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_with_shear_strength.py @@ -1,6 +1,6 @@ """Module for checking torsional shear stress resistance with shear force present (Eurocode 2, formula 6.23).""" -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import ClassVar, Literal import numpy as np @@ -78,13 +78,11 @@ class TorsionWithShearStrengthIProfileCheck: section_properties: SectionProperties | None = None name: str = "Torsion strength check for steel I-profiles" source_docs: ClassVar[list] = [EN_1993_1_1_2005] - _profile: IProfile = field(init=False, repr=False) 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.") - object.__setattr__(self, "_profile", self.steel_cross_section.profile) if self.section_properties is None: section_properties = self.steel_cross_section.profile.section_properties() object.__setattr__(self, "section_properties", section_properties) From 3dbacdf7fdca572b3f70a533ff5f23b04ba571b9 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 10:04:01 +0100 Subject: [PATCH 32/46] refactor: remove unused profile attribute from PlasticShearStrengthIProfileCheck --- blueprints/checks/eurocode/steel/shear_strength.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index a23f62402..9dd71f766 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -1,6 +1,6 @@ """Module for checking plastic shear force resistance of steel(Eurocode 3).""" -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import ClassVar, Literal from sectionproperties.post.post import SectionProperties @@ -73,13 +73,11 @@ class PlasticShearStrengthIProfileCheck: section_properties: SectionProperties | None = None name: str = "Plastic shear strength check for steel I-profiles" source_docs: ClassVar[list] = [EN_1993_1_1_2005] - _profile: IProfile = field(init=False, repr=False) 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.") - object.__setattr__(self, "_profile", self.steel_cross_section.profile) if self.section_properties is None: section_properties = self.steel_cross_section.profile.section_properties() object.__setattr__(self, "section_properties", section_properties) From 483441649e5853a9a01c38cd6de3279d55973f17 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 16:06:16 +0100 Subject: [PATCH 33/46] Update torsion_strength.py --- blueprints/checks/eurocode/steel/torsion_strength.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/checks/eurocode/steel/torsion_strength.py b/blueprints/checks/eurocode/steel/torsion_strength.py index 035e7bd0a..3b236bd17 100644 --- a/blueprints/checks/eurocode/steel/torsion_strength.py +++ b/blueprints/checks/eurocode/steel/torsion_strength.py @@ -39,7 +39,7 @@ class StVenantTorsionStrengthCheck: steel_cross_section : SteelCrossSection The steel cross-section, of type I-profile, to check. mx : KNM - The applied shear force (positive value, in kN). + The applied shear force (in kN). gamma_m0 : DIMENSIONLESS, optional Partial safety factor for resistance of cross-sections, default is 1.0. section_properties : SectionProperties | None, optional From e6ecdbd83ebb9a75913bd0b8cc41aea1e9ca83aa Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 16:06:58 +0100 Subject: [PATCH 34/46] Update shear_strength.py --- blueprints/checks/eurocode/steel/shear_strength.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/checks/eurocode/steel/shear_strength.py b/blueprints/checks/eurocode/steel/shear_strength.py index 9dd71f766..0d72a56fc 100644 --- a/blueprints/checks/eurocode/steel/shear_strength.py +++ b/blueprints/checks/eurocode/steel/shear_strength.py @@ -42,7 +42,7 @@ class PlasticShearStrengthIProfileCheck: steel_cross_section : SteelCrossSection The steel cross-section, of type I-profile, to check. v : KN - The applied shear force (positive value, in 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 From 19dbdaf9c2569fd0dbf6c865f95d896de133293f Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 18:04:42 +0100 Subject: [PATCH 35/46] fix: use variable for equation type in LaTeX output --- blueprints/utils/report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blueprints/utils/report.py b/blueprints/utils/report.py index aa1405814..5b28af5ad 100644 --- a/blueprints/utils/report.py +++ b/blueprints/utils/report.py @@ -171,13 +171,14 @@ def _split_equation(eq: str, split_after: list[tuple[int, str]] | None) -> str: 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"${eq_to_use}$" + f"{f' ({tag})' if tag else ''}" + r" }" elif tag: - self.content += rf"\begin{{multline}} {eq_to_use} \tag{{{tag}}} \end{{multline}}" + self.content += rf"\begin{{{multline_vs_equation}}} {eq_to_use} \tag{{{tag}}} \end{{{multline_vs_equation}}}" else: - self.content += rf"\begin{{multline}} {eq_to_use} \notag \end{{multline}}" + self.content += rf"\begin{{{multline_vs_equation}}} {eq_to_use} \notag \end{{{multline_vs_equation}}}" # Add a newline for visual separation self.content += "\n" From 14a9b4d173c7e66892fe090bd699546faaf9147c Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 18:07:09 +0100 Subject: [PATCH 36/46] fix: update equation formatting in test_report.py to use equation environment --- tests/utils/test_report.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/utils/test_report.py b/tests/utils/test_report.py index 199f7f642..2f2f10d83 100644 --- a/tests/utils/test_report.py +++ b/tests/utils/test_report.py @@ -119,13 +119,13 @@ def test_add_text_method_chaining(self, fixture_report: Report) -> None: def test_add_equation_without_tag(self, fixture_report: Report) -> None: """Test adding equation without tag.""" fixture_report.add_equation("a^2+b^2=c^2") - expected = r"\begin{multline} a^2+b^2=c^2 \notag \end{multline}" + "\n" + expected = r"\begin{equation} a^2+b^2=c^2 \notag \end{equation}" + "\n" assert fixture_report.content == expected def test_add_equation_with_tag(self, fixture_report: Report) -> None: """Test adding equation with tag.""" fixture_report.add_equation("a^2+b^2=c^2", tag="6.83") - expected = r"\begin{multline} a^2+b^2=c^2 \tag{6.83} \end{multline}" + "\n" + 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: @@ -160,7 +160,7 @@ def test_add_equation_inline_method_chaining(self, fixture_report: Report) -> No def test_add_formula(self, fixture_report: Report) -> None: """Test adding formula.""" fixture_report.add_formula(formula_6_5.Form6Dot5UnityCheckTensileStrength(n_ed=150000, n_t_rd=200000), options="complete") - expected = r"\begin{multline} CHECK" + expected = r"\begin{equation} CHECK" assert expected in fixture_report.content def test_add_formula_inline(self, fixture_report: Report) -> None: @@ -172,7 +172,7 @@ def test_add_formula_inline(self, fixture_report: Report) -> None: def test_add_formula_complete_with_units(self, fixture_report: Report) -> None: """Test adding formula with complete_with_units option.""" fixture_report.add_formula(formula_6_5.Form6Dot5UnityCheckTensileStrength(n_ed=150000, n_t_rd=200000), options="complete_with_units") - expected = r"\begin{multline} CHECK" + expected = r"\begin{equation} CHECK" assert expected in fixture_report.content def test_add_section(self, fixture_report: Report) -> None: @@ -399,8 +399,8 @@ def test_comprehensive_example_from_docstring(self) -> None: assert r"\textbf{This is bold text with newline after.}" in latex_document assert r"\textit{This is italic text with 4 newlines after.}" in latex_document assert r"\textbf{\textit{This is bold and italic text.}}" in latex_document - assert r"\begin{multline} E=mc^2 \tag{3.14} \end{multline}" in latex_document - assert r"\begin{multline} CHECK" in latex_document + assert r"\begin{equation} E=mc^2 \tag{3.14} \end{equation}" in latex_document + assert r"\begin{equation} CHECK" in latex_document assert r"$\frac{a}{b}$" in latex_document assert r"Parameter & Value & Unit" in latex_document assert r"\text{Length} & 10 & \text{m}" in latex_document From 9047dd4b4a388609440192bbf6970bd7000ba22a Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 18:10:15 +0100 Subject: [PATCH 37/46] fix: include additional equation count in report summary --- blueprints/utils/report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/utils/report.py b/blueprints/utils/report.py index 5b28af5ad..c989502e6 100644 --- a/blueprints/utils/report.py +++ b/blueprints/utils/report.py @@ -539,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{multline}") + 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}") @@ -557,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{multline}") + 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}") From d99985fbed32e892344a7397ea788dea602c49b0 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 18:12:53 +0100 Subject: [PATCH 38/46] fix: update equation handling to support both multline and equation environments --- blueprints/utils/_report_to_word.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/blueprints/utils/_report_to_word.py b/blueprints/utils/_report_to_word.py index e7fd14df0..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{multline}...\end{multline} 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\{multline\}", + "equation": r"\\begin\{(multline|equation)\}", "newline": r"\\newline", } @@ -438,7 +438,13 @@ def _add_equation(self, doc: DocumentObject, content: str) -> None: equation_content = re.sub(r"\\notag", "", equation_content) # Split equation content on line breaks (\\) - lines = equation_content.replace("\\begin{multline}", "").replace("\\end{multline}", "").split(r"\\") + 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" From 40c581d43d730589d0f1032822a11844d883c7ec Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sat, 7 Feb 2026 20:45:08 +0100 Subject: [PATCH 39/46] feat: update stress calculation to use correct unit conversions for forces and moments --- blueprints/structural_sections/_profile.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blueprints/structural_sections/_profile.py b/blueprints/structural_sections/_profile.py index d0fc8502f..d52a0bcfc 100644 --- a/blueprints/structural_sections/_profile.py +++ b/blueprints/structural_sections/_profile.py @@ -16,7 +16,7 @@ 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) @@ -207,12 +207,12 @@ def calculate_stress(self, result_internal_force_1d: ResultInternalForce1D) -> S # Blueprints uses x for longitudinal axis, y for horizontal, z for vertical # sectionproperties uses x for horizontal, y for vertical, z for longitudinal return section.calculate_stress( - n=-float(result_internal_force_1d.n) * 1e3, - vx=-float(result_internal_force_1d.vy) * 1e3, - vy=float(result_internal_force_1d.vz) * 1e3, - mxx=-float(result_internal_force_1d.my) * 1e6, - myy=float(result_internal_force_1d.mz) * 1e6, - mzz=-float(result_internal_force_1d.mx) * 1e6, + 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: From 350d3959f8660f408f49dff744b1d86bded69c2b Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 8 Feb 2026 12:03:59 +0100 Subject: [PATCH 40/46] Add stress calculation method to Profile class for internal force analysis --- blueprints/structural_sections/_profile.py | 45 +++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) 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. From d147fc5b8c923c2373079622f9e0b039f48306a0 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 8 Feb 2026 13:02:43 +0100 Subject: [PATCH 41/46] Add plastic shear strength check for steel cross-section class 3 and 4 --- .../checks/eurocode/steel/strength_shear.py | 154 +++++++++++++++++- .../eurocode/steel/test_strength_shear.py | 59 ++++++- 2 files changed, 205 insertions(+), 8 deletions(-) diff --git a/blueprints/checks/eurocode/steel/strength_shear.py b/blueprints/checks/eurocode/steel/strength_shear.py index c6307b89b..9c77d082b 100644 --- a/blueprints/checks/eurocode/steel/strength_shear.py +++ b/blueprints/checks/eurocode/steel/strength_shear.py @@ -3,16 +3,14 @@ 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, -) +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 @@ -22,7 +20,7 @@ @dataclass(frozen=True) class CheckStrengthShearClass12IProfile: - """Class to perform plastic shear force resistance check for steel I-profiles (Eurocode 3). + """Class to perform plastic shear force resistance check for steel I-profiles of cross-section class 1 and 2 (Eurocode 3). Coordinate System: @@ -167,3 +165,147 @@ def report(self, n: int = 2) -> Report: 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]: + """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=self.v if self.axis == "Vy" else 0, + vz=self.v if self.axis == "Vz" else 0, + ) + + stress = self.steel_cross_section.profile.calculate_stress(rif1d) + sig_zxy = float(max(abs(stress.get_stress()[0]["sig_zxy"]))) + + 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 = self.steel_cross_section.yield_strength / (np.sqrt(3) * self.gamma_m0) + 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:") + report.add_formula(formulas["check"], 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/tests/checks/eurocode/steel/test_strength_shear.py b/tests/checks/eurocode/steel/test_strength_shear.py index 9241c781c..6c8002924 100644 --- a/tests/checks/eurocode/steel/test_strength_shear.py +++ b/tests/checks/eurocode/steel/test_strength_shear.py @@ -1,9 +1,9 @@ -"""Tests for CheckStrengthShearClass12IProfile according to Eurocode 3.""" +"""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 +from blueprints.checks.eurocode.steel.strength_shear import CheckStrengthShearClass12IProfile, CheckStrengthShearClass34 from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection @@ -98,3 +98,58 @@ def test_check_wrong_profile(self, chs_steel_cross_section: tuple[SteelCrossSect 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 From 50a15108bdebcd6b33c8fad7dcb4a6fc9b089ee8 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 8 Feb 2026 13:04:45 +0100 Subject: [PATCH 42/46] Round maximum allowed shear stress calculation to specified decimal places --- blueprints/checks/eurocode/steel/strength_shear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/checks/eurocode/steel/strength_shear.py b/blueprints/checks/eurocode/steel/strength_shear.py index 9c77d082b..7002b0a9e 100644 --- a/blueprints/checks/eurocode/steel/strength_shear.py +++ b/blueprints/checks/eurocode/steel/strength_shear.py @@ -298,7 +298,7 @@ def report(self, n: int = 2) -> Report: formulas = self.calculation_formula() report.add_paragraph(f"The maximum shear stress is: {formulas['shear_stress']:.{n}f} N/mm². ") - tau_max = self.steel_cross_section.yield_strength / (np.sqrt(3) * self.gamma_m0) + 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². ") From 1b1f75b546afed37c902580a297f8cf93c0b66f1 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 8 Feb 2026 13:11:52 +0100 Subject: [PATCH 43/46] Enhance calculation_formula method to support multiple return types and improve stress calculation in CheckStrengthShearClass34 --- blueprints/checks/eurocode/steel/strength_shear.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/blueprints/checks/eurocode/steel/strength_shear.py b/blueprints/checks/eurocode/steel/strength_shear.py index 7002b0a9e..1fe7fe5e0 100644 --- a/blueprints/checks/eurocode/steel/strength_shear.py +++ b/blueprints/checks/eurocode/steel/strength_shear.py @@ -227,7 +227,7 @@ def __post_init__(self) -> None: section_properties = self.steel_cross_section.profile.section_properties() object.__setattr__(self, "section_properties", section_properties) - def calculation_formula(self) -> dict[str, Formula]: + def calculation_formula(self) -> dict[str, Formula | float | int]: """Calculate plastic shear force resistance check. Returns @@ -245,7 +245,8 @@ def calculation_formula(self) -> dict[str, Formula]: ) stress = self.steel_cross_section.profile.calculate_stress(rif1d) - sig_zxy = float(max(abs(stress.get_stress()[0]["sig_zxy"]))) + sig_zxy_data = stress.get_stress()[0]["sig_zxy"] + sig_zxy = float(np.max(np.abs(sig_zxy_data))) resistance = float(self.steel_cross_section.yield_strength / np.sqrt(3) / self.gamma_m0 / sig_zxy * self.v * KN_TO_N) @@ -303,7 +304,9 @@ def report(self, n: int = 2) -> Report: 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:") - report.add_formula(formulas["check"], n=n) + 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: From 242c5d05acff53a69eb68545cec806aedfeb57ad Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 8 Feb 2026 13:37:50 +0100 Subject: [PATCH 44/46] Refactor CheckStrengthTorsionShearClass to simplify class name and update references in tests --- .../checks/eurocode/steel/strength_shear.py | 11 ++++--- .../eurocode/steel/strength_torsion_shear.py | 6 ++-- .../steel/test_strength_torsion_shear.py | 30 +++++++++---------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/blueprints/checks/eurocode/steel/strength_shear.py b/blueprints/checks/eurocode/steel/strength_shear.py index 1fe7fe5e0..7708d206f 100644 --- a/blueprints/checks/eurocode/steel/strength_shear.py +++ b/blueprints/checks/eurocode/steel/strength_shear.py @@ -240,14 +240,13 @@ def calculation_formula(self) -> dict[str, Formula | float | int]: 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, + vy=1 if self.axis == "Vy" else 0, + vz=1 if self.axis == "Vz" else 0, ) - stress = self.steel_cross_section.profile.calculate_stress(rif1d) - sig_zxy_data = stress.get_stress()[0]["sig_zxy"] - sig_zxy = float(np.max(np.abs(sig_zxy_data))) - + 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( diff --git a/blueprints/checks/eurocode/steel/strength_torsion_shear.py b/blueprints/checks/eurocode/steel/strength_torsion_shear.py index 981dd0f8b..16c287930 100644 --- a/blueprints/checks/eurocode/steel/strength_torsion_shear.py +++ b/blueprints/checks/eurocode/steel/strength_torsion_shear.py @@ -21,7 +21,7 @@ @dataclass(frozen=True) -class CheckStrengthTorsionShearClass1234IProfile: +class CheckStrengthTorsionShearClass12IProfile: """Class to perform torsion resistance check with extra shear force for I profiles (Eurocode 3), using St. Venant torsion. Coordinate System: @@ -54,7 +54,7 @@ class CheckStrengthTorsionShearClass1234IProfile: Example ------- - from blueprints.checks.eurocode.steel.strength_torsion_shear import CheckStrengthTorsionShearClass1234IProfile + 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 @@ -65,7 +65,7 @@ class CheckStrengthTorsionShearClass1234IProfile: axis = "Vz" # Shear force applied in z-direction heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) - calc = CheckStrengthTorsionShearClass1234IProfile(heb_300_s355, mx, v=v, axis=axis, gamma_m0=1.0) + 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") """ diff --git a/tests/checks/eurocode/steel/test_strength_torsion_shear.py b/tests/checks/eurocode/steel/test_strength_torsion_shear.py index 9e9ca9cb0..6965eb633 100644 --- a/tests/checks/eurocode/steel/test_strength_torsion_shear.py +++ b/tests/checks/eurocode/steel/test_strength_torsion_shear.py @@ -3,19 +3,19 @@ import pytest from sectionproperties.post.post import SectionProperties -from blueprints.checks.eurocode.steel.strength_torsion_shear import CheckStrengthTorsionShearClass1234IProfile +from blueprints.checks.eurocode.steel.strength_torsion_shear import CheckStrengthTorsionShearClass12IProfile from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection -class TestCheckStrengthTorsionShearClass1234IProfile: - """Tests for CheckStrengthTorsionShearClass1234IProfile, using St. Venant torsion.""" +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 mx = 1 - calc = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 @@ -23,15 +23,15 @@ def test_result_none_v(self, heb_steel_cross_section: tuple[SteelCrossSection, S assert result.provided == 0.0 assert calc.report() - calc_without_section_props = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0) + calc_without_section_props = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0) assert calc == calc_without_section_props def test_result_none_mx(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: - """Test result() returns True for no shear force.""" + """Test result() returns True for no torsional moment.""" cross_section, section_properties = heb_steel_cross_section mx = 0 v = 1 - calc = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 @@ -44,7 +44,7 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, cross_section, section_properties = heb_steel_cross_section v = 585.023 * 0.99 mx = 10 - calc = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 @@ -52,14 +52,14 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, assert calc.report() v = -v - calc = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 @@ -67,7 +67,7 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, v = 355.277 * 0.99 object.__setattr__(cross_section, "fabrication_method", "welded") - calc = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 @@ -79,7 +79,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect object.__setattr__(cross_section, "fabrication_method", "rolled") v = 585.023 * 1.01 mx = 10 - calc = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 @@ -87,7 +87,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect assert calc.report() v = 1482.833 * 1.01 - calc = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 @@ -95,7 +95,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect v = 355.277 * 1.01 object.__setattr__(cross_section, "fabrication_method", "welded") - calc = CheckStrengthTorsionShearClass1234IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, 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 @@ -105,4 +105,4 @@ def test_check_wrong_profile(self, chs_steel_cross_section: tuple[SteelCrossSect """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"): - CheckStrengthTorsionShearClass1234IProfile(cross_section, mx=10, v=1, gamma_m0=1.0, section_properties=section_properties) + CheckStrengthTorsionShearClass12IProfile(cross_section, mx=10, v=1, gamma_m0=1.0, section_properties=section_properties) From 9ab3649d7ba14a62cbc9bc1a48c3e875a34aab74 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 8 Feb 2026 15:19:55 +0100 Subject: [PATCH 45/46] Refactor torsional moment variable names to improve clarity and add CheckStrengthTorsionShearClass34 for class 3 and 4 sections --- .../checks/eurocode/steel/strength_torsion.py | 35 +-- .../eurocode/steel/strength_torsion_shear.py | 215 ++++++++++++++++-- .../eurocode/steel/test_strength_torsion.py | 14 +- .../steel/test_strength_torsion_shear.py | 93 ++++++-- 4 files changed, 295 insertions(+), 62 deletions(-) diff --git a/blueprints/checks/eurocode/steel/strength_torsion.py b/blueprints/checks/eurocode/steel/strength_torsion.py index 8a2bf0476..f02cb6cb7 100644 --- a/blueprints/checks/eurocode/steel/strength_torsion.py +++ b/blueprints/checks/eurocode/steel/strength_torsion.py @@ -38,7 +38,7 @@ class CheckStrengthStVenantTorsionClass1234: ---------- steel_cross_section : SteelCrossSection The steel cross-section, of type I-profile, to check. - mx : KNM + 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. @@ -53,16 +53,16 @@ class CheckStrengthStVenantTorsionClass1234: steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) heb_300_profile = HEB.HEB300.with_corrosion(1.5) - mx = 10 # Applied torsional moment in kNm + m_x = 10 # Applied torsional moment in kNm heb_300_s355 = SteelCrossSection(profile=heb_300_profile, material=steel_material) - calc = TorsionStrengthCheck(heb_300_s355, mx, gamma_m0=1.0) + calc = TorsionStrengthCheck(heb_300_s355, m_x, gamma_m0=1.0) calc.report().to_word("torsion_strength.docx", language="nl") """ steel_cross_section: SteelCrossSection - mx: KNM = 0 + m_x: KNM = 0 gamma_m0: DIMENSIONLESS = 1.0 section_properties: SectionProperties | None = None name: str = "Torsion strength check" @@ -74,12 +74,12 @@ def __post_init__(self) -> None: section_properties = self.steel_cross_section.profile.section_properties() object.__setattr__(self, "section_properties", section_properties) - def calculation_formula(self) -> dict[str, Formula]: + def calculation_formula(self) -> dict[str, Formula | float]: """Calculate torsion resistance check. Returns ------- - dict[str, Formula] + dict[str, Formula | float] Calculation results keyed by formula number. Returns an empty dict if no torsion is applied. """ rif1d = ResultInternalForce1D( @@ -91,17 +91,16 @@ def calculation_formula(self) -> dict[str, Formula]: ) unit_stress = self.steel_cross_section.profile.calculate_stress(rif1d) - unit_sig_zx_mzz = unit_stress.get_stress()[0]["sig_zx_mzz"] - unit_sig_zy_mzz = unit_stress.get_stress()[0]["sig_zy_mzz"] - unit_max_mzz_zxy = np.max(np.sqrt(np.array(unit_sig_zx_mzz) ** 2 + np.array(unit_sig_zy_mzz) ** 2)) + 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_mzz_zxy - t_ed = abs(self.mx) + 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_mzz_zxy, + "unit_shear_stress": unit_max_sig_zxy, "resistance": t_rd, "check": check_torsion, } @@ -115,7 +114,7 @@ def result(self) -> CheckResult: True if the torsion check passes, False otherwise. """ steps = self.calculation_formula() - provided = abs(self.mx) * KNM_TO_NMM + provided = abs(self.m_x) * KNM_TO_NMM required = steps["resistance"] * KNM_TO_NMM return CheckResult.from_comparison(provided=provided, required=float(required)) @@ -133,7 +132,7 @@ def report(self, n: int = 2) -> Report: Report of the torsion check. """ report = Report("Check: torsion steel beam") - if self.mx == 0: + if self.m_x == 0: report.add_paragraph("No torsion was applied; therefore, no torsion check is necessary.") return report @@ -143,12 +142,12 @@ def report(self, n: int = 2) -> Report: # 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 - mx_val = f"{self.mx:.{n}f}" + 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 {mx_val} kNm. " + 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:" ) @@ -165,7 +164,9 @@ def report(self, n: int = 2) -> Report: ) report.add_equation(eqn_1) report.add_paragraph("The unity check is calculated as follows:") - report.add_formula(formulas["check"], n=n) + 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: diff --git a/blueprints/checks/eurocode/steel/strength_torsion_shear.py b/blueprints/checks/eurocode/steel/strength_torsion_shear.py index 16c287930..2bdeeab22 100644 --- a/blueprints/checks/eurocode/steel/strength_torsion_shear.py +++ b/blueprints/checks/eurocode/steel/strength_torsion_shear.py @@ -9,6 +9,7 @@ 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 @@ -22,7 +23,7 @@ @dataclass(frozen=True) class CheckStrengthTorsionShearClass12IProfile: - """Class to perform torsion resistance check with extra shear force for I profiles (Eurocode 3), using St. Venant torsion. + """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: @@ -60,7 +61,7 @@ class CheckStrengthTorsionShearClass12IProfile: steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) heb_300_profile = HEB.HEB300.with_corrosion(1.5) - mx = 10 # Applied torsional moment in kNm + m_x = 10 # Applied torsional moment in kNm v = 100 # Applied shear force in kN axis = "Vz" # Shear force applied in z-direction @@ -71,12 +72,12 @@ class CheckStrengthTorsionShearClass12IProfile: """ steel_cross_section: SteelCrossSection - mx: KNM = 0 + 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" + 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: @@ -87,12 +88,12 @@ def __post_init__(self) -> None: section_properties = self.steel_cross_section.profile.section_properties() object.__setattr__(self, "section_properties", section_properties) - def calculation_formula(self) -> dict[str, Formula]: + def calculation_formula(self) -> dict[str, Formula | float]: """Calculate torsion resistance check. Returns ------- - dict[str, Formula] + dict[str, Formula | float] Calculation results keyed by formula number. Returns an empty dict if no torsion is applied. """ shear_calculation = CheckStrengthShearClass12IProfile( @@ -116,11 +117,10 @@ def calculation_formula(self) -> dict[str, Formula]: ) unit_stress = self.steel_cross_section.profile.calculate_stress(rif1d) - unit_sig_zx_mzz = unit_stress.get_stress()[0]["sig_zx_mzz"] - unit_sig_zy_mzz = unit_stress.get_stress()[0]["sig_zy_mzz"] - unit_max_mzz_zxy = np.max(np.sqrt(np.array(unit_sig_zx_mzz) ** 2 + np.array(unit_sig_zy_mzz) ** 2)) + 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.mx) * unit_max_mzz_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( @@ -130,7 +130,7 @@ def calculation_formula(self) -> dict[str, Formula]: check_torsion_with_shear = Form6Dot25CheckCombinedShearForceAndTorsionalMoment(v_ed=v_ed, v_pl_t_rd=v_pl_t_rd) return { - "unit_shear_stress": unit_max_mzz_zxy, + "unit_shear_stress": unit_max_sig_zxy, "shear_area": a_v, "raw_shear_resistance": v_pl_rd, "resistance": v_pl_t_rd, @@ -146,7 +146,7 @@ def result(self) -> CheckResult: True if the torsion check passes, False otherwise. """ steps = self.calculation_formula() - provided = 0 if self.mx == 0 else abs(self.v) * KN_TO_N + 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)) @@ -164,7 +164,7 @@ def report(self, n: int = 2) -> Report: Report of the torsion check. """ report = Report("Check: torsion with shear force on steel beam") - if self.mx == 0: + 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: @@ -177,27 +177,202 @@ def report(self, n: int = 2) -> Report: # 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 - mx_val = f"{self.mx:.{n}f}" + 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.mx:.{n}f}" + 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 {mx_val} kNm a shear force of {abs(self.v):.{n}f} kN in the {axis_label}-direction. " + 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:" ) - report.add_formula(formulas["shear_area"], n=n, split_after=[(2, "="), (7, "+"), (3, "=")]) - report.add_formula(formulas["raw_shear_resistance"], n=n) + 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(formulas["resistance"], n=n) + report.add_formula(resistance_formula, n=n) report.add_paragraph("The unity check is calculated as follows:") - report.add_formula(formulas["check"], n=n) + 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/tests/checks/eurocode/steel/test_strength_torsion.py b/tests/checks/eurocode/steel/test_strength_torsion.py index 64ea06c72..64ea95e45 100644 --- a/tests/checks/eurocode/steel/test_strength_torsion.py +++ b/tests/checks/eurocode/steel/test_strength_torsion.py @@ -12,9 +12,9 @@ class TestCheckStrengthStVenantTorsionClass1234: def test_result_none(self, unp_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: """Test result() returns True for no torsion.""" - mx = 0 + m_x = 0 cross_section, section_properties = unp_steel_cross_section - calc = CheckStrengthStVenantTorsionClass1234(cross_section, mx, gamma_m0=1.0, section_properties=section_properties) + 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 @@ -22,14 +22,14 @@ def test_result_none(self, unp_steel_cross_section: tuple[SteelCrossSection, Sec assert result.provided == 0.0 assert calc.report() - calc_without_section_props = CheckStrengthStVenantTorsionClass1234(cross_section, mx, gamma_m0=1.0) + 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.""" - mx = -0.3896 * 0.99 + m_x = -0.3896 * 0.99 cross_section, section_properties = unp_steel_cross_section - calc = CheckStrengthStVenantTorsionClass1234(cross_section, mx, gamma_m0=1.0, section_properties=section_properties) + 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 @@ -38,9 +38,9 @@ def test_result_tension_ok(self, unp_steel_cross_section: tuple[SteelCrossSectio def test_result_tension_not_ok(self, unp_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: """Test result() for not ok tension load.""" - mx = 0.3896 * 1.01 + m_x = 0.3896 * 1.01 cross_section, section_properties = unp_steel_cross_section - calc = CheckStrengthStVenantTorsionClass1234(cross_section, mx, gamma_m0=1.0, section_properties=section_properties) + 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 diff --git a/tests/checks/eurocode/steel/test_strength_torsion_shear.py b/tests/checks/eurocode/steel/test_strength_torsion_shear.py index 6965eb633..91a2f480a 100644 --- a/tests/checks/eurocode/steel/test_strength_torsion_shear.py +++ b/tests/checks/eurocode/steel/test_strength_torsion_shear.py @@ -3,7 +3,7 @@ import pytest from sectionproperties.post.post import SectionProperties -from blueprints.checks.eurocode.steel.strength_torsion_shear import CheckStrengthTorsionShearClass12IProfile +from blueprints.checks.eurocode.steel.strength_torsion_shear import CheckStrengthTorsionShearClass12IProfile, CheckStrengthTorsionShearClass34 from blueprints.structural_sections.steel.steel_cross_section import SteelCrossSection @@ -14,8 +14,8 @@ def test_result_none_v(self, heb_steel_cross_section: tuple[SteelCrossSection, S """Test result() returns True for no shear force.""" cross_section, section_properties = heb_steel_cross_section v = 0 - mx = 1 - calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + 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 @@ -23,15 +23,15 @@ def test_result_none_v(self, heb_steel_cross_section: tuple[SteelCrossSection, S assert result.provided == 0.0 assert calc.report() - calc_without_section_props = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0) + 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_mx(self, heb_steel_cross_section: tuple[SteelCrossSection, SectionProperties]) -> None: + 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 - mx = 0 + m_x = 0 v = 1 - calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + 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 @@ -43,8 +43,8 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, """Test result() for ok shear force.""" cross_section, section_properties = heb_steel_cross_section v = 585.023 * 0.99 - mx = 10 - calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + 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 @@ -52,14 +52,14 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, assert calc.report() v = -v - calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + 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, mx, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + 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 @@ -67,7 +67,7 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, v = 355.277 * 0.99 object.__setattr__(cross_section, "fabrication_method", "welded") - calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + 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 @@ -76,10 +76,10 @@ def test_result_shear_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, 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", "rolled") + object.__setattr__(cross_section, "fabrication_method", "hot-rolled") v = 585.023 * 1.01 - mx = 10 - calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + 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 @@ -87,7 +87,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect assert calc.report() v = 1482.833 * 1.01 - calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vy", gamma_m0=1.0, section_properties=section_properties) + 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 @@ -95,7 +95,7 @@ def test_result_shear_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSect v = 355.277 * 1.01 object.__setattr__(cross_section, "fabrication_method", "welded") - calc = CheckStrengthTorsionShearClass12IProfile(cross_section, mx, v, axis="Vz", gamma_m0=1.0, section_properties=section_properties) + 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 @@ -105,4 +105,61 @@ def test_check_wrong_profile(self, chs_steel_cross_section: tuple[SteelCrossSect """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, mx=10, v=1, gamma_m0=1.0, section_properties=section_properties) + 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.81 * 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.81 * 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() From aad589c9f6930571b81735a2f60c99aafcbfa1bd Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Sun, 8 Feb 2026 16:52:55 +0100 Subject: [PATCH 46/46] Update test_strength_torsion_shear.py --- tests/checks/eurocode/steel/test_strength_torsion_shear.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/checks/eurocode/steel/test_strength_torsion_shear.py b/tests/checks/eurocode/steel/test_strength_torsion_shear.py index 91a2f480a..3ba32c961 100644 --- a/tests/checks/eurocode/steel/test_strength_torsion_shear.py +++ b/tests/checks/eurocode/steel/test_strength_torsion_shear.py @@ -144,7 +144,7 @@ def test_result_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, Secti """Test result() for ok shear force in Vz direction.""" cross_section, section_properties = heb_steel_cross_section v = 690 * 0.99 - m_x = 7.81 * 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 @@ -156,7 +156,7 @@ def test_result_not_ok(self, heb_steel_cross_section: tuple[SteelCrossSection, S """Test result() for not ok shear force.""" cross_section, section_properties = heb_steel_cross_section v = 690 * 1.01 - m_x = 7.81 * 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