From 44b27220ac066a1cc1efc03cc733463196f6f188 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 09:48:06 +0200 Subject: [PATCH 01/70] Add initial implementation of general steel plotter and default plotters for cross sections --- .../steel_cross_sections/plotters/__init__.py | 1 + .../plotters/general_steel_plotter.py | 202 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 blueprints/structural_sections/steel/steel_cross_sections/plotters/__init__.py create mode 100644 blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py diff --git a/blueprints/structural_sections/steel/steel_cross_sections/plotters/__init__.py b/blueprints/structural_sections/steel/steel_cross_sections/plotters/__init__.py new file mode 100644 index 000000000..861df9e06 --- /dev/null +++ b/blueprints/structural_sections/steel/steel_cross_sections/plotters/__init__.py @@ -0,0 +1 @@ +"""Default plotters for Blueprint's steel cross sections.""" diff --git a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py new file mode 100644 index 000000000..74938bd6a --- /dev/null +++ b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py @@ -0,0 +1,202 @@ +"""Defines a general steel plotter for cross sections and its characteristics.""" +# ruff: noqa: PLR0913, F821 + +import matplotlib.pyplot as plt +from matplotlib import patches as mplpatches +from matplotlib.patches import Polygon as MplPolygon +from shapely.geometry import Point + +from blueprints.structural_sections.steel.steel_cross_sections.base import SteelCrossSection +from blueprints.structural_sections.steel.steel_element import SteelElement + +# Define color +STEEL_COLOR = (0.683, 0.0, 0.0) + + +def plot_shapes( + profile: SteelCrossSection, + figsize: tuple[float, float] = (15.0, 8.0), + title: str = "", + font_size_title: float = 18.0, + font_size_legend: float = 10.0, + show: bool = False, +) -> plt.Figure: + """ + Plot the given shapes. + + Parameters + ---------- + profile : SteelCrossSection + The steel cross-sections to plot. + figsize : tuple[float, float], optional + The size of the figure in inches. Default is (15.0, 8.0). + title : str, optional + The title of the plot. Default is "". + font_size_title : float, optional + The font size of the title. Default is 18.0. + font_size_legend : float, optional + The font size of the legend. Default is 10.0. + show : bool, optional + Whether to show the plot. Default is False. + """ + fig, ax = plt.subplots(figsize=figsize) + + for element in profile.elements: + # Plot the exterior polygon + x, y = element.geometry.exterior.xy + patch = MplPolygon(xy=list(zip(x, y)), lw=1, fill=True, facecolor=STEEL_COLOR, edgecolor=STEEL_COLOR) + ax.add_patch(patch) + + # Plot the interior polygons (holes) if any + for interior in element.geometry.interiors: + x, y = interior.xy + patch = MplPolygon(xy=list(zip(x, y)), lw=0, fill=True, facecolor="white") + ax.add_patch(patch) + + # Add dimension lines and centroid + _add_dimension_lines(ax, profile.elements, profile.centroid) + ax.plot(profile.centroid.x, profile.centroid.y, "o", color="black") + + # Add legend text + legend_text = f"Total area: {profile.steel_area:.1f} mm²\n" + legend_text += f"Weight per meter: {profile.steel_weight_per_meter:.1f} kg/m\n" + legend_text += f"Moment of inertia about y: {profile.moment_of_inertia_about_y:.0f} mm⁴\n" + legend_text += f"Moment of inertia about z: {profile.moment_of_inertia_about_z:.0f} mm⁴\n" + legend_text += f"Steel quality: {profile.steel_material.name}\n" + + ax.text( + x=0.05, + y=0.95, + s=legend_text, + transform=ax.transAxes, + verticalalignment="top", + horizontalalignment="left", + fontsize=font_size_legend, + ) + + ax.set_xlabel("X") + ax.set_ylabel("Y") + ax.set_title(title, fontsize=font_size_title) + ax.grid(True) + ax.axis("equal") + ax.axis("off") + + if show: + plt.show() # pragma: no cover + + assert fig is not None + return fig + + +def _add_dimension_lines(ax: plt.Axes, elements: list[SteelElement], centroid: Point) -> None: + """Adds dimension lines to show the outer dimensions of the geometry. + + Parameters + ---------- + ax : plt.Axes + The matplotlib axes to draw on. + elements : tuple[CrossSection, ...] + The cross-sections to plot. + centroid : Point + The centroid of the cross-section. + """ + # Calculate the bounds of all elements in the geometry + min_x, min_y, max_x, max_y = float("inf"), float("inf"), float("-inf"), float("-inf") + for element in elements: + bounds = element.geometry.bounds + min_x = min(min_x, bounds[0]) + min_y = min(min_y, bounds[1]) + max_x = max(max_x, bounds[2]) + max_y = max(max_y, bounds[3]) + + width = max_x - min_x + height = max_y - min_y + centroid_width = centroid.x - min_x + centroid_height = centroid.y - min_y + + # Add the width dimension line (below the geometry) + diameter_line_style = { + "arrowstyle": mplpatches.ArrowStyle(stylename="<->", head_length=0.5, head_width=0.5), + } + offset_width = max(height, width) / 20 + ax.annotate( + text="", + xy=(min_x, min_y - offset_width), + xytext=(max_x, min_y - offset_width), + verticalalignment="center", + horizontalalignment="center", + arrowprops=diameter_line_style, + annotation_clip=False, + ) + ax.text( + s=f"{width:.1f} mm", + x=(min_x + max_x) / 2, + y=min_y - offset_width - 1, + verticalalignment="top", + horizontalalignment="center", + fontsize=10, + ) + + # Add the height dimension line (on the right side of the geometry) + offset_height = offset_width + ax.annotate( + text="", + xy=(max_x + offset_height, max_y), + xytext=(max_x + offset_height, min_y), + verticalalignment="center", + horizontalalignment="center", + arrowprops=diameter_line_style, + rotation=90, + annotation_clip=False, + ) + ax.text( + s=f"{height:.1f} mm", + x=max_x + offset_height + 1 + height / 200, + y=(min_y + max_y) / 2, + verticalalignment="center", + horizontalalignment="left", + fontsize=10, + rotation=90, + ) + + # Add the distance from the left to the centroid (below the geometry, double offset) + offset_centroid_left_bottom = 2 * offset_width + ax.annotate( + text="", + xy=(min_x, min_y - offset_centroid_left_bottom), + xytext=(centroid.x, min_y - offset_centroid_left_bottom), + verticalalignment="center", + horizontalalignment="center", + arrowprops=diameter_line_style, + annotation_clip=False, + ) + ax.text( + s=f"{centroid_width:.1f} mm", + x=(min_x + centroid.x) / 2, + y=min_y - offset_centroid_left_bottom - 1, + verticalalignment="top", + horizontalalignment="center", + fontsize=10, + ) + + # Add the distance from the bottom to the centroid (on the right side, double offset) + offset_centroid_bottom_right = 2 * offset_height + ax.annotate( + text="", + xy=(max_x + offset_centroid_bottom_right, min_y), + xytext=(max_x + offset_centroid_bottom_right, centroid.y), + verticalalignment="center", + horizontalalignment="center", + arrowprops=diameter_line_style, + rotation=90, + annotation_clip=False, + ) + ax.text( + s=f"{centroid_height:.1f} mm", + x=max_x + offset_centroid_bottom_right + 1 + height / 200, + y=(min_y + centroid.y) / 2, + verticalalignment="center", + horizontalalignment="left", + fontsize=10, + rotation=90, + ) From 622cb0f28d8386602f212e446e98dfa096bb1008 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 09:48:39 +0200 Subject: [PATCH 02/70] Add tests for the steel plotter --- .../steel/steel_cross_sections/plotters/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/structural_sections/steel/steel_cross_sections/plotters/__init__.py diff --git a/tests/structural_sections/steel/steel_cross_sections/plotters/__init__.py b/tests/structural_sections/steel/steel_cross_sections/plotters/__init__.py new file mode 100644 index 000000000..1c2f15118 --- /dev/null +++ b/tests/structural_sections/steel/steel_cross_sections/plotters/__init__.py @@ -0,0 +1 @@ +"""Tests for the steel plotter.""" From 9dd452132219f6f4ca7dd377ed8d05aeac802fdd Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 09:48:55 +0200 Subject: [PATCH 03/70] Add base implementation and tests for SteelCrossSection class --- .../steel/steel_cross_sections/base.py | 136 ++++++++++++++++ .../steel/steel_cross_sections/test_base.py | 149 ++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 blueprints/structural_sections/steel/steel_cross_sections/base.py create mode 100644 tests/structural_sections/steel/steel_cross_sections/test_base.py diff --git a/blueprints/structural_sections/steel/steel_cross_sections/base.py b/blueprints/structural_sections/steel/steel_cross_sections/base.py new file mode 100644 index 000000000..f29c14aad --- /dev/null +++ b/blueprints/structural_sections/steel/steel_cross_sections/base.py @@ -0,0 +1,136 @@ +"""Base class of all steel cross-sections.""" + +from abc import ABC + +from shapely.geometry import Point + +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.steel.steel_element import SteelElement +from blueprints.type_alias import KG_M, M3_M, MM2, MM3 +from blueprints.unit_conversion import MM3_TO_M3 + + +class SteelCrossSection(ABC): + """Base class of all steel cross-sections.""" + + def __init__( + self, + steel_material: SteelMaterial, + ) -> None: + """Initialize the steel cross-section. + + Parameters + ---------- + steel_material : SteelMaterial + Material properties of the steel. + """ + self.steel_material = steel_material # pragma: no cover + self.elements: list[SteelElement] = [] # pragma: no cover + + @property + def steel_volume_per_meter(self) -> M3_M: + """Total volume of the reinforced cross-section per meter length [m³/m].""" + length = 1000 # mm + return sum(element.area * length * MM3_TO_M3 for element in self.elements) + + @property + def steel_weight_per_meter(self) -> KG_M: + """Total weight of the steel elements per meter length [kg/m].""" + return self.steel_material.density * self.steel_volume_per_meter + + @property + def steel_area(self) -> MM2: + """Total cross sectional area of the steel element [mm²].""" + return sum(element.area for element in self.elements) + + @property + def centroid(self) -> Point: + """Centroid of the steel cross-section.""" + area_weighted_centroids_x = sum(element.centroid.x * element.area for element in self.elements) + area_weighted_centroids_y = sum(element.centroid.y * element.area for element in self.elements) + centroid_x = area_weighted_centroids_x / self.steel_area + centroid_y = area_weighted_centroids_y / self.steel_area + return Point(centroid_x, centroid_y) + + @property + def moment_of_inertia_about_y(self) -> KG_M: + """Moment of inertia about the y-axis per meter length [mm⁴].""" + body_moments_of_inertia = sum(element.moment_of_inertia_about_y for element in self.elements) + parallel_axis_theorem = sum(element.area * (element.centroid.y - self.centroid.y) ** 2 for element in self.elements) + return body_moments_of_inertia + parallel_axis_theorem + + @property + def moment_of_inertia_about_z(self) -> KG_M: + """Moment of inertia about the z-axis per meter length [mm⁴].""" + body_moments_of_inertia = sum(element.moment_of_inertia_about_z for element in self.elements) + parallel_axis_theorem = sum(element.area * (element.centroid.x - self.centroid.x) ** 2 for element in self.elements) + return body_moments_of_inertia + parallel_axis_theorem + + @property + def elastic_section_modulus_about_y_positive(self) -> KG_M: + """Elastic section modulus about the y-axis on the positive z side [mm³].""" + distance_to_top = max(point.y for element in self.elements for point in element.cross_section.vertices) - self.centroid.y + return self.moment_of_inertia_about_y / distance_to_top + + @property + def elastic_section_modulus_about_y_negative(self) -> KG_M: + """Elastic section modulus about the y-axis on the negative z side [mm³].""" + distance_to_bottom = self.centroid.y - min(point.y for element in self.elements for point in element.cross_section.vertices) + return self.moment_of_inertia_about_y / distance_to_bottom + + @property + def elastic_section_modulus_about_z_positive(self) -> KG_M: + """Elastic section modulus about the z-axis on the positive y side [mm³].""" + distance_to_right = max(point.x for element in self.elements for point in element.cross_section.vertices) - self.centroid.x + return self.moment_of_inertia_about_z / distance_to_right + + @property + def elastic_section_modulus_about_z_negative(self) -> KG_M: + """Elastic section modulus about the z-axis on the negative y side [mm³].""" + distance_to_left = self.centroid.x - min(point.x for element in self.elements for point in element.cross_section.vertices) + return self.moment_of_inertia_about_z / distance_to_left + + @property + def plastic_section_modulus_about_y(self) -> MM3: + """Plastic section modulus about the y-axis [mm³].""" + # Calculate the area and yield strength weighted midpoint + total_weighted_area = sum(element.area * element.yield_strength for element in self.elements) + weighted_midpoint_y = sum(element.centroid.y * element.area * element.yield_strength for element in self.elements) / total_weighted_area + + # Create a dotted mesh for each element + max_mesh_size = 0 # zero lets the cross-section decide the mesh size + dotted_meshes = [element.cross_section.dotted_mesh(max_mesh_size) for element in self.elements] + + # Calculate the plastic section modulus by integrating the area over the distance to the weighted midpoint + plastic_section_modulus: float = 0.0 + for element, dotted_mesh in zip(self.elements, dotted_meshes): + mesh_area = element.area / len(dotted_mesh) + for node in dotted_mesh: + plastic_section_modulus += abs(node.y - weighted_midpoint_y) * mesh_area + + return plastic_section_modulus + + @property + def plastic_section_modulus_about_z(self) -> MM3: + """Plastic section modulus about the z-axis [mm³].""" + # Calculate the area and yield strength weighted midpoint + total_weighted_area = sum(element.area * element.yield_strength for element in self.elements) + weighted_midpoint_x = sum(element.centroid.x * element.area * element.yield_strength for element in self.elements) / total_weighted_area + + # Create a dotted mesh for each element + max_mesh_size = 0 # zero lets the cross-section decide the mesh size + dotted_meshes = [element.cross_section.dotted_mesh(max_mesh_size) for element in self.elements] + + # Calculate the plastic section modulus by integrating the area over the distance to the weighted midpoint + plastic_section_modulus: float = 0.0 + for element, dotted_mesh in zip(self.elements, dotted_meshes): + mesh_area = element.area / len(dotted_mesh) + for node in dotted_mesh: + plastic_section_modulus += abs(node.x - weighted_midpoint_x) * mesh_area + + return plastic_section_modulus + + @property + def vertices(self) -> list[Point]: + """Vertices of the cross-section.""" + return [point for element in self.elements for point in element.cross_section.vertices] diff --git a/tests/structural_sections/steel/steel_cross_sections/test_base.py b/tests/structural_sections/steel/steel_cross_sections/test_base.py new file mode 100644 index 000000000..06ec35819 --- /dev/null +++ b/tests/structural_sections/steel/steel_cross_sections/test_base.py @@ -0,0 +1,149 @@ +"""Test the SteelCrossSection class.""" + +from unittest.mock import Mock + +import pytest +from shapely.geometry import Point + +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.steel.steel_cross_sections.base import SteelCrossSection +from blueprints.structural_sections.steel.steel_element import SteelElement + + +@pytest.fixture +def mock_steel_material(mocker: Mock) -> Mock: + """Mock a SteelMaterial object.""" + material: Mock = mocker.Mock(spec=SteelMaterial) + material.density = 7850 # kg/m³ + return material + + +@pytest.fixture +def mock_steel_element(mocker: Mock) -> Mock: + """Mock a SteelElement object.""" + element: Mock = mocker.Mock(spec=SteelElement) + element.area = 500 # mm² + element.centroid = Point(50, 50) + element.moment_of_inertia_about_y = 2000 # mm⁴ + element.moment_of_inertia_about_z = 3000 # mm⁴ + element.yield_strength = 250 # MPa + element.cross_section = Mock() + element.cross_section.vertices = [Point(0, 0), Point(100, 0), Point(100, 50), Point(0, 50)] + element.cross_section.dotted_mesh = Mock(return_value=[Point(10, 10), Point(20, 20)]) + element.cross_section.area = 500 # mm² + element.cross_section.centroid = Point(50, 50) + element.cross_section.moment_of_inertia_about_y = 2000 # mm⁴ + element.cross_section.moment_of_inertia_about_z = 3000 # mm⁴ + element.cross_section.elastic_section_modulus_about_y_positive = 100 # mm³ + element.cross_section.elastic_section_modulus_about_y_negative = 90 # mm³ + element.cross_section.elastic_section_modulus_about_z_positive = 80 # mm³ + element.cross_section.elastic_section_modulus_about_z_negative = 70 # mm³ + element.cross_section.plastic_section_modulus_about_y = 60 # mm³ + element.cross_section.plastic_section_modulus_about_z = 50 # mm³ + element.cross_section.geometry = {"type": "rectangle", "width": 100, "height": 50} + element.cross_section.name = "MockSection" + return element + + +@pytest.fixture +def steel_cross_section(mock_steel_material: Mock, mock_steel_element: Mock) -> SteelCrossSection: + """Create a SteelCrossSection instance with mocked elements.""" + cross_section = Mock(spec=SteelCrossSection) + cross_section.steel_material = mock_steel_material + cross_section.elements = [mock_steel_element, mock_steel_element] + cross_section.steel_volume_per_meter = 0.001 + cross_section.steel_weight_per_meter = 7.85 + cross_section.steel_area = 1000 + cross_section.centroid = Point(50, 50) + cross_section.moment_of_inertia_about_y = 4000 + cross_section.moment_of_inertia_about_z = 6000 + cross_section.elastic_section_modulus_about_y_positive = 80 + cross_section.elastic_section_modulus_about_y_negative = 80 + cross_section.elastic_section_modulus_about_z_positive = 120 + cross_section.elastic_section_modulus_about_z_negative = 120 + cross_section.plastic_section_modulus_about_y = 100 + cross_section.plastic_section_modulus_about_z = 150 + cross_section.vertices = [[Point(0, 0), Point(100, 0), Point(100, 50), Point(0, 50)], [Point(0, 0), Point(100, 0), Point(100, 50), Point(0, 50)]] + return cross_section + + +def test_steel_volume_per_meter(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: + """Test the steel volume per meter calculation.""" + expected_volume = 2 * mock_steel_element.area * 1000 * 1e-9 # Convert mm³ to m³ + assert steel_cross_section.steel_volume_per_meter == expected_volume + + +def test_steel_weight_per_meter(steel_cross_section: SteelCrossSection, mock_steel_material: Mock, mock_steel_element: Mock) -> None: + """Test the steel weight per meter calculation.""" + expected_weight = mock_steel_material.density * 2 * mock_steel_element.area * 1000 * 1e-9 + assert steel_cross_section.steel_weight_per_meter == pytest.approx(expected_weight) + + +def test_steel_area(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: + """Test the total steel area calculation.""" + expected_area = 2 * mock_steel_element.area + assert steel_cross_section.steel_area == expected_area + + +def test_centroid(steel_cross_section: SteelCrossSection) -> None: + """Test the centroid calculation.""" + expected_centroid = Point(50, 50) + assert steel_cross_section.centroid == expected_centroid + + +def test_moment_of_inertia_about_y(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: + """Test the moment of inertia about the y-axis calculation.""" + expected_moi_y = 2 * mock_steel_element.moment_of_inertia_about_y + assert steel_cross_section.moment_of_inertia_about_y == expected_moi_y + + +def test_moment_of_inertia_about_z(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: + """Test the moment of inertia about the z-axis calculation.""" + expected_moi_z = 2 * mock_steel_element.moment_of_inertia_about_z + assert steel_cross_section.moment_of_inertia_about_z == expected_moi_z + + +def test_elastic_section_modulus_about_y_positive(steel_cross_section: SteelCrossSection) -> None: + """Test the elastic section modulus about the y-axis on the positive z side.""" + distance_to_top = 50 # Distance from centroid to top + expected_modulus = steel_cross_section.moment_of_inertia_about_y / distance_to_top + assert steel_cross_section.elastic_section_modulus_about_y_positive == expected_modulus + + +def test_elastic_section_modulus_about_y_negative(steel_cross_section: SteelCrossSection) -> None: + """Test the elastic section modulus about the y-axis on the negative z side.""" + distance_to_bottom = 50 # Distance from centroid to bottom + expected_modulus = steel_cross_section.moment_of_inertia_about_y / distance_to_bottom + assert steel_cross_section.elastic_section_modulus_about_y_negative == expected_modulus + + +def test_elastic_section_modulus_about_z_positive(steel_cross_section: SteelCrossSection) -> None: + """Test the elastic section modulus about the z-axis on the positive y side.""" + distance_to_right = 50 # Distance from centroid to right + expected_modulus = steel_cross_section.moment_of_inertia_about_z / distance_to_right + assert steel_cross_section.elastic_section_modulus_about_z_positive == expected_modulus + + +def test_elastic_section_modulus_about_z_negative(steel_cross_section: SteelCrossSection) -> None: + """Test the elastic section modulus about the z-axis on the negative y side.""" + distance_to_left = 50 # Distance from centroid to left + expected_modulus = steel_cross_section.moment_of_inertia_about_z / distance_to_left + assert steel_cross_section.elastic_section_modulus_about_z_negative == expected_modulus + + +def test_plastic_section_modulus_about_y(steel_cross_section: SteelCrossSection) -> None: + """Test the plastic section modulus about the y-axis.""" + # Mocked calculation based on the dotted mesh and weighted midpoint + assert steel_cross_section.plastic_section_modulus_about_y > 0 + + +def test_plastic_section_modulus_about_z(steel_cross_section: SteelCrossSection) -> None: + """Test the plastic section modulus about the z-axis.""" + # Mocked calculation based on the dotted mesh and weighted midpoint + assert steel_cross_section.plastic_section_modulus_about_z > 0 + + +def test_vertices(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: + """Test the vertices of the cross-section.""" + expected_vertices = [mock_steel_element.cross_section.vertices, mock_steel_element.cross_section.vertices] + assert steel_cross_section.vertices == expected_vertices From 55a70007be2fc47845ab989157b905cfc3d44239 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 09:52:11 +0200 Subject: [PATCH 04/70] Add SteelElement class and corresponding tests for steel cross-section functionality --- .../steel/steel_element.py | 143 +++++++++++++++++ .../steel/test_steel_element.py | 145 ++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 blueprints/structural_sections/steel/steel_element.py create mode 100644 tests/structural_sections/steel/test_steel_element.py diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py new file mode 100644 index 000000000..bed055c8a --- /dev/null +++ b/blueprints/structural_sections/steel/steel_element.py @@ -0,0 +1,143 @@ +"""Module containing the class definition for a steel cross-section element.""" + +from dataclasses import dataclass + +from shapely import Point, Polygon + +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.general_cross_section import CrossSection +from blueprints.type_alias import KG_M, MM, MM2, MM3, MM4, MPA +from blueprints.unit_conversion import MM2_TO_M2 + + +@dataclass(frozen=True, kw_only=True) +class SteelElement: + """ + General class for a steel cross-section element. + + Parameters + ---------- + cross_section : CrossSection + The cross-section of the steel element. + material : SteelMaterial + The material of the steel element. + """ + + cross_section: CrossSection + material: SteelMaterial + + def __post_init__(self) -> None: + """Check if the material is a SteelMaterial.""" + if not isinstance(self.material, SteelMaterial): + raise TypeError(f"Expected a SteelMaterial, but got: {type(self.material)}") + + @property + def name(self) -> str: + """Name of the steel element.""" + return self.cross_section.name + + @property + def area(self) -> MM2: + """Area of the cross-section [mm²].""" + return self.cross_section.area + + @property + def perimeter(self) -> MM: + """Perimeter of the cross-section [mm].""" + return self.cross_section.perimeter + + @property + def centroid(self) -> Point: + """Centroid of the cross-section [mm].""" + return self.cross_section.centroid + + @property + def moment_of_inertia_about_y(self) -> MM4: + """Moments of inertia of the cross-section [mm⁴].""" + return self.cross_section.moment_of_inertia_about_y + + @property + def moment_of_inertia_about_z(self) -> MM4: + """Moments of inertia of the cross-section [mm⁴].""" + return self.cross_section.moment_of_inertia_about_z + + @property + def elastic_section_modulus_about_y_positive(self) -> MM3: + """Elastic section modulus about the y-axis on the positive z side [mm³].""" + return self.cross_section.elastic_section_modulus_about_y_positive + + @property + def elastic_section_modulus_about_y_negative(self) -> MM3: + """Elastic section modulus about the y-axis on the negative z side [mm³].""" + return self.cross_section.elastic_section_modulus_about_y_negative + + @property + def elastic_section_modulus_about_z_positive(self) -> MM3: + """Elastic section modulus about the z-axis on the positive y side [mm³].""" + return self.cross_section.elastic_section_modulus_about_z_positive + + @property + def elastic_section_modulus_about_z_negative(self) -> MM3: + """Elastic section modulus about the z-axis on the negative y side [mm³].""" + return self.cross_section.elastic_section_modulus_about_z_negative + + @property + def plastic_section_modulus_about_y(self) -> MM3: + """Plastic section modulus about the y-axis [mm³].""" + return self.cross_section.plastic_section_modulus_about_y + + @property + def plastic_section_modulus_about_z(self) -> MM3: + """Plastic section modulus about the z-axis [mm³].""" + return self.cross_section.plastic_section_modulus_about_z + + @property + def geometry(self) -> Polygon: + """Return the geometry of the steel element.""" + return self.cross_section.geometry + + @property + def vertices(self) -> list[Point]: + """Return the vertices of the steel element.""" + return self.cross_section.vertices + + @property + def dotted_mesh(self) -> list: + """Return the dotted mesh of the steel element.""" + return self.cross_section.dotted_mesh(max_mesh_size=0) + + @property + def weight_per_meter(self) -> KG_M: + """ + Calculate the weight per meter of the steel element. + + Returns + ------- + KG_M + The weight per meter of the steel element. + """ + return self.material.density * (self.cross_section.area * MM2_TO_M2) + + @property + def yield_strength(self) -> MPA: + """ + Calculate the yield strength of the steel element. + + Returns + ------- + MPa + The yield strength of the steel element. + """ + return 0 + + @property + def ultimate_strength(self) -> MPA: + """ + Calculate the ultimate strength of the steel element. + + Returns + ------- + MPa + The ultimate strength of the steel element. + """ + return 0 diff --git a/tests/structural_sections/steel/test_steel_element.py b/tests/structural_sections/steel/test_steel_element.py new file mode 100644 index 000000000..00313397f --- /dev/null +++ b/tests/structural_sections/steel/test_steel_element.py @@ -0,0 +1,145 @@ +"""Test the SteelElement class.""" + +from unittest.mock import Mock + +import pytest +from shapely.geometry import Point + +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.general_cross_section import CrossSection +from blueprints.structural_sections.steel.steel_element import SteelElement + + +@pytest.fixture +def mock_cross_section(mocker: Mock) -> Mock: + """Mock a CrossSection object.""" + cross_section: Mock = mocker.Mock(spec=CrossSection) + cross_section.name = "MockSection" + cross_section.area = 683 # mm² + cross_section.perimeter = 400 # mm + cross_section.centroid = Point(50, 50) + cross_section.moment_of_inertia_about_y = 2000 # mm⁴ + cross_section.moment_of_inertia_about_z = 3000 # mm⁴ + cross_section.elastic_section_modulus_about_y_positive = 100 # mm³ + cross_section.elastic_section_modulus_about_y_negative = 90 # mm³ + cross_section.elastic_section_modulus_about_z_positive = 80 # mm³ + cross_section.elastic_section_modulus_about_z_negative = 70 # mm³ + cross_section.plastic_section_modulus_about_y = 60 # mm³ + cross_section.plastic_section_modulus_about_z = 50 # mm³ + cross_section.geometry = {"type": "rectangle", "width": 100, "height": 50} + cross_section.vertices = [Point(0, 0), Point(100, 0), Point(100, 50), Point(0, 50)] + cross_section.dotted_mesh.return_value = [(10, 10), (20, 20)] + return cross_section + + +@pytest.fixture +def mock_material(mocker: Mock) -> Mock: + """Mock a SteelMaterial object.""" + material: Mock = mocker.Mock(spec=SteelMaterial) + material.density = 7850 # kg/m³ + material.yield_strength.return_value = 250 # MPa + material.ultimate_strength.return_value = 400 # MPa + return material + + +@pytest.fixture +def steel_element(mock_cross_section: Mock, mock_material: Mock) -> SteelElement: + """Create a SteelElement instance using mocked cross-section and material.""" + return SteelElement(cross_section=mock_cross_section, material=mock_material) + + +def test_name(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement name matches the mock cross-section name.""" + assert steel_element.name == mock_cross_section.name + + +def test_area(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement area matches the mock cross-section area.""" + assert steel_element.area == mock_cross_section.area + + +def test_perimeter(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement perimeter matches the mock cross-section perimeter.""" + assert steel_element.perimeter == mock_cross_section.perimeter + + +def test_centroid(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement centroid matches the mock cross-section centroid.""" + assert steel_element.centroid == mock_cross_section.centroid + + +def test_moment_of_inertia_about_y(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement moment of inertia about Y matches the mock cross-section.""" + assert steel_element.moment_of_inertia_about_y == mock_cross_section.moment_of_inertia_about_y + + +def test_moment_of_inertia_about_z(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement moment of inertia about Z matches the mock cross-section.""" + assert steel_element.moment_of_inertia_about_z == mock_cross_section.moment_of_inertia_about_z + + +def test_elastic_section_modulus_about_y_positive(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement elastic section modulus about Y+ matches the mock cross-section.""" + assert steel_element.elastic_section_modulus_about_y_positive == mock_cross_section.elastic_section_modulus_about_y_positive + + +def test_elastic_section_modulus_about_y_negative(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement elastic section modulus about Y- matches the mock cross-section.""" + assert steel_element.elastic_section_modulus_about_y_negative == mock_cross_section.elastic_section_modulus_about_y_negative + + +def test_elastic_section_modulus_about_z_positive(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement elastic section modulus about Z+ matches the mock cross-section.""" + assert steel_element.elastic_section_modulus_about_z_positive == mock_cross_section.elastic_section_modulus_about_z_positive + + +def test_elastic_section_modulus_about_z_negative(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement elastic section modulus about Z- matches the mock cross-section.""" + assert steel_element.elastic_section_modulus_about_z_negative == mock_cross_section.elastic_section_modulus_about_z_negative + + +def test_plastic_section_modulus_about_y(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement plastic section modulus about Y matches the mock cross-section.""" + assert steel_element.plastic_section_modulus_about_y == mock_cross_section.plastic_section_modulus_about_y + + +def test_plastic_section_modulus_about_z(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement plastic section modulus about Z matches the mock cross-section.""" + assert steel_element.plastic_section_modulus_about_z == mock_cross_section.plastic_section_modulus_about_z + + +def test_geometry(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement geometry matches the mock cross-section geometry.""" + assert steel_element.geometry == mock_cross_section.geometry + + +def test_vertices(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement vertices match the mock cross-section vertices.""" + assert steel_element.vertices == mock_cross_section.vertices + + +def test_dotted_mesh(steel_element: SteelElement, mock_cross_section: Mock) -> None: + """Test that the SteelElement dotted mesh matches the mock cross-section dotted mesh.""" + assert steel_element.dotted_mesh == mock_cross_section.dotted_mesh.return_value + + +def test_weight_per_meter(steel_element: SteelElement, mock_cross_section: Mock, mock_material: Mock) -> None: + """Test that the SteelElement weight per meter is calculated correctly.""" + expected_weight: float = mock_material.density * (mock_cross_section.area * 1e-6) + assert steel_element.weight_per_meter == expected_weight + + +def test_yield_strength(steel_element: SteelElement, mock_material: Mock) -> None: + """Test that the SteelElement yield strength matches the mock material yield strength.""" + assert steel_element.yield_strength == mock_material.yield_strength.return_value + + +def test_ultimate_strength(steel_element: SteelElement, mock_material: Mock) -> None: + """Test that the SteelElement ultimate strength matches the mock material ultimate strength.""" + assert steel_element.ultimate_strength == mock_material.ultimate_strength.return_value + + +def test_invalid_material_type(mock_cross_section: Mock) -> None: + """Test that creating a SteelElement with an invalid material type raises a TypeError.""" + with pytest.raises(TypeError): + SteelElement(cross_section=mock_cross_section, material=mock_material) From e133a615cc020ef3bb16328077174805180ddacb Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 13:23:12 +0200 Subject: [PATCH 05/70] Add CHSSteelProfile class and corresponding tests for circular hollow section functionality --- .../steel/steel_cross_sections/chs_profile.py | 103 ++++++++++++++++++ .../steel_cross_sections/test_chs_profile.py | 101 +++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py create mode 100644 tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py diff --git a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py new file mode 100644 index 000000000..004d1c4e5 --- /dev/null +++ b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py @@ -0,0 +1,103 @@ +"""Circular Hollow Section (CHS) steel profile.""" + +from matplotlib import pyplot as plt + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.cross_section_tube import TubeCrossSection +from blueprints.structural_sections.steel.steel_cross_sections.base import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections.plotters.general_steel_plotter import plot_shapes +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS +from blueprints.structural_sections.steel.steel_element import SteelElement +from blueprints.type_alias import MM + + +class CHSSteelProfile(SteelCrossSection): + """Representation of a Circular Hollow Section (CHS) steel profile. + + Parameters + ---------- + outer_diameter : MM + The outer diameter of the CHS profile [mm]. + wall_thickness : MM + The wall thickness of the CHS profile [mm]. + steel_class : SteelStrengthClass + The steel strength class of the profile. + """ + + def __init__( + self, + outer_diameter: MM, + wall_thickness: MM, + steel_class: SteelStrengthClass, + ) -> None: + """Initialize the CHS steel profile.""" + self.thickness = wall_thickness + self.outer_diameter = outer_diameter + self.inner_diameter = outer_diameter - 2 * wall_thickness + + self.chs = TubeCrossSection( + name="Ring", + outer_diameter=self.outer_diameter, + inner_diameter=self.inner_diameter, + x=0, + y=0, + ) + self.steel_material = SteelMaterial(steel_class=steel_class) + self.elements = [SteelElement(cross_section=self.chs, material=self.steel_material)] + + def plot(self, *args, **kwargs) -> plt.Figure: + """Plot the cross-section. Making use of the standard plotter. + + Parameters + ---------- + *args + Additional arguments passed to the plotter. + **kwargs + Additional keyword arguments passed to the plotter. + """ + return plot_shapes( + self, + *args, + **kwargs, + ) + + +class LoadStandardCHS: + r"""Class to load in values for standard CHS profile. + + Parameters + ---------- + steel_class: SteelStrengthClass + Enumeration of steel strength classes (default: S355) + profile: CHS + Enumeration of standard CHS profiles (default: CHS508x20) + """ + + def __init__( + self, + steel_class: SteelStrengthClass = SteelStrengthClass.S355, + profile: CHS = CHS.CHS508x20, + ) -> None: + self.steel_class = steel_class + self.profile = profile + + def __str__(self) -> str: + """Return the steel class and profile.""" + return f"Steel class: {self.steel_class}, Profile: {self.profile}" + + def alias(self) -> str: + """Return the alias of the CHS profile.""" + return self.profile.alias + + def diameter(self) -> MM: + """Return the outer diameter of the CHS profile.""" + return self.profile.diameter + + def thickness(self) -> MM: + """Return the wall thickness of the CHS profile.""" + return self.profile.thickness + + def get_profile(self) -> CHSSteelProfile: + """Return the CHS profile.""" + return CHSSteelProfile(outer_diameter=self.diameter(), wall_thickness=self.thickness(), steel_class=self.steel_class) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py new file mode 100644 index 000000000..c14e3e431 --- /dev/null +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -0,0 +1,101 @@ +"""Test suite for the CHSSteelProfile class.""" + +import pytest +from matplotlib import pyplot as plt +from matplotlib.figure import Figure + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile, LoadStandardCHS +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS + + +class TestCHSSteelProfile: + """Test suite for CHSSteelProfile.""" + + @pytest.fixture + def chs_profile(self) -> CHSSteelProfile: + """Fixture to set up a CHS profile for testing.""" + profile: CHS = CHS.CHS508x16 + steel_class: SteelStrengthClass = SteelStrengthClass.S355 + return LoadStandardCHS(profile=profile, steel_class=steel_class).get_profile() + + def test_str(self) -> None: + """Test the string representation of the CHS profile.""" + profile: CHS = CHS.CHS508x16 + steel_class: SteelStrengthClass = SteelStrengthClass.S355 + expected_str: str = "Steel class: SteelStrengthClass.EN_10025_2_S355, Profile: CHS.CHS_508x16" + assert LoadStandardCHS(profile=profile, steel_class=steel_class).__str__() == expected_str + + def test_code(self) -> None: + """Test the code of the CHS profile.""" + profile: CHS = CHS.CHS508x16 + steel_class: SteelStrengthClass = SteelStrengthClass.S355 + alias: str = LoadStandardCHS(profile=profile, steel_class=steel_class).alias() + expected_alias: str = "CHS 508x16" + assert alias == expected_alias + + def test_steel_volume_per_meter(self, chs_profile: CHSSteelProfile) -> None: + """Test the steel volume per meter.""" + expected_volume: float = 2.47e-2 # m³/m + assert pytest.approx(chs_profile.steel_volume_per_meter, rel=1e-2) == expected_volume + + def test_steel_weight_per_meter(self, chs_profile: CHSSteelProfile) -> None: + """Test the steel weight per meter.""" + expected_weight: float = 2.47e-2 * 7850 # kg/m + assert pytest.approx(chs_profile.steel_weight_per_meter, rel=1e-2) == expected_weight + + def test_steel_area(self, chs_profile: CHSSteelProfile) -> None: + """Test the steel cross-sectional area.""" + expected_area: float = 2.47e4 # mm² + assert pytest.approx(chs_profile.steel_area, rel=1e-2) == expected_area + + def test_centroid(self, chs_profile: CHSSteelProfile) -> None: + """Test the centroid of the steel cross-section.""" + expected_centroid: tuple[float, float] = (0, 0) # (x, y) coordinates + assert pytest.approx(chs_profile.centroid.x, rel=1e-2) == expected_centroid[0] + assert pytest.approx(chs_profile.centroid.y, rel=1e-2) == expected_centroid[1] + + def test_moment_of_inertia_about_y(self, chs_profile: CHSSteelProfile) -> None: + """Test the moment of inertia about the y-axis.""" + expected_moi_y: float = 7.4909e8 # mm⁴ + assert pytest.approx(chs_profile.moment_of_inertia_about_y, rel=1e-2) == expected_moi_y + + def test_moment_of_inertia_about_z(self, chs_profile: CHSSteelProfile) -> None: + """Test the moment of inertia about the z-axis.""" + expected_moi_z: float = 7.4909e8 # mm⁴ + assert pytest.approx(chs_profile.moment_of_inertia_about_z, rel=1e-2) == expected_moi_z + + def test_elastic_section_modulus_about_y_positive(self, chs_profile: CHSSteelProfile) -> None: + """Test the elastic section modulus about the y-axis on the positive z side.""" + expected_modulus_y_positive: float = 2.9490e6 # mm³ + assert pytest.approx(chs_profile.elastic_section_modulus_about_y_positive, rel=1e-2) == expected_modulus_y_positive + + def test_elastic_section_modulus_about_y_negative(self, chs_profile: CHSSteelProfile) -> None: + """Test the elastic section modulus about the y-axis on the negative z side.""" + expected_modulus_y_negative: float = 2.9490e6 # mm³ + assert pytest.approx(chs_profile.elastic_section_modulus_about_y_negative, rel=1e-2) == expected_modulus_y_negative + + def test_elastic_section_modulus_about_z_positive(self, chs_profile: CHSSteelProfile) -> None: + """Test the elastic section modulus about the z-axis on the positive y side.""" + expected_modulus_z_positive: float = 2.9490e6 # mm³ + assert pytest.approx(chs_profile.elastic_section_modulus_about_z_positive, rel=1e-2) == expected_modulus_z_positive + + def test_elastic_section_modulus_about_z_negative(self, chs_profile: CHSSteelProfile) -> None: + """Test the elastic section modulus about the z-axis on the negative y side.""" + expected_modulus_z_negative: float = 2.9490e6 # mm³ + assert pytest.approx(chs_profile.elastic_section_modulus_about_z_negative, rel=1e-2) == expected_modulus_z_negative + + def test_plastic_section_modulus_about_y(self, chs_profile: CHSSteelProfile) -> None: + """Test the plastic section modulus about the y-axis.""" + expected_plastic_modulus_y: float = 3.874e6 # mm³ + assert pytest.approx(chs_profile.plastic_section_modulus_about_y, rel=1e-2) == expected_plastic_modulus_y + + def test_plastic_section_modulus_about_z(self, chs_profile: CHSSteelProfile) -> None: + """Test the plastic section modulus about the z-axis.""" + expected_plastic_modulus_z: float = 3.874e6 # mm³ + assert pytest.approx(chs_profile.plastic_section_modulus_about_z, rel=1e-2) == expected_plastic_modulus_z + + def test_plot(self, chs_profile: CHSSteelProfile) -> None: + """Test the plot method (ensure it runs without errors).""" + fig: Figure = chs_profile.plot() + assert isinstance(fig, plt.Figure) From 6fd18a69139f1f1485bdca3172a94a2a31831de7 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 13:23:47 +0200 Subject: [PATCH 06/70] Add StripSteelProfile class and corresponding tests for steel strip profile functionality --- .../steel_cross_sections/strip_profile.py | 103 ++++++++++++++++++ .../test_strip_profile.py | 101 +++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py create mode 100644 tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py diff --git a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py new file mode 100644 index 000000000..8756abcde --- /dev/null +++ b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py @@ -0,0 +1,103 @@ +"""Steel Strip Profile.""" + +from matplotlib import pyplot as plt + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.cross_section_rectangle import RectangularCrossSection +from blueprints.structural_sections.steel.steel_cross_sections.base import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections.plotters.general_steel_plotter import plot_shapes +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip +from blueprints.structural_sections.steel.steel_element import SteelElement +from blueprints.type_alias import MM + + +class StripSteelProfile(SteelCrossSection): + """Representation of a Steel Strip profile. + + Parameters + ---------- + width : MM + The width of the strip profile [mm]. + height : MM + The height (thickness) of the strip profile [mm]. + steel_class : SteelStrengthClass + The steel strength class of the profile. + """ + + def __init__( + self, + width: MM, + height: MM, + steel_class: SteelStrengthClass, + ) -> None: + """Initialize the Steel Strip profile.""" + self.width = width + self.height = height + + self.strip = RectangularCrossSection( + name="Steel Strip", + width=self.width, + height=self.height, + x=0, + y=0, + ) + + self.steel_material = SteelMaterial(steel_class=steel_class) + self.elements = [SteelElement(cross_section=self.strip, material=self.steel_material)] + + def plot(self, *args, **kwargs) -> plt.Figure: + """Plot the cross-section. Making use of the standard plotter. + + Parameters + ---------- + *args + Additional arguments passed to the plotter. + **kwargs + Additional keyword arguments passed to the plotter. + """ + return plot_shapes( + self, + *args, + **kwargs, + ) + + +class LoadStandardStrip: + r"""Class to load in values for standard Strip profile. + + Parameters + ---------- + steel_class: SteelStrengthClass + Enumeration of steel strength classes (default: S355) + profile: Strip + Enumeration of standard steel strip profiles (default: STRIP160x5) + """ + + def __init__( + self, + steel_class: SteelStrengthClass = SteelStrengthClass.S355, + profile: Strip = Strip.STRIP160x5, + ) -> None: + self.steel_class = steel_class + self.profile = profile + + def __str__(self) -> str: + """Return the steel class and profile.""" + return f"Steel class: {self.steel_class}, Profile: {self.profile}" + + def alias(self) -> str: + """Return the code of the strip profile.""" + return self.profile.alias + + def width(self) -> MM: + """Return the width of the strip profile.""" + return self.profile.width + + def height(self) -> MM: + """Return the height (thickness) of the strip profile.""" + return self.profile.height + + def get_profile(self) -> StripSteelProfile: + """Return the strip profile.""" + return StripSteelProfile(width=self.width(), height=self.height(), steel_class=self.steel_class) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py new file mode 100644 index 000000000..eb070b2f6 --- /dev/null +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -0,0 +1,101 @@ +"""Test suite for StripSteelProfile.""" + +import matplotlib.pyplot as plt +import pytest + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip +from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import LoadStandardStrip, StripSteelProfile + + +class TestStripSteelProfile: + """Test suite for StripSteelProfile.""" + + @pytest.fixture + def strip_profile(self) -> StripSteelProfile: + """Fixture to set up a Strip profile for testing.""" + profile = Strip.STRIP160x5 + steel_class = SteelStrengthClass.S355 + return LoadStandardStrip(profile=profile, steel_class=steel_class).get_profile() + + def test_str(self) -> None: + """Test the string representation of the Strip profile.""" + profile = Strip.STRIP160x5 + steel_class = SteelStrengthClass.S355 + desc = LoadStandardStrip(profile=profile, steel_class=steel_class).__str__() + expected_str = "Steel class: SteelStrengthClass.EN_10025_2_S355, Profile: StripStandardProfileClass.STRIP_160x5" + assert desc == expected_str + + def test_code(self) -> None: + """Test the code of the Strip profile.""" + profile = Strip.STRIP160x5 + steel_class = SteelStrengthClass.S355 + alias = LoadStandardStrip(profile=profile, steel_class=steel_class).alias() + expected_alias = "160x5" + assert alias == expected_alias + + def test_steel_volume_per_meter(self, strip_profile: StripSteelProfile) -> None: + """Test the steel volume per meter.""" + expected_volume = 0.160 * 0.005 # m³/m + assert pytest.approx(strip_profile.steel_volume_per_meter, rel=1e-6) == expected_volume + + def test_steel_weight_per_meter(self, strip_profile: StripSteelProfile) -> None: + """Test the steel weight per meter.""" + expected_weight = 0.160 * 0.005 * 7850 # kg/m + assert pytest.approx(strip_profile.steel_weight_per_meter, rel=1e-6) == expected_weight + + def test_steel_area(self, strip_profile: StripSteelProfile) -> None: + """Test the steel cross-sectional area.""" + expected_area = 160 * 5 # mm² + assert pytest.approx(strip_profile.steel_area, rel=1e-6) == expected_area + + def test_centroid(self, strip_profile: StripSteelProfile) -> None: + """Test the centroid of the steel cross-section.""" + expected_centroid = (0, 0) # (x, y) coordinates + assert pytest.approx(strip_profile.centroid.x, rel=1e-6) == expected_centroid[0] + assert pytest.approx(strip_profile.centroid.y, rel=1e-6) == expected_centroid[1] + + def test_moment_of_inertia_about_y(self, strip_profile: StripSteelProfile) -> None: + """Test the moment of inertia about the y-axis.""" + expected_moi_y = 1 / 12 * 160 * 5**3 # mm⁴ + assert pytest.approx(strip_profile.moment_of_inertia_about_y, rel=1e-6) == expected_moi_y + + def test_moment_of_inertia_about_z(self, strip_profile: StripSteelProfile) -> None: + """Test the moment of inertia about the z-axis.""" + expected_moi_z = 1 / 12 * 160**3 * 5 # mm⁴ + assert pytest.approx(strip_profile.moment_of_inertia_about_z, rel=1e-6) == expected_moi_z + + def test_elastic_section_modulus_about_y_positive(self, strip_profile: StripSteelProfile) -> None: + """Test the elastic section modulus about the y-axis on the positive z side.""" + expected_modulus_y_positive = 1 / 6 * 160 * 5**2 # mm³ + assert pytest.approx(strip_profile.elastic_section_modulus_about_y_positive, rel=1e-6) == expected_modulus_y_positive + + def test_elastic_section_modulus_about_y_negative(self, strip_profile: StripSteelProfile) -> None: + """Test the elastic section modulus about the y-axis on the negative z side.""" + expected_modulus_y_negative = 1 / 6 * 160 * 5**2 # mm³ + assert pytest.approx(strip_profile.elastic_section_modulus_about_y_negative, rel=1e-6) == expected_modulus_y_negative + + def test_elastic_section_modulus_about_z_positive(self, strip_profile: StripSteelProfile) -> None: + """Test the elastic section modulus about the z-axis on the positive y side.""" + expected_modulus_z_positive = 1 / 6 * 160**2 * 5 # mm³ + assert pytest.approx(strip_profile.elastic_section_modulus_about_z_positive, rel=1e-6) == expected_modulus_z_positive + + def test_elastic_section_modulus_about_z_negative(self, strip_profile: StripSteelProfile) -> None: + """Test the elastic section modulus about the z-axis on the negative y side.""" + expected_modulus_z_negative = 1 / 6 * 160**2 * 5 # mm³ + assert pytest.approx(strip_profile.elastic_section_modulus_about_z_negative, rel=1e-6) == expected_modulus_z_negative + + def test_plastic_section_modulus_about_y(self, strip_profile: StripSteelProfile) -> None: + """Test the plastic section modulus about the y-axis.""" + expected_plastic_modulus_y = 1 / 4 * 160 * 5**2 # mm³ + assert pytest.approx(strip_profile.plastic_section_modulus_about_y, rel=1e-6) == expected_plastic_modulus_y + + def test_plastic_section_modulus_about_z(self, strip_profile: StripSteelProfile) -> None: + """Test the plastic section modulus about the z-axis.""" + expected_plastic_modulus_z = 1 / 4 * 160**2 * 5 # mm³ + assert pytest.approx(strip_profile.plastic_section_modulus_about_z, rel=1e-6) == expected_plastic_modulus_z + + def test_plot(self, strip_profile: StripSteelProfile) -> None: + """Test the plot method (ensure it runs without errors).""" + fig = strip_profile.plot(show=False) + assert isinstance(fig, plt.Figure) From 1330cbd677e9e6b44515ccaad92e740e340238ca Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 13:24:23 +0200 Subject: [PATCH 07/70] Add examples and documentation for visualizing steel profile shapes --- docs/examples/_code/steel_profile_shapes.py | 40 +++++++++++++ docs/examples/steel_profile_shapes.md | 65 +++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 docs/examples/_code/steel_profile_shapes.py create mode 100644 docs/examples/steel_profile_shapes.md diff --git a/docs/examples/_code/steel_profile_shapes.py b/docs/examples/_code/steel_profile_shapes.py new file mode 100644 index 000000000..9d0421530 --- /dev/null +++ b/docs/examples/_code/steel_profile_shapes.py @@ -0,0 +1,40 @@ +"""Steel Profile Shapes Example +This example demonstrates how to create and visualize different steel profile shapes using the Blueprints library. +""" + +# ruff: noqa: T201 + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile, LoadStandardCHS +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip +from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import LoadStandardStrip, StripSteelProfile + +# Define steel class +steel_class = SteelStrengthClass.S355 + +# Example usage for CHS profile +chs_profile = LoadStandardCHS(profile=CHS.CHS273x5, steel_class=steel_class).get_profile() +chs_profile.plot(show=True) +print(f"Steel class: {chs_profile.steel_material}") +print(f"Moment of inertia about y-axis: {chs_profile.moment_of_inertia_about_y} mm⁴") +print(f"Moment of inertia about z-axis: {chs_profile.moment_of_inertia_about_z} mm⁴") +print(f"Elastic section modulus about y-axis: {chs_profile.elastic_section_modulus_about_y_negative} mm³") +print(f"Elastic section modulus about z-axis: {chs_profile.elastic_section_modulus_about_z_positive} mm³") +print(f"Area: {chs_profile.steel_area} mm²") + +# Example usage for custom CHS profile +custom_chs_profile = CHSSteelProfile(outer_diameter=150, wall_thickness=10, steel_class=steel_class) +custom_chs_profile.plot(show=True) + +# Example usage for Strip profile +strip_profile = LoadStandardStrip(profile=Strip.STRIP160x5, steel_class=steel_class).get_profile() +strip_profile.plot(show=True) + +# Example usage for custom Strip profile +custom_strip_profile = StripSteelProfile( + width=100, + height=30, + steel_class=steel_class, +) +custom_strip_profile.plot(show=True) diff --git a/docs/examples/steel_profile_shapes.md b/docs/examples/steel_profile_shapes.md new file mode 100644 index 000000000..dfc3b0981 --- /dev/null +++ b/docs/examples/steel_profile_shapes.md @@ -0,0 +1,65 @@ +--- +hide: + - toc +--- +# Steel Profile Shapes + +Steel profiles are essential components in structural engineering, and their properties are critical for designing safe and efficient structures. This example demonstrates how to work with various steel profile shapes using `Blueprints`. The library provides predefined standard profiles as well as the ability to define custom profiles. + +Follow the steps below to explore the usage of different steel profile shapes (or [go to the full code example](#full-code-example)): + +## Define the Steel Class + +Start by defining the steel class to be used for the profiles: + +```python +--8<-- "examples/_code/steel_profile_shapes.py:17:18" +``` + +## Circular Hollow Section (CHS) Profiles + +### Standard CHS Profile + +Structual parameters are automatically calculated and can be obtained with: + +```python +--8<-- "examples/_code/steel_profile_shapes.py:20:28" +``` + +### Custom CHS Profile + +Alternatively, define a custom CHS profile by specifying its dimensions: + +```python +--8<-- "examples/_code/steel_profile_shapes.py:30:32" +``` + +## Strip Profiles + +### Standard Strip Profile + +Predefined strip profiles are also available: + +```python +--8<-- "examples/_code/steel_profile_shapes.py:30:32" +``` + +### Custom Strip Profile + +Define a custom strip profile by specifying its width and height: + +```python +--8<-- "examples/_code/steel_profile_shapes.py:34:40" +``` + +## Visualizing Profiles + +For each profile, the `plot` method is used to visualize the shape. The plots will display the geometry of the profiles, making it easier to understand their dimensions and configurations. + + +## Full Code Example + +```python +--8<-- "examples/_code/steel_profile_shapes.py" +``` + \ No newline at end of file From c34349584d416185c57f4e22e5461d2d3c201915 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 13:57:59 +0200 Subject: [PATCH 08/70] Refactor elastic section modulus calculations and remove plastic section modulus properties --- .../steel/steel_cross_sections/base.py | 55 ++----------------- .../steel/steel_cross_sections/test_base.py | 18 ------ 2 files changed, 5 insertions(+), 68 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/base.py b/blueprints/structural_sections/steel/steel_cross_sections/base.py index f29c14aad..6ad1a89a2 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/base.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/base.py @@ -6,7 +6,7 @@ from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_element import SteelElement -from blueprints.type_alias import KG_M, M3_M, MM2, MM3 +from blueprints.type_alias import KG_M, M3_M, MM2 from blueprints.unit_conversion import MM3_TO_M3 @@ -69,68 +69,23 @@ def moment_of_inertia_about_z(self) -> KG_M: @property def elastic_section_modulus_about_y_positive(self) -> KG_M: """Elastic section modulus about the y-axis on the positive z side [mm³].""" - distance_to_top = max(point.y for element in self.elements for point in element.cross_section.vertices) - self.centroid.y + distance_to_top = max(y for element in self.elements for _, y in element.geometry.points) - self.centroid.y return self.moment_of_inertia_about_y / distance_to_top @property def elastic_section_modulus_about_y_negative(self) -> KG_M: """Elastic section modulus about the y-axis on the negative z side [mm³].""" - distance_to_bottom = self.centroid.y - min(point.y for element in self.elements for point in element.cross_section.vertices) + distance_to_bottom = self.centroid.y - min(y for element in self.elements for _, y in element.geometry.points) return self.moment_of_inertia_about_y / distance_to_bottom @property def elastic_section_modulus_about_z_positive(self) -> KG_M: """Elastic section modulus about the z-axis on the positive y side [mm³].""" - distance_to_right = max(point.x for element in self.elements for point in element.cross_section.vertices) - self.centroid.x + distance_to_right = max(x for element in self.elements for x, _ in element.geometry.points) - self.centroid.x return self.moment_of_inertia_about_z / distance_to_right @property def elastic_section_modulus_about_z_negative(self) -> KG_M: """Elastic section modulus about the z-axis on the negative y side [mm³].""" - distance_to_left = self.centroid.x - min(point.x for element in self.elements for point in element.cross_section.vertices) + distance_to_left = self.centroid.x - min(x for element in self.elements for x, _ in element.geometry.points) return self.moment_of_inertia_about_z / distance_to_left - - @property - def plastic_section_modulus_about_y(self) -> MM3: - """Plastic section modulus about the y-axis [mm³].""" - # Calculate the area and yield strength weighted midpoint - total_weighted_area = sum(element.area * element.yield_strength for element in self.elements) - weighted_midpoint_y = sum(element.centroid.y * element.area * element.yield_strength for element in self.elements) / total_weighted_area - - # Create a dotted mesh for each element - max_mesh_size = 0 # zero lets the cross-section decide the mesh size - dotted_meshes = [element.cross_section.dotted_mesh(max_mesh_size) for element in self.elements] - - # Calculate the plastic section modulus by integrating the area over the distance to the weighted midpoint - plastic_section_modulus: float = 0.0 - for element, dotted_mesh in zip(self.elements, dotted_meshes): - mesh_area = element.area / len(dotted_mesh) - for node in dotted_mesh: - plastic_section_modulus += abs(node.y - weighted_midpoint_y) * mesh_area - - return plastic_section_modulus - - @property - def plastic_section_modulus_about_z(self) -> MM3: - """Plastic section modulus about the z-axis [mm³].""" - # Calculate the area and yield strength weighted midpoint - total_weighted_area = sum(element.area * element.yield_strength for element in self.elements) - weighted_midpoint_x = sum(element.centroid.x * element.area * element.yield_strength for element in self.elements) / total_weighted_area - - # Create a dotted mesh for each element - max_mesh_size = 0 # zero lets the cross-section decide the mesh size - dotted_meshes = [element.cross_section.dotted_mesh(max_mesh_size) for element in self.elements] - - # Calculate the plastic section modulus by integrating the area over the distance to the weighted midpoint - plastic_section_modulus: float = 0.0 - for element, dotted_mesh in zip(self.elements, dotted_meshes): - mesh_area = element.area / len(dotted_mesh) - for node in dotted_mesh: - plastic_section_modulus += abs(node.x - weighted_midpoint_x) * mesh_area - - return plastic_section_modulus - - @property - def vertices(self) -> list[Point]: - """Vertices of the cross-section.""" - return [point for element in self.elements for point in element.cross_section.vertices] diff --git a/tests/structural_sections/steel/steel_cross_sections/test_base.py b/tests/structural_sections/steel/steel_cross_sections/test_base.py index 06ec35819..4f0060309 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_base.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_base.py @@ -129,21 +129,3 @@ def test_elastic_section_modulus_about_z_negative(steel_cross_section: SteelCros distance_to_left = 50 # Distance from centroid to left expected_modulus = steel_cross_section.moment_of_inertia_about_z / distance_to_left assert steel_cross_section.elastic_section_modulus_about_z_negative == expected_modulus - - -def test_plastic_section_modulus_about_y(steel_cross_section: SteelCrossSection) -> None: - """Test the plastic section modulus about the y-axis.""" - # Mocked calculation based on the dotted mesh and weighted midpoint - assert steel_cross_section.plastic_section_modulus_about_y > 0 - - -def test_plastic_section_modulus_about_z(steel_cross_section: SteelCrossSection) -> None: - """Test the plastic section modulus about the z-axis.""" - # Mocked calculation based on the dotted mesh and weighted midpoint - assert steel_cross_section.plastic_section_modulus_about_z > 0 - - -def test_vertices(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: - """Test the vertices of the cross-section.""" - expected_vertices = [mock_steel_element.cross_section.vertices, mock_steel_element.cross_section.vertices] - assert steel_cross_section.vertices == expected_vertices From a082479fa640d17fac4657c9b5e261086bb9915a Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 13:59:03 +0200 Subject: [PATCH 09/70] Add fixtures for StripSteelProfile and CHSSteelProfile testing --- .../steel/steel_cross_sections/conftest.py | 25 +++++++++++++++++++ .../steel_cross_sections/test_chs_profile.py | 17 ------------- .../test_strip_profile.py | 17 ------------- 3 files changed, 25 insertions(+), 34 deletions(-) create mode 100644 tests/structural_sections/steel/steel_cross_sections/conftest.py diff --git a/tests/structural_sections/steel/steel_cross_sections/conftest.py b/tests/structural_sections/steel/steel_cross_sections/conftest.py new file mode 100644 index 000000000..facf11dc6 --- /dev/null +++ b/tests/structural_sections/steel/steel_cross_sections/conftest.py @@ -0,0 +1,25 @@ +"""Fixtures for testing steel cross sections.""" + +import pytest + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile, LoadStandardCHS +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip +from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import LoadStandardStrip, StripSteelProfile + + +@pytest.fixture +def strip_profile() -> StripSteelProfile: + """Fixture to set up a Strip profile for testing.""" + profile = Strip.STRIP160x5 + steel_class = SteelStrengthClass.S355 + return LoadStandardStrip(profile=profile, steel_class=steel_class).get_profile() + + +@pytest.fixture +def chs_profile() -> CHSSteelProfile: + """Fixture to set up a CHS profile for testing.""" + profile: CHS = CHS.CHS508x16 + steel_class: SteelStrengthClass = SteelStrengthClass.S355 + return LoadStandardCHS(profile=profile, steel_class=steel_class).get_profile() diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index c14e3e431..893edb329 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -12,13 +12,6 @@ class TestCHSSteelProfile: """Test suite for CHSSteelProfile.""" - @pytest.fixture - def chs_profile(self) -> CHSSteelProfile: - """Fixture to set up a CHS profile for testing.""" - profile: CHS = CHS.CHS508x16 - steel_class: SteelStrengthClass = SteelStrengthClass.S355 - return LoadStandardCHS(profile=profile, steel_class=steel_class).get_profile() - def test_str(self) -> None: """Test the string representation of the CHS profile.""" profile: CHS = CHS.CHS508x16 @@ -85,16 +78,6 @@ def test_elastic_section_modulus_about_z_negative(self, chs_profile: CHSSteelPro expected_modulus_z_negative: float = 2.9490e6 # mm³ assert pytest.approx(chs_profile.elastic_section_modulus_about_z_negative, rel=1e-2) == expected_modulus_z_negative - def test_plastic_section_modulus_about_y(self, chs_profile: CHSSteelProfile) -> None: - """Test the plastic section modulus about the y-axis.""" - expected_plastic_modulus_y: float = 3.874e6 # mm³ - assert pytest.approx(chs_profile.plastic_section_modulus_about_y, rel=1e-2) == expected_plastic_modulus_y - - def test_plastic_section_modulus_about_z(self, chs_profile: CHSSteelProfile) -> None: - """Test the plastic section modulus about the z-axis.""" - expected_plastic_modulus_z: float = 3.874e6 # mm³ - assert pytest.approx(chs_profile.plastic_section_modulus_about_z, rel=1e-2) == expected_plastic_modulus_z - def test_plot(self, chs_profile: CHSSteelProfile) -> None: """Test the plot method (ensure it runs without errors).""" fig: Figure = chs_profile.plot() diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index eb070b2f6..ac5bd8b89 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -11,13 +11,6 @@ class TestStripSteelProfile: """Test suite for StripSteelProfile.""" - @pytest.fixture - def strip_profile(self) -> StripSteelProfile: - """Fixture to set up a Strip profile for testing.""" - profile = Strip.STRIP160x5 - steel_class = SteelStrengthClass.S355 - return LoadStandardStrip(profile=profile, steel_class=steel_class).get_profile() - def test_str(self) -> None: """Test the string representation of the Strip profile.""" profile = Strip.STRIP160x5 @@ -85,16 +78,6 @@ def test_elastic_section_modulus_about_z_negative(self, strip_profile: StripStee expected_modulus_z_negative = 1 / 6 * 160**2 * 5 # mm³ assert pytest.approx(strip_profile.elastic_section_modulus_about_z_negative, rel=1e-6) == expected_modulus_z_negative - def test_plastic_section_modulus_about_y(self, strip_profile: StripSteelProfile) -> None: - """Test the plastic section modulus about the y-axis.""" - expected_plastic_modulus_y = 1 / 4 * 160 * 5**2 # mm³ - assert pytest.approx(strip_profile.plastic_section_modulus_about_y, rel=1e-6) == expected_plastic_modulus_y - - def test_plastic_section_modulus_about_z(self, strip_profile: StripSteelProfile) -> None: - """Test the plastic section modulus about the z-axis.""" - expected_plastic_modulus_z = 1 / 4 * 160**2 * 5 # mm³ - assert pytest.approx(strip_profile.plastic_section_modulus_about_z, rel=1e-6) == expected_plastic_modulus_z - def test_plot(self, strip_profile: StripSteelProfile) -> None: """Test the plot method (ensure it runs without errors).""" fig = strip_profile.plot(show=False) From 08b3647256905af0fa08385224373358408e0481 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 14:00:02 +0200 Subject: [PATCH 10/70] Refactor SteelElement class: update imports, remove unused properties, and add geometry property --- .../steel/steel_element.py | 36 ++++++++----------- .../steel/test_steel_element.py | 19 +--------- 2 files changed, 16 insertions(+), 39 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index bed055c8a..2b99f406e 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -2,10 +2,11 @@ from dataclasses import dataclass -from shapely import Point, Polygon +from sectionproperties.pre import Geometry +from shapely import Point from blueprints.materials.steel import SteelMaterial -from blueprints.structural_sections.general_cross_section import CrossSection +from blueprints.structural_sections._cross_section import CrossSection from blueprints.type_alias import KG_M, MM, MM2, MM3, MM4, MPA from blueprints.unit_conversion import MM2_TO_M2 @@ -21,16 +22,24 @@ class SteelElement: The cross-section of the steel element. material : SteelMaterial The material of the steel element. + nominal_thickness : MM + The nominal thickness of the steel element, default is 10.0 mm. """ cross_section: CrossSection material: SteelMaterial + nominal_thickness: MM = 10.0 # mm def __post_init__(self) -> None: """Check if the material is a SteelMaterial.""" if not isinstance(self.material, SteelMaterial): raise TypeError(f"Expected a SteelMaterial, but got: {type(self.material)}") + @property + def geometry(self) -> Geometry: + """Return the geometry of the steel element.""" + return self.cross_section.geometry + @property def name(self) -> str: """Name of the steel element.""" @@ -91,21 +100,6 @@ def plastic_section_modulus_about_z(self) -> MM3: """Plastic section modulus about the z-axis [mm³].""" return self.cross_section.plastic_section_modulus_about_z - @property - def geometry(self) -> Polygon: - """Return the geometry of the steel element.""" - return self.cross_section.geometry - - @property - def vertices(self) -> list[Point]: - """Return the vertices of the steel element.""" - return self.cross_section.vertices - - @property - def dotted_mesh(self) -> list: - """Return the dotted mesh of the steel element.""" - return self.cross_section.dotted_mesh(max_mesh_size=0) - @property def weight_per_meter(self) -> KG_M: """ @@ -119,7 +113,7 @@ def weight_per_meter(self) -> KG_M: return self.material.density * (self.cross_section.area * MM2_TO_M2) @property - def yield_strength(self) -> MPA: + def yield_strength(self) -> MPA | None: """ Calculate the yield strength of the steel element. @@ -128,10 +122,10 @@ def yield_strength(self) -> MPA: MPa The yield strength of the steel element. """ - return 0 + return self.material.yield_strength(thickness=self.nominal_thickness) @property - def ultimate_strength(self) -> MPA: + def ultimate_strength(self) -> MPA | None: """ Calculate the ultimate strength of the steel element. @@ -140,4 +134,4 @@ def ultimate_strength(self) -> MPA: MPa The ultimate strength of the steel element. """ - return 0 + return self.material.ultimate_strength(thickness=self.nominal_thickness) diff --git a/tests/structural_sections/steel/test_steel_element.py b/tests/structural_sections/steel/test_steel_element.py index 00313397f..384890f9a 100644 --- a/tests/structural_sections/steel/test_steel_element.py +++ b/tests/structural_sections/steel/test_steel_element.py @@ -6,7 +6,7 @@ from shapely.geometry import Point from blueprints.materials.steel import SteelMaterial -from blueprints.structural_sections.general_cross_section import CrossSection +from blueprints.structural_sections._cross_section import CrossSection from blueprints.structural_sections.steel.steel_element import SteelElement @@ -27,8 +27,6 @@ def mock_cross_section(mocker: Mock) -> Mock: cross_section.plastic_section_modulus_about_y = 60 # mm³ cross_section.plastic_section_modulus_about_z = 50 # mm³ cross_section.geometry = {"type": "rectangle", "width": 100, "height": 50} - cross_section.vertices = [Point(0, 0), Point(100, 0), Point(100, 50), Point(0, 50)] - cross_section.dotted_mesh.return_value = [(10, 10), (20, 20)] return cross_section @@ -108,21 +106,6 @@ def test_plastic_section_modulus_about_z(steel_element: SteelElement, mock_cross assert steel_element.plastic_section_modulus_about_z == mock_cross_section.plastic_section_modulus_about_z -def test_geometry(steel_element: SteelElement, mock_cross_section: Mock) -> None: - """Test that the SteelElement geometry matches the mock cross-section geometry.""" - assert steel_element.geometry == mock_cross_section.geometry - - -def test_vertices(steel_element: SteelElement, mock_cross_section: Mock) -> None: - """Test that the SteelElement vertices match the mock cross-section vertices.""" - assert steel_element.vertices == mock_cross_section.vertices - - -def test_dotted_mesh(steel_element: SteelElement, mock_cross_section: Mock) -> None: - """Test that the SteelElement dotted mesh matches the mock cross-section dotted mesh.""" - assert steel_element.dotted_mesh == mock_cross_section.dotted_mesh.return_value - - def test_weight_per_meter(steel_element: SteelElement, mock_cross_section: Mock, mock_material: Mock) -> None: """Test that the SteelElement weight per meter is calculated correctly.""" expected_weight: float = mock_material.density * (mock_cross_section.area * 1e-6) From 17b9851a9e990ab2b615e1f6c074351d46f0ec05 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 14:01:01 +0200 Subject: [PATCH 11/70] Fix yield_strength and ultimate_strength properties to raise ValueError for undefined material strengths --- .../structural_sections/steel/steel_element.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index 2b99f406e..b1d349286 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -113,7 +113,7 @@ def weight_per_meter(self) -> KG_M: return self.material.density * (self.cross_section.area * MM2_TO_M2) @property - def yield_strength(self) -> MPA | None: + def yield_strength(self) -> MPA: """ Calculate the yield strength of the steel element. @@ -122,10 +122,13 @@ def yield_strength(self) -> MPA | None: MPa The yield strength of the steel element. """ - return self.material.yield_strength(thickness=self.nominal_thickness) + fy = self.material.yield_strength(thickness=self.nominal_thickness) + if fy is None: + raise ValueError("Yield strength is not defined for this material.") + return fy @property - def ultimate_strength(self) -> MPA | None: + def ultimate_strength(self) -> MPA: """ Calculate the ultimate strength of the steel element. @@ -134,4 +137,7 @@ def ultimate_strength(self) -> MPA | None: MPa The ultimate strength of the steel element. """ - return self.material.ultimate_strength(thickness=self.nominal_thickness) + fu = self.material.ultimate_strength(thickness=self.nominal_thickness) + if fu is None: + raise ValueError("Ultimate strength is not defined for this material.") + return fu From c1f76a64c5ddd18d0784cc22c8677fa375e08537 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 14:22:24 +0200 Subject: [PATCH 12/70] Fix geometry method calls in SteelElement class and update expected string representations in CHSSteelProfile and StripSteelProfile tests --- blueprints/structural_sections/steel/steel_element.py | 2 +- .../steel/steel_cross_sections/test_chs_profile.py | 2 +- .../steel/steel_cross_sections/test_strip_profile.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index b1d349286..fd215b3ac 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -38,7 +38,7 @@ def __post_init__(self) -> None: @property def geometry(self) -> Geometry: """Return the geometry of the steel element.""" - return self.cross_section.geometry + return self.cross_section.geometry() @property def name(self) -> str: diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index 893edb329..9f7601291 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -16,7 +16,7 @@ def test_str(self) -> None: """Test the string representation of the CHS profile.""" profile: CHS = CHS.CHS508x16 steel_class: SteelStrengthClass = SteelStrengthClass.S355 - expected_str: str = "Steel class: SteelStrengthClass.EN_10025_2_S355, Profile: CHS.CHS_508x16" + expected_str: str = "Steel class: SteelStrengthClass.S355, Profile: CHS.CHS508x16" assert LoadStandardCHS(profile=profile, steel_class=steel_class).__str__() == expected_str def test_code(self) -> None: diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index ac5bd8b89..5a8b26d2d 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -16,7 +16,7 @@ def test_str(self) -> None: profile = Strip.STRIP160x5 steel_class = SteelStrengthClass.S355 desc = LoadStandardStrip(profile=profile, steel_class=steel_class).__str__() - expected_str = "Steel class: SteelStrengthClass.EN_10025_2_S355, Profile: StripStandardProfileClass.STRIP_160x5" + expected_str = "Steel class: SteelStrengthClass.S355, Profile: Strip.STRIP160x5" assert desc == expected_str def test_code(self) -> None: From d300845c78b45e189206d151b297c2b9ac5fcf50 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 14:44:25 +0200 Subject: [PATCH 13/70] Add polygon property to SteelElement and update geometry handling in SteelCrossSection for improved shape representation --- .../steel/steel_cross_sections/base.py | 33 +++++++++++++++++-- .../plotters/general_steel_plotter.py | 6 ++-- .../steel/steel_element.py | 7 +++- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/base.py b/blueprints/structural_sections/steel/steel_cross_sections/base.py index 6ad1a89a2..4b5243416 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/base.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/base.py @@ -2,11 +2,12 @@ from abc import ABC -from shapely.geometry import Point +from sectionproperties.pre import Geometry +from shapely.geometry import Point, Polygon from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_element import SteelElement -from blueprints.type_alias import KG_M, M3_M, MM2 +from blueprints.type_alias import KG_M, M3_M, MM, MM2 from blueprints.unit_conversion import MM3_TO_M3 @@ -27,6 +28,34 @@ def __init__( self.steel_material = steel_material # pragma: no cover self.elements: list[SteelElement] = [] # pragma: no cover + @property + def polygon(self) -> Polygon: + """Return the polygon of the steel cross-section.""" + combined_polygon = self.elements[0].polygon + for element in self.elements[1:]: + combined_polygon = combined_polygon.union(element.polygon) + return combined_polygon + + def geometry( + self, + mesh_size: MM | None = None, + ) -> Geometry: + """Return the geometry of the cross-section. + + Properties + ---------- + mesh_size : MM + Maximum mesh element area to be used within + the Geometry-object finite-element mesh. If not provided, a default value will be used. + + """ + if mesh_size is None: + mesh_size = 2.0 + + circular = Geometry(geom=self.polygon) + circular.create_mesh(mesh_sizes=mesh_size) + return circular + @property def steel_volume_per_meter(self) -> M3_M: """Total volume of the reinforced cross-section per meter length [m³/m].""" diff --git a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py index 74938bd6a..79b05f086 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py @@ -43,12 +43,12 @@ def plot_shapes( for element in profile.elements: # Plot the exterior polygon - x, y = element.geometry.exterior.xy + x, y = element.polygon.exterior.xy patch = MplPolygon(xy=list(zip(x, y)), lw=1, fill=True, facecolor=STEEL_COLOR, edgecolor=STEEL_COLOR) ax.add_patch(patch) # Plot the interior polygons (holes) if any - for interior in element.geometry.interiors: + for interior in element.polygon.interiors: x, y = interior.xy patch = MplPolygon(xy=list(zip(x, y)), lw=0, fill=True, facecolor="white") ax.add_patch(patch) @@ -103,7 +103,7 @@ def _add_dimension_lines(ax: plt.Axes, elements: list[SteelElement], centroid: P # Calculate the bounds of all elements in the geometry min_x, min_y, max_x, max_y = float("inf"), float("inf"), float("-inf"), float("-inf") for element in elements: - bounds = element.geometry.bounds + bounds = element.polygon.bounds min_x = min(min_x, bounds[0]) min_y = min(min_y, bounds[1]) max_x = max(max_x, bounds[2]) diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index fd215b3ac..756544882 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from sectionproperties.pre import Geometry -from shapely import Point +from shapely import Point, Polygon from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections._cross_section import CrossSection @@ -40,6 +40,11 @@ def geometry(self) -> Geometry: """Return the geometry of the steel element.""" return self.cross_section.geometry() + @property + def polygon(self) -> Polygon: + """Return the polygon of the steel element.""" + return self.cross_section.polygon + @property def name(self) -> str: """Name of the steel element.""" From 5c9d9fc2bc5f2d6008d2ec1ff4917457f40fe720 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 14:59:54 +0200 Subject: [PATCH 14/70] Add tests for handling invalid yield and ultimate strength in SteelElement --- .../steel/test_steel_element.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/structural_sections/steel/test_steel_element.py b/tests/structural_sections/steel/test_steel_element.py index 384890f9a..7e40b2729 100644 --- a/tests/structural_sections/steel/test_steel_element.py +++ b/tests/structural_sections/steel/test_steel_element.py @@ -126,3 +126,17 @@ def test_invalid_material_type(mock_cross_section: Mock) -> None: """Test that creating a SteelElement with an invalid material type raises a TypeError.""" with pytest.raises(TypeError): SteelElement(cross_section=mock_cross_section, material=mock_material) + + +def test_invalid_yield_strength(steel_element: SteelElement, mock_material: Mock) -> None: + """Test that accessing yield strength raises a ValueError if not defined.""" + mock_material.yield_strength.return_value = None + with pytest.raises(ValueError, match="Yield strength is not defined for this material."): + _ = steel_element.yield_strength + + +def test_invalid_ultimate_strength(steel_element: SteelElement, mock_material: Mock) -> None: + """Test that accessing ultimate strength raises a ValueError if not defined.""" + mock_material.ultimate_strength.return_value = None + with pytest.raises(ValueError, match="Ultimate strength is not defined for this material."): + _ = steel_element.ultimate_strength From a7504286eb53b70a09f2bf5675589b1960b28891 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 15:07:49 +0200 Subject: [PATCH 15/70] Implement SteelCrossSection class with geometry and section properties --- .../{base.py => _steel_cross_section.py} | 33 +++++ .../steel/steel_cross_sections/test_base.py | 131 ------------------ 2 files changed, 33 insertions(+), 131 deletions(-) rename blueprints/structural_sections/steel/steel_cross_sections/{base.py => _steel_cross_section.py} (82%) delete mode 100644 tests/structural_sections/steel/steel_cross_sections/test_base.py diff --git a/blueprints/structural_sections/steel/steel_cross_sections/base.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py similarity index 82% rename from blueprints/structural_sections/steel/steel_cross_sections/base.py rename to blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index 4b5243416..cb1fd09f3 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/base.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -2,6 +2,8 @@ from abc import ABC +from sectionproperties.analysis import Section +from sectionproperties.post.post import SectionProperties from sectionproperties.pre import Geometry from shapely.geometry import Point, Polygon @@ -118,3 +120,34 @@ def elastic_section_modulus_about_z_negative(self) -> KG_M: """Elastic section modulus about the z-axis on the negative y side [mm³].""" distance_to_left = self.centroid.x - min(x for element in self.elements for x, _ in element.geometry.points) return self.moment_of_inertia_about_z / distance_to_left + + def section(self) -> Section: + """Section object representing the cross-section.""" + return Section(geometry=self.geometry()) + + def section_properties( + self, + geometric: bool = True, + plastic: bool = True, + warping: bool = True, + ) -> SectionProperties: + """Calculate and return the section properties of the cross-section. + + Parameters + ---------- + geometric : bool + Whether to calculate geometric properties. + plastic: bool + Whether to calculate plastic properties. + warping: bool + Whether to calculate warping properties. + """ + section = self.section() + + if any([geometric, plastic, warping]): + section.calculate_geometric_properties() + if warping: + section.calculate_warping_properties() + if plastic: + section.calculate_plastic_properties() + return section.section_props diff --git a/tests/structural_sections/steel/steel_cross_sections/test_base.py b/tests/structural_sections/steel/steel_cross_sections/test_base.py deleted file mode 100644 index 4f0060309..000000000 --- a/tests/structural_sections/steel/steel_cross_sections/test_base.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Test the SteelCrossSection class.""" - -from unittest.mock import Mock - -import pytest -from shapely.geometry import Point - -from blueprints.materials.steel import SteelMaterial -from blueprints.structural_sections.steel.steel_cross_sections.base import SteelCrossSection -from blueprints.structural_sections.steel.steel_element import SteelElement - - -@pytest.fixture -def mock_steel_material(mocker: Mock) -> Mock: - """Mock a SteelMaterial object.""" - material: Mock = mocker.Mock(spec=SteelMaterial) - material.density = 7850 # kg/m³ - return material - - -@pytest.fixture -def mock_steel_element(mocker: Mock) -> Mock: - """Mock a SteelElement object.""" - element: Mock = mocker.Mock(spec=SteelElement) - element.area = 500 # mm² - element.centroid = Point(50, 50) - element.moment_of_inertia_about_y = 2000 # mm⁴ - element.moment_of_inertia_about_z = 3000 # mm⁴ - element.yield_strength = 250 # MPa - element.cross_section = Mock() - element.cross_section.vertices = [Point(0, 0), Point(100, 0), Point(100, 50), Point(0, 50)] - element.cross_section.dotted_mesh = Mock(return_value=[Point(10, 10), Point(20, 20)]) - element.cross_section.area = 500 # mm² - element.cross_section.centroid = Point(50, 50) - element.cross_section.moment_of_inertia_about_y = 2000 # mm⁴ - element.cross_section.moment_of_inertia_about_z = 3000 # mm⁴ - element.cross_section.elastic_section_modulus_about_y_positive = 100 # mm³ - element.cross_section.elastic_section_modulus_about_y_negative = 90 # mm³ - element.cross_section.elastic_section_modulus_about_z_positive = 80 # mm³ - element.cross_section.elastic_section_modulus_about_z_negative = 70 # mm³ - element.cross_section.plastic_section_modulus_about_y = 60 # mm³ - element.cross_section.plastic_section_modulus_about_z = 50 # mm³ - element.cross_section.geometry = {"type": "rectangle", "width": 100, "height": 50} - element.cross_section.name = "MockSection" - return element - - -@pytest.fixture -def steel_cross_section(mock_steel_material: Mock, mock_steel_element: Mock) -> SteelCrossSection: - """Create a SteelCrossSection instance with mocked elements.""" - cross_section = Mock(spec=SteelCrossSection) - cross_section.steel_material = mock_steel_material - cross_section.elements = [mock_steel_element, mock_steel_element] - cross_section.steel_volume_per_meter = 0.001 - cross_section.steel_weight_per_meter = 7.85 - cross_section.steel_area = 1000 - cross_section.centroid = Point(50, 50) - cross_section.moment_of_inertia_about_y = 4000 - cross_section.moment_of_inertia_about_z = 6000 - cross_section.elastic_section_modulus_about_y_positive = 80 - cross_section.elastic_section_modulus_about_y_negative = 80 - cross_section.elastic_section_modulus_about_z_positive = 120 - cross_section.elastic_section_modulus_about_z_negative = 120 - cross_section.plastic_section_modulus_about_y = 100 - cross_section.plastic_section_modulus_about_z = 150 - cross_section.vertices = [[Point(0, 0), Point(100, 0), Point(100, 50), Point(0, 50)], [Point(0, 0), Point(100, 0), Point(100, 50), Point(0, 50)]] - return cross_section - - -def test_steel_volume_per_meter(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: - """Test the steel volume per meter calculation.""" - expected_volume = 2 * mock_steel_element.area * 1000 * 1e-9 # Convert mm³ to m³ - assert steel_cross_section.steel_volume_per_meter == expected_volume - - -def test_steel_weight_per_meter(steel_cross_section: SteelCrossSection, mock_steel_material: Mock, mock_steel_element: Mock) -> None: - """Test the steel weight per meter calculation.""" - expected_weight = mock_steel_material.density * 2 * mock_steel_element.area * 1000 * 1e-9 - assert steel_cross_section.steel_weight_per_meter == pytest.approx(expected_weight) - - -def test_steel_area(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: - """Test the total steel area calculation.""" - expected_area = 2 * mock_steel_element.area - assert steel_cross_section.steel_area == expected_area - - -def test_centroid(steel_cross_section: SteelCrossSection) -> None: - """Test the centroid calculation.""" - expected_centroid = Point(50, 50) - assert steel_cross_section.centroid == expected_centroid - - -def test_moment_of_inertia_about_y(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: - """Test the moment of inertia about the y-axis calculation.""" - expected_moi_y = 2 * mock_steel_element.moment_of_inertia_about_y - assert steel_cross_section.moment_of_inertia_about_y == expected_moi_y - - -def test_moment_of_inertia_about_z(steel_cross_section: SteelCrossSection, mock_steel_element: Mock) -> None: - """Test the moment of inertia about the z-axis calculation.""" - expected_moi_z = 2 * mock_steel_element.moment_of_inertia_about_z - assert steel_cross_section.moment_of_inertia_about_z == expected_moi_z - - -def test_elastic_section_modulus_about_y_positive(steel_cross_section: SteelCrossSection) -> None: - """Test the elastic section modulus about the y-axis on the positive z side.""" - distance_to_top = 50 # Distance from centroid to top - expected_modulus = steel_cross_section.moment_of_inertia_about_y / distance_to_top - assert steel_cross_section.elastic_section_modulus_about_y_positive == expected_modulus - - -def test_elastic_section_modulus_about_y_negative(steel_cross_section: SteelCrossSection) -> None: - """Test the elastic section modulus about the y-axis on the negative z side.""" - distance_to_bottom = 50 # Distance from centroid to bottom - expected_modulus = steel_cross_section.moment_of_inertia_about_y / distance_to_bottom - assert steel_cross_section.elastic_section_modulus_about_y_negative == expected_modulus - - -def test_elastic_section_modulus_about_z_positive(steel_cross_section: SteelCrossSection) -> None: - """Test the elastic section modulus about the z-axis on the positive y side.""" - distance_to_right = 50 # Distance from centroid to right - expected_modulus = steel_cross_section.moment_of_inertia_about_z / distance_to_right - assert steel_cross_section.elastic_section_modulus_about_z_positive == expected_modulus - - -def test_elastic_section_modulus_about_z_negative(steel_cross_section: SteelCrossSection) -> None: - """Test the elastic section modulus about the z-axis on the negative y side.""" - distance_to_left = 50 # Distance from centroid to left - expected_modulus = steel_cross_section.moment_of_inertia_about_z / distance_to_left - assert steel_cross_section.elastic_section_modulus_about_z_negative == expected_modulus From 5cf5339e4d697d5d090a6c6b7b6ebbd87da14d2d Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 15:08:08 +0200 Subject: [PATCH 16/70] Refactor import statements for SteelCrossSection to use the correct module path and add geometry tests for CHSSteelProfile and StripSteelProfile. --- .../steel/steel_cross_sections/chs_profile.py | 2 +- .../steel_cross_sections/plotters/general_steel_plotter.py | 2 +- .../steel/steel_cross_sections/strip_profile.py | 2 +- .../steel/steel_cross_sections/test_chs_profile.py | 5 +++++ .../steel/steel_cross_sections/test_strip_profile.py | 5 +++++ 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py index 004d1c4e5..1afdb29bc 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py @@ -5,7 +5,7 @@ from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.cross_section_tube import TubeCrossSection -from blueprints.structural_sections.steel.steel_cross_sections.base import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import SteelCrossSection from blueprints.structural_sections.steel.steel_cross_sections.plotters.general_steel_plotter import plot_shapes from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS from blueprints.structural_sections.steel.steel_element import SteelElement diff --git a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py index 79b05f086..f80608121 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py @@ -6,7 +6,7 @@ from matplotlib.patches import Polygon as MplPolygon from shapely.geometry import Point -from blueprints.structural_sections.steel.steel_cross_sections.base import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import SteelCrossSection from blueprints.structural_sections.steel.steel_element import SteelElement # Define color diff --git a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py index 8756abcde..620a63851 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py @@ -5,7 +5,7 @@ from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.cross_section_rectangle import RectangularCrossSection -from blueprints.structural_sections.steel.steel_cross_sections.base import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import SteelCrossSection from blueprints.structural_sections.steel.steel_cross_sections.plotters.general_steel_plotter import plot_shapes from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip from blueprints.structural_sections.steel.steel_element import SteelElement diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index 9f7601291..a90578b63 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -82,3 +82,8 @@ def test_plot(self, chs_profile: CHSSteelProfile) -> None: """Test the plot method (ensure it runs without errors).""" fig: Figure = chs_profile.plot() assert isinstance(fig, plt.Figure) + + def test_geometry(self, chs_profile: CHSSteelProfile) -> None: + """Test the geometry of the Strip profile.""" + expected_geometry = chs_profile.geometry + assert expected_geometry is not None diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index 5a8b26d2d..7b53438af 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -82,3 +82,8 @@ def test_plot(self, strip_profile: StripSteelProfile) -> None: """Test the plot method (ensure it runs without errors).""" fig = strip_profile.plot(show=False) assert isinstance(fig, plt.Figure) + + def test_geometry(self, strip_profile: StripSteelProfile) -> None: + """Test the geometry of the Strip profile.""" + expected_geometry = strip_profile.geometry + assert expected_geometry is not None From 3a2e38b5753f03b05c535309beaf2b787f149791 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 21 Apr 2025 16:47:48 +0200 Subject: [PATCH 17/70] Refactor SteelCrossSection and related profiles to unify area property naming and enhance geometry handling; update tests accordingly. --- .../_steel_cross_section.py | 48 +++++++++---------- .../steel/steel_cross_sections/chs_profile.py | 2 +- .../plotters/general_steel_plotter.py | 2 +- .../steel_cross_sections/strip_profile.py | 4 +- .../steel/steel_element.py | 4 +- docs/examples/_code/steel_profile_shapes.py | 4 +- .../steel_cross_sections/test_chs_profile.py | 17 ++++++- .../test_strip_profile.py | 17 ++++++- 8 files changed, 63 insertions(+), 35 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index cb1fd09f3..42333ec18 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -38,26 +38,6 @@ def polygon(self) -> Polygon: combined_polygon = combined_polygon.union(element.polygon) return combined_polygon - def geometry( - self, - mesh_size: MM | None = None, - ) -> Geometry: - """Return the geometry of the cross-section. - - Properties - ---------- - mesh_size : MM - Maximum mesh element area to be used within - the Geometry-object finite-element mesh. If not provided, a default value will be used. - - """ - if mesh_size is None: - mesh_size = 2.0 - - circular = Geometry(geom=self.polygon) - circular.create_mesh(mesh_sizes=mesh_size) - return circular - @property def steel_volume_per_meter(self) -> M3_M: """Total volume of the reinforced cross-section per meter length [m³/m].""" @@ -70,7 +50,7 @@ def steel_weight_per_meter(self) -> KG_M: return self.steel_material.density * self.steel_volume_per_meter @property - def steel_area(self) -> MM2: + def area(self) -> MM2: """Total cross sectional area of the steel element [mm²].""" return sum(element.area for element in self.elements) @@ -79,8 +59,8 @@ def centroid(self) -> Point: """Centroid of the steel cross-section.""" area_weighted_centroids_x = sum(element.centroid.x * element.area for element in self.elements) area_weighted_centroids_y = sum(element.centroid.y * element.area for element in self.elements) - centroid_x = area_weighted_centroids_x / self.steel_area - centroid_y = area_weighted_centroids_y / self.steel_area + centroid_x = area_weighted_centroids_x / self.area + centroid_y = area_weighted_centroids_y / self.area return Point(centroid_x, centroid_y) @property @@ -121,9 +101,29 @@ def elastic_section_modulus_about_z_negative(self) -> KG_M: distance_to_left = self.centroid.x - min(x for element in self.elements for x, _ in element.geometry.points) return self.moment_of_inertia_about_z / distance_to_left + def geometry( + self, + mesh_size: MM | None = None, + ) -> Geometry: + """Return the geometry of the cross-section. + + Properties + ---------- + mesh_size : MM + Maximum mesh element area to be used within + the Geometry-object finite-element mesh. If not provided, a default value will be used. + + """ + if mesh_size is None: + mesh_size = 2.0 + + geom = Geometry(geom=self.polygon) + geom.create_mesh(mesh_sizes=mesh_size) + return geom + def section(self) -> Section: """Section object representing the cross-section.""" - return Section(geometry=self.geometry()) + return Section(geometry=self.geometry) def section_properties( self, diff --git a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py index 1afdb29bc..ccac1fc87 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py @@ -44,7 +44,7 @@ def __init__( y=0, ) self.steel_material = SteelMaterial(steel_class=steel_class) - self.elements = [SteelElement(cross_section=self.chs, material=self.steel_material)] + self.elements = [SteelElement(cross_section=self.chs, material=self.steel_material, nominal_thickness=self.thickness)] def plot(self, *args, **kwargs) -> plt.Figure: """Plot the cross-section. Making use of the standard plotter. diff --git a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py index f80608121..d29cf365b 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py @@ -58,7 +58,7 @@ def plot_shapes( ax.plot(profile.centroid.x, profile.centroid.y, "o", color="black") # Add legend text - legend_text = f"Total area: {profile.steel_area:.1f} mm²\n" + legend_text = f"Total area: {profile.area:.1f} mm²\n" legend_text += f"Weight per meter: {profile.steel_weight_per_meter:.1f} kg/m\n" legend_text += f"Moment of inertia about y: {profile.moment_of_inertia_about_y:.0f} mm⁴\n" legend_text += f"Moment of inertia about z: {profile.moment_of_inertia_about_z:.0f} mm⁴\n" diff --git a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py index 620a63851..d8307bd01 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py @@ -34,6 +34,7 @@ def __init__( """Initialize the Steel Strip profile.""" self.width = width self.height = height + self.thickness = min(width, height) # Nominal thickness is the minimum of width and height self.strip = RectangularCrossSection( name="Steel Strip", @@ -44,7 +45,8 @@ def __init__( ) self.steel_material = SteelMaterial(steel_class=steel_class) - self.elements = [SteelElement(cross_section=self.strip, material=self.steel_material)] + + self.elements = [SteelElement(cross_section=self.strip, material=self.steel_material, nominal_thickness=self.thickness)] def plot(self, *args, **kwargs) -> plt.Figure: """Plot the cross-section. Making use of the standard plotter. diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index 756544882..88e0ee36a 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -23,12 +23,12 @@ class SteelElement: material : SteelMaterial The material of the steel element. nominal_thickness : MM - The nominal thickness of the steel element, default is 10.0 mm. + The nominal thickness of the steel element """ cross_section: CrossSection material: SteelMaterial - nominal_thickness: MM = 10.0 # mm + nominal_thickness: MM def __post_init__(self) -> None: """Check if the material is a SteelMaterial.""" diff --git a/docs/examples/_code/steel_profile_shapes.py b/docs/examples/_code/steel_profile_shapes.py index 9d0421530..d0a898ea4 100644 --- a/docs/examples/_code/steel_profile_shapes.py +++ b/docs/examples/_code/steel_profile_shapes.py @@ -21,7 +21,7 @@ print(f"Moment of inertia about z-axis: {chs_profile.moment_of_inertia_about_z} mm⁴") print(f"Elastic section modulus about y-axis: {chs_profile.elastic_section_modulus_about_y_negative} mm³") print(f"Elastic section modulus about z-axis: {chs_profile.elastic_section_modulus_about_z_positive} mm³") -print(f"Area: {chs_profile.steel_area} mm²") +print(f"Area: {chs_profile.area} mm²") # Example usage for custom CHS profile custom_chs_profile = CHSSteelProfile(outer_diameter=150, wall_thickness=10, steel_class=steel_class) @@ -34,7 +34,7 @@ # Example usage for custom Strip profile custom_strip_profile = StripSteelProfile( width=100, - height=30, + height=41, steel_class=steel_class, ) custom_strip_profile.plot(show=True) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index a90578b63..a3e5c69dd 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -37,10 +37,10 @@ def test_steel_weight_per_meter(self, chs_profile: CHSSteelProfile) -> None: expected_weight: float = 2.47e-2 * 7850 # kg/m assert pytest.approx(chs_profile.steel_weight_per_meter, rel=1e-2) == expected_weight - def test_steel_area(self, chs_profile: CHSSteelProfile) -> None: + def test_area(self, chs_profile: CHSSteelProfile) -> None: """Test the steel cross-sectional area.""" expected_area: float = 2.47e4 # mm² - assert pytest.approx(chs_profile.steel_area, rel=1e-2) == expected_area + assert pytest.approx(chs_profile.area, rel=1e-2) == expected_area def test_centroid(self, chs_profile: CHSSteelProfile) -> None: """Test the centroid of the steel cross-section.""" @@ -87,3 +87,16 @@ def test_geometry(self, chs_profile: CHSSteelProfile) -> None: """Test the geometry of the Strip profile.""" expected_geometry = chs_profile.geometry assert expected_geometry is not None + + def test_section_properties(self, chs_profile: CHSSteelProfile) -> None: + """Test the section properties of the Strip profile.""" + section_properties = chs_profile.section_properties() + assert section_properties.mass == pytest.approx(expected=chs_profile.area, rel=1e-2) + assert section_properties.cx == pytest.approx(expected=chs_profile.centroid.x, rel=1e-2) + assert section_properties.cy == pytest.approx(expected=chs_profile.centroid.y, rel=1e-2) + assert section_properties.ixx_c == pytest.approx(expected=chs_profile.moment_of_inertia_about_y, rel=1e-2) + assert section_properties.iyy_c == pytest.approx(expected=chs_profile.moment_of_inertia_about_z, rel=1e-2) + assert section_properties.zxx_plus == pytest.approx(expected=chs_profile.elastic_section_modulus_about_y_positive, rel=1e-2) + assert section_properties.zyy_plus == pytest.approx(expected=chs_profile.elastic_section_modulus_about_z_positive, rel=1e-2) + assert section_properties.zxx_minus == pytest.approx(expected=chs_profile.elastic_section_modulus_about_y_negative, rel=1e-2) + assert section_properties.zyy_minus == pytest.approx(expected=chs_profile.elastic_section_modulus_about_z_negative, rel=1e-2) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index 7b53438af..badab3f66 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -37,10 +37,10 @@ def test_steel_weight_per_meter(self, strip_profile: StripSteelProfile) -> None: expected_weight = 0.160 * 0.005 * 7850 # kg/m assert pytest.approx(strip_profile.steel_weight_per_meter, rel=1e-6) == expected_weight - def test_steel_area(self, strip_profile: StripSteelProfile) -> None: + def test_area(self, strip_profile: StripSteelProfile) -> None: """Test the steel cross-sectional area.""" expected_area = 160 * 5 # mm² - assert pytest.approx(strip_profile.steel_area, rel=1e-6) == expected_area + assert pytest.approx(strip_profile.area, rel=1e-6) == expected_area def test_centroid(self, strip_profile: StripSteelProfile) -> None: """Test the centroid of the steel cross-section.""" @@ -87,3 +87,16 @@ def test_geometry(self, strip_profile: StripSteelProfile) -> None: """Test the geometry of the Strip profile.""" expected_geometry = strip_profile.geometry assert expected_geometry is not None + + def test_section_properties(self, strip_profile: StripSteelProfile) -> None: + """Test the section properties of the Strip profile.""" + section_properties = strip_profile.section_properties() + assert section_properties.mass == pytest.approx(expected=strip_profile.area, rel=1e-2) + assert section_properties.cx == pytest.approx(expected=strip_profile.centroid.x, rel=1e-2) + assert section_properties.cy == pytest.approx(expected=strip_profile.centroid.y, rel=1e-2) + assert section_properties.ixx_c == pytest.approx(expected=strip_profile.moment_of_inertia_about_y, rel=1e-2) + assert section_properties.iyy_c == pytest.approx(expected=strip_profile.moment_of_inertia_about_z, rel=1e-2) + assert section_properties.zxx_plus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_y_positive, rel=1e-2) + assert section_properties.zyy_plus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_z_positive, rel=1e-2) + assert section_properties.zxx_minus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_y_negative, rel=1e-2) + assert section_properties.zyy_minus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_z_negative, rel=1e-2) From 198888d7c79e5de54302e735eff3f5350210ece1 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 19:06:08 +0200 Subject: [PATCH 18/70] Fix test_centroid to use correct expected value for QuarterCircularSpandrelCrossSection --- .../test_cross_section_quarter_circular_spandrel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/structural_sections/test_cross_section_quarter_circular_spandrel.py b/tests/structural_sections/test_cross_section_quarter_circular_spandrel.py index 7b0b154c8..f9d4eb384 100644 --- a/tests/structural_sections/test_cross_section_quarter_circular_spandrel.py +++ b/tests/structural_sections/test_cross_section_quarter_circular_spandrel.py @@ -22,8 +22,8 @@ def test_perimeter(self, qcs_cross_section: QuarterCircularSpandrelCrossSection) def test_centroid(self, qcs_cross_section: QuarterCircularSpandrelCrossSection) -> None: """Test the centroid property of the QuarterCircularSpandrelCrossSection class.""" - expected_perimeter = Point(111.1683969472876, 261.1683969472876) - assert qcs_cross_section.centroid == pytest.approx(expected=expected_perimeter, rel=1e-6) + expected_centroid = Point(111.1683969472876, 261.1683969472876) + assert qcs_cross_section.centroid == pytest.approx(expected=expected_centroid, rel=1e-6) def test_moments_of_inertia(self, qcs_cross_section: QuarterCircularSpandrelCrossSection) -> None: """Test the moments of inertia properties of the QuarterCircularSpandrelCrossSection class.""" From badce9a76214474dbc6b3b06ab59870c5c1d831a Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 19:17:28 +0200 Subject: [PATCH 19/70] Add corrosion adjustment parameters to get_profile methods for CHS and Strip profiles --- .../steel/steel_cross_sections/chs_profile.py | 29 +++++++++++++++++-- .../steel_cross_sections/strip_profile.py | 14 +++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py index ccac1fc87..938efd0ec 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py @@ -98,6 +98,29 @@ def thickness(self) -> MM: """Return the wall thickness of the CHS profile.""" return self.profile.thickness - def get_profile(self) -> CHSSteelProfile: - """Return the CHS profile.""" - return CHSSteelProfile(outer_diameter=self.diameter(), wall_thickness=self.thickness(), steel_class=self.steel_class) + def get_profile(self, corrosion_outside: MM = 0, corrosion_inside: MM = 0) -> CHSSteelProfile: + """Return the CHS profile with optional corrosion adjustments. + + Parameters + ---------- + corrosion_outside : MM, optional + Corrosion thickness to be subtracted from the outer diameter [mm] (default: 0). + corrosion_inside : MM, optional + Corrosion thickness to be added to the inner diameter [mm] (default: 0). + + Returns + ------- + CHSSteelProfile + The adjusted CHS steel profile considering corrosion effects. + """ + adjusted_outer_diameter = self.diameter() - 2 * corrosion_outside + adjusted_thickness = self.thickness() - corrosion_outside - corrosion_inside + + if adjusted_thickness <= 0: + raise ValueError("Adjusted wall thickness must be greater than zero.") + + return CHSSteelProfile( + outer_diameter=adjusted_outer_diameter, + wall_thickness=adjusted_thickness, + steel_class=self.steel_class, + ) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py index d8307bd01..dc686699a 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py @@ -100,6 +100,14 @@ def height(self) -> MM: """Return the height (thickness) of the strip profile.""" return self.profile.height - def get_profile(self) -> StripSteelProfile: - """Return the strip profile.""" - return StripSteelProfile(width=self.width(), height=self.height(), steel_class=self.steel_class) + def get_profile(self, corrosion: MM = 0) -> StripSteelProfile: + """Return the strip profile. + + Parameters + ---------- + corrosion : MM, optional + Corrosion thickness per side (default is 0). + """ + width = self.width() - corrosion * 2 + height = self.height() - corrosion * 2 + return StripSteelProfile(width=width, height=height, steel_class=self.steel_class) From 22dd2d883d0ae65d5d70f7bb093b09a0d795f4fd Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 19:26:36 +0200 Subject: [PATCH 20/70] Fix section method to call geometry() and update SteelElement instantiation with nominal thickness --- .../steel/steel_cross_sections/_steel_cross_section.py | 2 +- tests/structural_sections/steel/test_steel_element.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index 42333ec18..76369ac42 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -123,7 +123,7 @@ def geometry( def section(self) -> Section: """Section object representing the cross-section.""" - return Section(geometry=self.geometry) + return Section(geometry=self.geometry()) def section_properties( self, diff --git a/tests/structural_sections/steel/test_steel_element.py b/tests/structural_sections/steel/test_steel_element.py index 7e40b2729..1062e9ba3 100644 --- a/tests/structural_sections/steel/test_steel_element.py +++ b/tests/structural_sections/steel/test_steel_element.py @@ -43,7 +43,7 @@ def mock_material(mocker: Mock) -> Mock: @pytest.fixture def steel_element(mock_cross_section: Mock, mock_material: Mock) -> SteelElement: """Create a SteelElement instance using mocked cross-section and material.""" - return SteelElement(cross_section=mock_cross_section, material=mock_material) + return SteelElement(cross_section=mock_cross_section, material=mock_material, nominal_thickness=10) def test_name(steel_element: SteelElement, mock_cross_section: Mock) -> None: @@ -125,7 +125,7 @@ def test_ultimate_strength(steel_element: SteelElement, mock_material: Mock) -> def test_invalid_material_type(mock_cross_section: Mock) -> None: """Test that creating a SteelElement with an invalid material type raises a TypeError.""" with pytest.raises(TypeError): - SteelElement(cross_section=mock_cross_section, material=mock_material) + SteelElement(cross_section=mock_cross_section, material=mock_material, nominal_thickness=10) def test_invalid_yield_strength(steel_element: SteelElement, mock_material: Mock) -> None: From b9a5305dd398da82c8f5ce2ed5f7f275c46d2d48 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 20:10:30 +0200 Subject: [PATCH 21/70] Add I-Profile steel section implementation and corresponding test suite --- .../steel/steel_cross_sections/i_profile.py | 240 ++++++++++++++++++ .../steel/steel_cross_sections/conftest.py | 10 + .../steel_cross_sections/test_i_profile.py | 101 ++++++++ 3 files changed, 351 insertions(+) create mode 100644 blueprints/structural_sections/steel/steel_cross_sections/i_profile.py create mode 100644 tests/structural_sections/steel/steel_cross_sections/test_i_profile.py diff --git a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py new file mode 100644 index 000000000..d22946333 --- /dev/null +++ b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py @@ -0,0 +1,240 @@ +"""I-Profile steel section.""" + +from matplotlib import pyplot as plt + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.cross_section_quarter_circular_spandrel import QuarterCircularSpandrelCrossSection +from blueprints.structural_sections.cross_section_rectangle import RectangularCrossSection +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections.plotters.general_steel_plotter import plot_shapes +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.hea import HEA +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.hem import HEM +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.ipe import IPE +from blueprints.structural_sections.steel.steel_element import SteelElement +from blueprints.type_alias import MM + + +class ISteelProfile(SteelCrossSection): + """Representation of an I-Profile steel section. + + Parameters + ---------- + top_flange_width : MM + The width of the top flange [mm]. + top_flange_thickness : MM + The thickness of the top flange [mm]. + bottom_flange_width : MM + The width of the bottom flange [mm]. + bottom_flange_thickness : MM + The thickness of the bottom flange [mm]. + total_height : MM + The total height of the profile [mm]. + web_thickness : MM + The thickness of the web [mm]. + steel_class : SteelStrengthClass + The steel strength class of the profile. + top_radius : MM | None + The radius of the curved corners of the top flange. Default is None, the corner radius is then taken as the thickness. + bottom_radius : MM | None + The radius of the curved corners of the bottom flange. Default is None, the corner radius is then taken as the thickness. + """ + + def __init__( + self, + top_flange_width: MM, + top_flange_thickness: MM, + bottom_flange_width: MM, + bottom_flange_thickness: MM, + total_height: MM, + web_thickness: MM, + steel_class: SteelStrengthClass, + top_radius: MM | None = None, + bottom_radius: MM | None = None, + ) -> None: + """Initialize the I-profile steel section.""" + self.top_flange_width = top_flange_width + self.top_flange_thickness = top_flange_thickness + self.bottom_flange_width = bottom_flange_width + self.bottom_flange_thickness = bottom_flange_thickness + self.total_height = total_height + self.web_thickness = web_thickness + self.top_radius = top_radius if top_radius is not None else top_flange_thickness + self.bottom_radius = bottom_radius if bottom_radius is not None else bottom_flange_thickness + + # Calculate web height + self.web_height = total_height - top_flange_thickness - bottom_flange_thickness + + # Create the cross-sections for the flanges and web + self.top_flange = RectangularCrossSection( + name="Top Flange", + width=top_flange_width, + height=top_flange_thickness, + x=0, + y=(self.web_height + top_flange_thickness) / 2, + ) + self.bottom_flange = RectangularCrossSection( + name="Bottom Flange", + width=bottom_flange_width, + height=bottom_flange_thickness, + x=0, + y=-(self.web_height + bottom_flange_thickness) / 2, + ) + self.web = RectangularCrossSection( + name="Web", + width=web_thickness, + height=self.web_height, + x=0, + y=0, + ) + + # Create curves for the corners of the flanges + self.curve_top_right = QuarterCircularSpandrelCrossSection( + name="Curve top right", + radius=self.top_radius, + x=web_thickness / 2, + y=self.web_height / 2, + mirrored_horizontally=False, + mirrored_vertically=True, + ) + self.curve_top_left = QuarterCircularSpandrelCrossSection( + name="Curve top left", + radius=self.top_radius, + x=-web_thickness / 2, + y=self.web_height / 2, + mirrored_horizontally=True, + mirrored_vertically=True, + ) + self.curve_bottom_right = QuarterCircularSpandrelCrossSection( + name="Curve bottom right", + radius=self.bottom_radius, + x=web_thickness / 2, + y=-self.web_height / 2, + mirrored_horizontally=False, + mirrored_vertically=False, + ) + self.curve_bottom_left = QuarterCircularSpandrelCrossSection( + name="Curve bottom left", + radius=self.bottom_radius, + x=-web_thickness / 2, + y=-self.web_height / 2, + mirrored_horizontally=True, + mirrored_vertically=False, + ) + + # material properties + self.steel_material = SteelMaterial(steel_class=steel_class) + + # Create the steel elements + self.elements = [ + SteelElement(cross_section=self.top_flange, material=self.steel_material, nominal_thickness=top_flange_thickness), + SteelElement(cross_section=self.bottom_flange, material=self.steel_material, nominal_thickness=bottom_flange_thickness), + SteelElement(cross_section=self.web, material=self.steel_material, nominal_thickness=web_thickness), + SteelElement(cross_section=self.curve_top_right, material=self.steel_material, nominal_thickness=top_flange_thickness), + SteelElement(cross_section=self.curve_top_left, material=self.steel_material, nominal_thickness=top_flange_thickness), + SteelElement(cross_section=self.curve_bottom_right, material=self.steel_material, nominal_thickness=bottom_flange_thickness), + SteelElement(cross_section=self.curve_bottom_left, material=self.steel_material, nominal_thickness=bottom_flange_thickness), + ] + + def plot(self, *args, **kwargs) -> plt.Figure: + """Plot the cross-section. Making use of the standard plotter. + + Parameters + ---------- + *args + Additional arguments passed to the plotter. + **kwargs + Additional keyword arguments passed to the plotter. + """ + return plot_shapes( + self, + *args, + **kwargs, + ) + + +class LoadStandardIProfile: + r"""Class to load in values for standard I profile. + + Parameters + ---------- + profile: ISteelProfile + Representation of an I-Profile steel section + steel_class: SteelStrengthClass + Enumeration of steel strength classes (default: S355) + """ + + def __init__( + self, + profile: HEA | HEB | HEM | IPE, + steel_class: SteelStrengthClass = SteelStrengthClass.S355, + ) -> None: + self.profile = profile + self.steel_class = steel_class + + def __str__(self) -> str: + """Return the steel class and profile.""" + return f"Steel class: {self.steel_class}, Profile: {self.profile}" + + def top_flange_width(self) -> MM: + """Return the top flange width of the I-profile.""" + return self.profile.top_flange_width + + def top_flange_thickness(self) -> MM: + """Return the top flange thickness of the I-profile.""" + return self.profile.top_flange_thickness + + def bottom_flange_width(self) -> MM: + """Return the bottom flange width of the I-profile.""" + return self.profile.bottom_flange_width + + def bottom_flange_thickness(self) -> MM: + """Return the bottom flange thickness of the I-profile.""" + return self.profile.bottom_flange_thickness + + def total_height(self) -> MM: + """Return the total height of the I-profile.""" + return self.profile.total_height + + def web_thickness(self) -> MM: + """Return the web thickness of the I-profile.""" + return self.profile.web_thickness + + def top_radius(self) -> MM: + """Return the top radius of the I-profile.""" + return self.profile.top_radius + + def bottom_radius(self) -> MM: + """Return the bottom radius of the I-profile.""" + return self.profile.bottom_radius + + def get_profile(self, corrosion: MM = 0) -> ISteelProfile: + """Return the CHS profile. + + Parameters + ---------- + corrosion : MM, optional + Corrosion thickness per side (default is 0). + """ + top_flange_width = self.top_flange_width() - corrosion * 2 + top_flange_thickness = self.top_flange_thickness() - corrosion * 2 + bottom_flange_width = self.bottom_flange_width() - corrosion * 2 + bottom_flange_thickness = self.bottom_flange_thickness() - corrosion * 2 + total_height = self.total_height() - corrosion * 2 + web_thickness = self.web_thickness() - corrosion * 2 + + if top_flange_thickness <= 0 or bottom_flange_thickness <= 0 or web_thickness <= 0: + raise ValueError("The profile has fully corroded.") + + return ISteelProfile( + top_flange_width=top_flange_width, + top_flange_thickness=top_flange_thickness, + bottom_flange_width=bottom_flange_width, + bottom_flange_thickness=bottom_flange_thickness, + total_height=total_height, + web_thickness=web_thickness, + steel_class=self.steel_class, + top_radius=self.top_radius(), + bottom_radius=self.bottom_radius(), + ) diff --git a/tests/structural_sections/steel/steel_cross_sections/conftest.py b/tests/structural_sections/steel/steel_cross_sections/conftest.py index facf11dc6..aa5e30717 100644 --- a/tests/structural_sections/steel/steel_cross_sections/conftest.py +++ b/tests/structural_sections/steel/steel_cross_sections/conftest.py @@ -4,7 +4,9 @@ from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile, LoadStandardCHS +from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile, LoadStandardIProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import LoadStandardStrip, StripSteelProfile @@ -23,3 +25,11 @@ def chs_profile() -> CHSSteelProfile: profile: CHS = CHS.CHS508x16 steel_class: SteelStrengthClass = SteelStrengthClass.S355 return LoadStandardCHS(profile=profile, steel_class=steel_class).get_profile() + + +@pytest.fixture +def i_profile() -> ISteelProfile: + """Fixture to set up an I-profile for testing.""" + profile = HEB.HEB360 + steel_class = SteelStrengthClass.S355 + return LoadStandardIProfile(profile=profile, steel_class=steel_class).get_profile() diff --git a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py new file mode 100644 index 000000000..e2e463879 --- /dev/null +++ b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py @@ -0,0 +1,101 @@ +"""Test suite for ISteelProfile.""" + +import pytest +from matplotlib import pyplot as plt +from matplotlib.figure import Figure + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile, LoadStandardIProfile +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB + + +class TestISteelProfile: + """Test suite for ISteelProfile.""" + + def test_str(self) -> None: + """Test the string representation of the I-profile.""" + profile = HEB.HEB360 + steel_class = SteelStrengthClass.S355 + expected_str = "Steel class: SteelStrengthClass.S355, Profile: HEB.HEB360" + assert LoadStandardIProfile(profile=profile, steel_class=steel_class).__str__() == expected_str + + def test_alias(self) -> None: + """Test the alias of the I-profile.""" + profile = HEB.HEB360 + alias = profile.alias + expected_alias = "HEB360" + assert alias == expected_alias + + def test_steel_volume_per_meter(self, i_profile: ISteelProfile) -> None: + """Test the steel volume per meter.""" + expected_volume = 1.806e-2 # m³/m + assert pytest.approx(i_profile.steel_volume_per_meter, rel=1e-2) == expected_volume + + def test_steel_weight_per_meter(self, i_profile: ISteelProfile) -> None: + """Test the steel weight per meter.""" + expected_weight = 1.806e-2 * 7850 # kg/m + assert pytest.approx(i_profile.steel_weight_per_meter, rel=1e-2) == expected_weight + + def test_steel_area(self, i_profile: ISteelProfile) -> None: + """Test the steel cross-sectional area.""" + expected_area = 1.806e4 # mm² + assert pytest.approx(i_profile.area, rel=1e-2) == expected_area + + def test_centroid(self, i_profile: ISteelProfile) -> None: + """Test the centroid of the steel cross-section.""" + expected_centroid = (0, 0) # (x, y) coordinates + assert pytest.approx(i_profile.centroid.x, rel=1e-2) == expected_centroid[0] + assert pytest.approx(i_profile.centroid.y, rel=1e-2) == expected_centroid[1] + + def test_moment_of_inertia_about_y(self, i_profile: ISteelProfile) -> None: + """Test the moment of inertia about the y-axis.""" + expected_moi_y = 4.319e8 # mm⁴ + assert pytest.approx(i_profile.moment_of_inertia_about_y, rel=1e-2) == expected_moi_y + + def test_moment_of_inertia_about_z(self, i_profile: ISteelProfile) -> None: + """Test the moment of inertia about the z-axis.""" + expected_moi_z = 1.014e8 # mm⁴ + assert pytest.approx(i_profile.moment_of_inertia_about_z, rel=1e-2) == expected_moi_z + + def test_elastic_section_modulus_about_y_positive(self, i_profile: ISteelProfile) -> None: + """Test the elastic section modulus about the y-axis on the positive z side.""" + expected_modulus_y_positive = 2.4e6 # mm³ + assert pytest.approx(i_profile.elastic_section_modulus_about_y_positive, rel=1e-2) == expected_modulus_y_positive + + def test_elastic_section_modulus_about_y_negative(self, i_profile: ISteelProfile) -> None: + """Test the elastic section modulus about the y-axis on the negative z side.""" + expected_modulus_y_negative = 2.4e6 # mm³ + assert pytest.approx(i_profile.elastic_section_modulus_about_y_negative, rel=1e-2) == expected_modulus_y_negative + + def test_elastic_section_modulus_about_z_positive(self, i_profile: ISteelProfile) -> None: + """Test the elastic section modulus about the z-axis on the positive y side.""" + expected_modulus_z_positive = 6.761e5 # mm³ + assert pytest.approx(i_profile.elastic_section_modulus_about_z_positive, rel=1e-2) == expected_modulus_z_positive + + def test_elastic_section_modulus_about_z_negative(self, i_profile: ISteelProfile) -> None: + """Test the elastic section modulus about the z-axis on the negative y side.""" + expected_modulus_z_negative = 6.761e5 # mm³ + assert pytest.approx(i_profile.elastic_section_modulus_about_z_negative, rel=1e-2) == expected_modulus_z_negative + + def test_plot(self, i_profile: ISteelProfile) -> None: + """Test the plot method (ensure it runs without errors).""" + fig: Figure = i_profile.plot() + assert isinstance(fig, plt.Figure) + + def test_geometry(self, i_profile: ISteelProfile) -> None: + """Test the geometry of the I profile.""" + expected_geometry = i_profile.geometry + assert expected_geometry is not None + + def test_section_properties(self, i_profile: ISteelProfile) -> None: + """Test the section properties of the I profile.""" + section_properties = i_profile.section_properties() + assert section_properties.mass == pytest.approx(expected=i_profile.area, rel=1e-2) + assert section_properties.cx == pytest.approx(expected=i_profile.centroid.x, rel=1e-2) + assert section_properties.cy == pytest.approx(expected=i_profile.centroid.y, rel=1e-2) + assert section_properties.ixx_c == pytest.approx(expected=i_profile.moment_of_inertia_about_y, rel=1e-2) + assert section_properties.iyy_c == pytest.approx(expected=i_profile.moment_of_inertia_about_z, rel=1e-2) + assert section_properties.zxx_plus == pytest.approx(expected=i_profile.elastic_section_modulus_about_y_positive, rel=1e-2) + assert section_properties.zyy_plus == pytest.approx(expected=i_profile.elastic_section_modulus_about_z_positive, rel=1e-2) + assert section_properties.zxx_minus == pytest.approx(expected=i_profile.elastic_section_modulus_about_y_negative, rel=1e-2) + assert section_properties.zyy_minus == pytest.approx(expected=i_profile.elastic_section_modulus_about_z_negative, rel=1e-2) From 50c5fed69badfcc36c5bae8a6512e785b151335f Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 20:17:31 +0200 Subject: [PATCH 22/70] Refactor LoadStandardCHS and LoadStandardStrip classes to clarify profile parameter and update corrosion error messages; adjust test cases for CHS profile geometry and section properties. --- .../steel/steel_cross_sections/chs_profile.py | 11 ++++++----- .../steel/steel_cross_sections/strip_profile.py | 12 ++++++++---- .../steel/steel_cross_sections/test_chs_profile.py | 4 ++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py index 938efd0ec..b5f0934c1 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py @@ -68,19 +68,20 @@ class LoadStandardCHS: Parameters ---------- + profile: CHS + Enumeration of standard CHS profiles steel_class: SteelStrengthClass Enumeration of steel strength classes (default: S355) - profile: CHS - Enumeration of standard CHS profiles (default: CHS508x20) + """ def __init__( self, + profile: CHS, steel_class: SteelStrengthClass = SteelStrengthClass.S355, - profile: CHS = CHS.CHS508x20, ) -> None: - self.steel_class = steel_class self.profile = profile + self.steel_class = steel_class def __str__(self) -> str: """Return the steel class and profile.""" @@ -117,7 +118,7 @@ def get_profile(self, corrosion_outside: MM = 0, corrosion_inside: MM = 0) -> CH adjusted_thickness = self.thickness() - corrosion_outside - corrosion_inside if adjusted_thickness <= 0: - raise ValueError("Adjusted wall thickness must be greater than zero.") + raise ValueError("The profile has fully corroded.") return CHSSteelProfile( outer_diameter=adjusted_outer_diameter, diff --git a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py index dc686699a..735d82fca 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py @@ -70,19 +70,19 @@ class LoadStandardStrip: Parameters ---------- + profile: Strip + Enumeration of standard steel strip profiles steel_class: SteelStrengthClass Enumeration of steel strength classes (default: S355) - profile: Strip - Enumeration of standard steel strip profiles (default: STRIP160x5) """ def __init__( self, + profile: Strip, steel_class: SteelStrengthClass = SteelStrengthClass.S355, - profile: Strip = Strip.STRIP160x5, ) -> None: - self.steel_class = steel_class self.profile = profile + self.steel_class = steel_class def __str__(self) -> str: """Return the steel class and profile.""" @@ -110,4 +110,8 @@ def get_profile(self, corrosion: MM = 0) -> StripSteelProfile: """ width = self.width() - corrosion * 2 height = self.height() - corrosion * 2 + + if width <= 0 or height <= 0: + raise ValueError("The profile has fully corroded.") + return StripSteelProfile(width=width, height=height, steel_class=self.steel_class) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index a3e5c69dd..47f87fa8c 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -84,12 +84,12 @@ def test_plot(self, chs_profile: CHSSteelProfile) -> None: assert isinstance(fig, plt.Figure) def test_geometry(self, chs_profile: CHSSteelProfile) -> None: - """Test the geometry of the Strip profile.""" + """Test the geometry of the CHS profile.""" expected_geometry = chs_profile.geometry assert expected_geometry is not None def test_section_properties(self, chs_profile: CHSSteelProfile) -> None: - """Test the section properties of the Strip profile.""" + """Test the section properties of the CHS profile.""" section_properties = chs_profile.section_properties() assert section_properties.mass == pytest.approx(expected=chs_profile.area, rel=1e-2) assert section_properties.cx == pytest.approx(expected=chs_profile.centroid.x, rel=1e-2) From a055ea79edd1ee37d05c34fee82e94e434037413 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 20:42:07 +0200 Subject: [PATCH 23/70] Add support for HEB and custom I profiles in steel profile shapes example; update documentation to include I profile usage --- docs/examples/_code/steel_profile_shapes.py | 20 ++++++++++++++++++++ docs/examples/steel_profile_shapes.md | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/docs/examples/_code/steel_profile_shapes.py b/docs/examples/_code/steel_profile_shapes.py index d0a898ea4..f894b98d6 100644 --- a/docs/examples/_code/steel_profile_shapes.py +++ b/docs/examples/_code/steel_profile_shapes.py @@ -6,7 +6,9 @@ from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile, LoadStandardCHS +from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile, LoadStandardIProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS +from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import LoadStandardStrip, StripSteelProfile @@ -38,3 +40,21 @@ steel_class=steel_class, ) custom_strip_profile.plot(show=True) + +# Example usage for HEB600 profile +heb_profile = LoadStandardIProfile(profile=HEB.HEB600, steel_class=steel_class).get_profile() +heb_profile.plot(show=True) + +# Example usage for custom I profile +custom_i_profile = ISteelProfile( + top_flange_width=300, # mm + top_flange_thickness=20, # mm + bottom_flange_width=200, # mm + bottom_flange_thickness=10, # mm + total_height=600, # mm + web_thickness=10, # mm + steel_class=steel_class, + top_radius=15, # mm + bottom_radius=8, # mm +) +custom_i_profile.plot(show=True) diff --git a/docs/examples/steel_profile_shapes.md b/docs/examples/steel_profile_shapes.md index dfc3b0981..4dfc8904b 100644 --- a/docs/examples/steel_profile_shapes.md +++ b/docs/examples/steel_profile_shapes.md @@ -52,6 +52,24 @@ Define a custom strip profile by specifying its width and height: --8<-- "examples/_code/steel_profile_shapes.py:34:40" ``` +## Strip Profiles + +### Standard I Profile + +Predefined I profiles are also available: + +```python +--8<-- "examples/_code/steel_profile_shapes.py:40:42" +``` + +### Custom Strip Profile + +Define a custom strip profile by specifying its width and height: + +```python +--8<-- "examples/_code/steel_profile_shapes.py:44:61" +``` + ## Visualizing Profiles For each profile, the `plot` method is used to visualize the shape. The plots will display the geometry of the profiles, making it easier to understand their dimensions and configurations. From c44cdccac08aef18faf164f6f817acaeb75ba4a5 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 20:45:47 +0200 Subject: [PATCH 24/70] Update steel profile examples to include corrosion parameters for CHS, Strip, and HEB profiles --- docs/examples/_code/steel_profile_shapes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/_code/steel_profile_shapes.py b/docs/examples/_code/steel_profile_shapes.py index f894b98d6..5c9abe64b 100644 --- a/docs/examples/_code/steel_profile_shapes.py +++ b/docs/examples/_code/steel_profile_shapes.py @@ -16,7 +16,7 @@ steel_class = SteelStrengthClass.S355 # Example usage for CHS profile -chs_profile = LoadStandardCHS(profile=CHS.CHS273x5, steel_class=steel_class).get_profile() +chs_profile = LoadStandardCHS(profile=CHS.CHS273x5, steel_class=steel_class).get_profile(corrosion_inside=0, corrosion_outside=1) chs_profile.plot(show=True) print(f"Steel class: {chs_profile.steel_material}") print(f"Moment of inertia about y-axis: {chs_profile.moment_of_inertia_about_y} mm⁴") @@ -30,7 +30,7 @@ custom_chs_profile.plot(show=True) # Example usage for Strip profile -strip_profile = LoadStandardStrip(profile=Strip.STRIP160x5, steel_class=steel_class).get_profile() +strip_profile = LoadStandardStrip(profile=Strip.STRIP160x5, steel_class=steel_class).get_profile(corrosion=1) strip_profile.plot(show=True) # Example usage for custom Strip profile @@ -42,7 +42,7 @@ custom_strip_profile.plot(show=True) # Example usage for HEB600 profile -heb_profile = LoadStandardIProfile(profile=HEB.HEB600, steel_class=steel_class).get_profile() +heb_profile = LoadStandardIProfile(profile=HEB.HEB600, steel_class=steel_class).get_profile(corrosion=7) heb_profile.plot(show=True) # Example usage for custom I profile From 4bd98bd32ed4e4834899f493bc500e0455b018e2 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 21:01:45 +0200 Subject: [PATCH 25/70] Add corrosion tests for CHS, HEB, and Strip profiles to validate error handling --- .../cross_section_quarter_circular_spandrel.py | 8 ++++---- .../steel/steel_cross_sections/test_chs_profile.py | 11 +++++++++++ .../steel/steel_cross_sections/test_i_profile.py | 11 +++++++++++ .../steel/steel_cross_sections/test_strip_profile.py | 11 +++++++++++ .../test_cross_section_quarter_circular_spandrel.py | 2 +- 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/blueprints/structural_sections/cross_section_quarter_circular_spandrel.py b/blueprints/structural_sections/cross_section_quarter_circular_spandrel.py index df40fb4a8..b639b4c48 100644 --- a/blueprints/structural_sections/cross_section_quarter_circular_spandrel.py +++ b/blueprints/structural_sections/cross_section_quarter_circular_spandrel.py @@ -51,12 +51,12 @@ def polygon(self) -> Polygon: """ left_lower = (self.x, self.y) - # Approximate the quarter circle with 50 straight lines + # Approximate the quarter circle with 25 straight lines quarter_circle_points = [ - (self.x + self.radius - self.radius * math.cos(math.pi / 2 * i / 50), self.y + self.radius - self.radius * math.sin(math.pi / 2 * i / 50)) - for i in range(51) + (self.x + self.radius - self.radius * math.cos(math.pi / 2 * i / 25), self.y + self.radius - self.radius * math.sin(math.pi / 2 * i / 25)) + for i in range(26) ] - for i in range(51): + for i in range(26): if self.mirrored_horizontally: quarter_circle_points[i] = (2 * left_lower[0] - quarter_circle_points[i][0], quarter_circle_points[i][1]) if self.mirrored_vertically: diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index 47f87fa8c..90063bc02 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -100,3 +100,14 @@ def test_section_properties(self, chs_profile: CHSSteelProfile) -> None: assert section_properties.zyy_plus == pytest.approx(expected=chs_profile.elastic_section_modulus_about_z_positive, rel=1e-2) assert section_properties.zxx_minus == pytest.approx(expected=chs_profile.elastic_section_modulus_about_y_negative, rel=1e-2) assert section_properties.zyy_minus == pytest.approx(expected=chs_profile.elastic_section_modulus_about_z_negative, rel=1e-2) + + def test_get_profile_with_corrosion(self) -> None: + """Test the CHS profile with 20 mm corrosion applied.""" + profile: CHS = CHS.CHS508x16 + steel_class: SteelStrengthClass = SteelStrengthClass.S355 + + loader = LoadStandardCHS(profile=profile, steel_class=steel_class) + + # Ensure the profile raises an error if fully corroded + with pytest.raises(ValueError, match="The profile has fully corroded."): + loader.get_profile(corrosion_outside=5, corrosion_inside=11) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py index e2e463879..16b69c7fe 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py @@ -99,3 +99,14 @@ def test_section_properties(self, i_profile: ISteelProfile) -> None: assert section_properties.zyy_plus == pytest.approx(expected=i_profile.elastic_section_modulus_about_z_positive, rel=1e-2) assert section_properties.zxx_minus == pytest.approx(expected=i_profile.elastic_section_modulus_about_y_negative, rel=1e-2) assert section_properties.zyy_minus == pytest.approx(expected=i_profile.elastic_section_modulus_about_z_negative, rel=1e-2) + + def test_get_profile_with_corrosion(self) -> None: + """Test the EHB profile with 20 mm corrosion applied.""" + profile: HEB = HEB.HEB360 + steel_class: SteelStrengthClass = SteelStrengthClass.S355 + + loader = LoadStandardIProfile(profile=profile, steel_class=steel_class) + + # Ensure the profile raises an error if fully corroded + with pytest.raises(ValueError, match="The profile has fully corroded."): + loader.get_profile(corrosion=20.0) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index badab3f66..b0daad827 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -100,3 +100,14 @@ def test_section_properties(self, strip_profile: StripSteelProfile) -> None: assert section_properties.zyy_plus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_z_positive, rel=1e-2) assert section_properties.zxx_minus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_y_negative, rel=1e-2) assert section_properties.zyy_minus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_z_negative, rel=1e-2) + + def test_get_profile_with_corrosion(self) -> None: + """Test the Strip profile with 2 mm corrosion applied.""" + profile: Strip = Strip.STRIP160x5 + steel_class: SteelStrengthClass = SteelStrengthClass.S355 + + loader = LoadStandardStrip(profile=profile, steel_class=steel_class) + + # Ensure the profile raises an error if fully corroded + with pytest.raises(ValueError, match="The profile has fully corroded."): + loader.get_profile(corrosion=2.5) diff --git a/tests/structural_sections/test_cross_section_quarter_circular_spandrel.py b/tests/structural_sections/test_cross_section_quarter_circular_spandrel.py index f9d4eb384..248ff027d 100644 --- a/tests/structural_sections/test_cross_section_quarter_circular_spandrel.py +++ b/tests/structural_sections/test_cross_section_quarter_circular_spandrel.py @@ -53,7 +53,7 @@ def test_polygon(self, qcs_cross_section: QuarterCircularSpandrelCrossSection) - """Test the geometry property of the QuarterCircularSpandrelCrossSection class.""" polygon = qcs_cross_section.polygon assert polygon.is_valid - assert polygon.area == pytest.approx(expected=qcs_cross_section.area, rel=1e-3) + assert polygon.area == pytest.approx(expected=qcs_cross_section.area, rel=1e-2) def test_mirrored_geometry(self) -> None: """Test the geometry property for flipped cross-sections.""" From 836d04e971b482170895d75baff66aa87155f2d4 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 21:31:55 +0200 Subject: [PATCH 26/70] Move mock fixtures for CrossSection and SteelMaterial to conftest.py for better test organization --- .../steel/steel_element.py | 5 -- tests/structural_sections/steel/conftest.py | 46 +++++++++++++++++++ .../steel/test_steel_element.py | 45 ------------------ 3 files changed, 46 insertions(+), 50 deletions(-) create mode 100644 tests/structural_sections/steel/conftest.py diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index 88e0ee36a..072f184f8 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -30,11 +30,6 @@ class SteelElement: material: SteelMaterial nominal_thickness: MM - def __post_init__(self) -> None: - """Check if the material is a SteelMaterial.""" - if not isinstance(self.material, SteelMaterial): - raise TypeError(f"Expected a SteelMaterial, but got: {type(self.material)}") - @property def geometry(self) -> Geometry: """Return the geometry of the steel element.""" diff --git a/tests/structural_sections/steel/conftest.py b/tests/structural_sections/steel/conftest.py new file mode 100644 index 000000000..dcd462eb0 --- /dev/null +++ b/tests/structural_sections/steel/conftest.py @@ -0,0 +1,46 @@ +"""Fixtures for steel elements.""" + +from unittest.mock import Mock + +import pytest +from shapely.geometry import Point + +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections._cross_section import CrossSection +from blueprints.structural_sections.steel.steel_element import SteelElement + + +@pytest.fixture +def mock_cross_section(mocker: Mock) -> Mock: + """Mock a CrossSection object.""" + cross_section: Mock = mocker.Mock(spec=CrossSection) + cross_section.name = "MockSection" + cross_section.area = 683 # mm² + cross_section.perimeter = 400 # mm + cross_section.centroid = Point(50, 50) + cross_section.moment_of_inertia_about_y = 2000 # mm⁴ + cross_section.moment_of_inertia_about_z = 3000 # mm⁴ + cross_section.elastic_section_modulus_about_y_positive = 100 # mm³ + cross_section.elastic_section_modulus_about_y_negative = 90 # mm³ + cross_section.elastic_section_modulus_about_z_positive = 80 # mm³ + cross_section.elastic_section_modulus_about_z_negative = 70 # mm³ + cross_section.plastic_section_modulus_about_y = 60 # mm³ + cross_section.plastic_section_modulus_about_z = 50 # mm³ + cross_section.geometry = {"type": "rectangle", "width": 100, "height": 50} + return cross_section + + +@pytest.fixture +def mock_material(mocker: Mock) -> Mock: + """Mock a SteelMaterial object.""" + material: Mock = mocker.Mock(spec=SteelMaterial) + material.density = 7850 # kg/m³ + material.yield_strength.return_value = 250 # MPa + material.ultimate_strength.return_value = 400 # MPa + return material + + +@pytest.fixture +def steel_element(mock_cross_section: Mock, mock_material: Mock) -> SteelElement: + """Create a SteelElement instance using mocked cross-section and material.""" + return SteelElement(cross_section=mock_cross_section, material=mock_material, nominal_thickness=10) diff --git a/tests/structural_sections/steel/test_steel_element.py b/tests/structural_sections/steel/test_steel_element.py index 1062e9ba3..80823e964 100644 --- a/tests/structural_sections/steel/test_steel_element.py +++ b/tests/structural_sections/steel/test_steel_element.py @@ -3,49 +3,10 @@ from unittest.mock import Mock import pytest -from shapely.geometry import Point -from blueprints.materials.steel import SteelMaterial -from blueprints.structural_sections._cross_section import CrossSection from blueprints.structural_sections.steel.steel_element import SteelElement -@pytest.fixture -def mock_cross_section(mocker: Mock) -> Mock: - """Mock a CrossSection object.""" - cross_section: Mock = mocker.Mock(spec=CrossSection) - cross_section.name = "MockSection" - cross_section.area = 683 # mm² - cross_section.perimeter = 400 # mm - cross_section.centroid = Point(50, 50) - cross_section.moment_of_inertia_about_y = 2000 # mm⁴ - cross_section.moment_of_inertia_about_z = 3000 # mm⁴ - cross_section.elastic_section_modulus_about_y_positive = 100 # mm³ - cross_section.elastic_section_modulus_about_y_negative = 90 # mm³ - cross_section.elastic_section_modulus_about_z_positive = 80 # mm³ - cross_section.elastic_section_modulus_about_z_negative = 70 # mm³ - cross_section.plastic_section_modulus_about_y = 60 # mm³ - cross_section.plastic_section_modulus_about_z = 50 # mm³ - cross_section.geometry = {"type": "rectangle", "width": 100, "height": 50} - return cross_section - - -@pytest.fixture -def mock_material(mocker: Mock) -> Mock: - """Mock a SteelMaterial object.""" - material: Mock = mocker.Mock(spec=SteelMaterial) - material.density = 7850 # kg/m³ - material.yield_strength.return_value = 250 # MPa - material.ultimate_strength.return_value = 400 # MPa - return material - - -@pytest.fixture -def steel_element(mock_cross_section: Mock, mock_material: Mock) -> SteelElement: - """Create a SteelElement instance using mocked cross-section and material.""" - return SteelElement(cross_section=mock_cross_section, material=mock_material, nominal_thickness=10) - - def test_name(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement name matches the mock cross-section name.""" assert steel_element.name == mock_cross_section.name @@ -122,12 +83,6 @@ def test_ultimate_strength(steel_element: SteelElement, mock_material: Mock) -> assert steel_element.ultimate_strength == mock_material.ultimate_strength.return_value -def test_invalid_material_type(mock_cross_section: Mock) -> None: - """Test that creating a SteelElement with an invalid material type raises a TypeError.""" - with pytest.raises(TypeError): - SteelElement(cross_section=mock_cross_section, material=mock_material, nominal_thickness=10) - - def test_invalid_yield_strength(steel_element: SteelElement, mock_material: Mock) -> None: """Test that accessing yield strength raises a ValueError if not defined.""" mock_material.yield_strength.return_value = None From 6c1eeaccb049ef261eb6743f271780db0d62372d Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 21:42:27 +0200 Subject: [PATCH 27/70] Fix type hints for mock fixtures in conftest.py to improve clarity --- tests/structural_sections/steel/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/structural_sections/steel/conftest.py b/tests/structural_sections/steel/conftest.py index dcd462eb0..c16932ce7 100644 --- a/tests/structural_sections/steel/conftest.py +++ b/tests/structural_sections/steel/conftest.py @@ -11,7 +11,7 @@ @pytest.fixture -def mock_cross_section(mocker: Mock) -> Mock: +def mock_cross_section(mocker: Mock) -> CrossSection: """Mock a CrossSection object.""" cross_section: Mock = mocker.Mock(spec=CrossSection) cross_section.name = "MockSection" @@ -31,7 +31,7 @@ def mock_cross_section(mocker: Mock) -> Mock: @pytest.fixture -def mock_material(mocker: Mock) -> Mock: +def mock_material(mocker: Mock) -> SteelMaterial: """Mock a SteelMaterial object.""" material: Mock = mocker.Mock(spec=SteelMaterial) material.density = 7850 # kg/m³ From 2f59eca81418dce7a959b5337cf15a0e66e4696e Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 21:46:47 +0200 Subject: [PATCH 28/70] Add BaseGeometry import for type checking and improve polygon method handling --- .../steel/steel_cross_sections/_steel_cross_section.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index 76369ac42..975351576 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -6,6 +6,7 @@ from sectionproperties.post.post import SectionProperties from sectionproperties.pre import Geometry from shapely.geometry import Point, Polygon +from shapely.geometry.base import BaseGeometry # Add this import for type checking from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_element import SteelElement @@ -33,10 +34,10 @@ def __init__( @property def polygon(self) -> Polygon: """Return the polygon of the steel cross-section.""" - combined_polygon = self.elements[0].polygon + combined_polygon: BaseGeometry = self.elements[0].polygon for element in self.elements[1:]: combined_polygon = combined_polygon.union(element.polygon) - return combined_polygon + return Polygon(combined_polygon) if not combined_polygon.is_empty else Polygon() @property def steel_volume_per_meter(self) -> M3_M: From b2e67e35dd9a27b9ac670d80645b7cb1837ff9d8 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 21:50:50 +0200 Subject: [PATCH 29/70] Enhance polygon method to ensure valid geometry and consistent orientation --- .../steel/steel_cross_sections/_steel_cross_section.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index 975351576..c08fc7a74 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -6,7 +6,8 @@ from sectionproperties.post.post import SectionProperties from sectionproperties.pre import Geometry from shapely.geometry import Point, Polygon -from shapely.geometry.base import BaseGeometry # Add this import for type checking +from shapely.geometry.base import BaseGeometry +from shapely.geometry.polygon import orient # Add this import for ensuring valid polygons from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_element import SteelElement @@ -37,7 +38,11 @@ def polygon(self) -> Polygon: combined_polygon: BaseGeometry = self.elements[0].polygon for element in self.elements[1:]: combined_polygon = combined_polygon.union(element.polygon) - return Polygon(combined_polygon) if not combined_polygon.is_empty else Polygon() + + # Ensure the result is a valid Polygon + if isinstance(combined_polygon, Polygon): + return orient(combined_polygon) # Ensure consistent orientation + raise TypeError("The combined geometry is not a valid Polygon.") @property def steel_volume_per_meter(self) -> M3_M: From 1811fb3a83b67ff1777e8dc9debb891486d672a7 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 22:02:28 +0200 Subject: [PATCH 30/70] Add mocker fixture for improved object mocking in tests --- tests/structural_sections/steel/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/structural_sections/steel/conftest.py b/tests/structural_sections/steel/conftest.py index c16932ce7..b5b7ad3d0 100644 --- a/tests/structural_sections/steel/conftest.py +++ b/tests/structural_sections/steel/conftest.py @@ -10,6 +10,12 @@ from blueprints.structural_sections.steel.steel_element import SteelElement +@pytest.fixture +def mocker() -> Mock: + """Provide a mocker instance for mocking objects.""" + return Mock() + + @pytest.fixture def mock_cross_section(mocker: Mock) -> CrossSection: """Mock a CrossSection object.""" From 15d0861bd6cb6658e3225117fbcf24d7eb62d4ac Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 22 Apr 2025 22:08:30 +0200 Subject: [PATCH 31/70] Ensure combined geometry is a valid Polygon with consistent orientation --- .../steel/steel_cross_sections/_steel_cross_section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index c08fc7a74..39186ce72 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -42,7 +42,7 @@ def polygon(self) -> Polygon: # Ensure the result is a valid Polygon if isinstance(combined_polygon, Polygon): return orient(combined_polygon) # Ensure consistent orientation - raise TypeError("The combined geometry is not a valid Polygon.") + raise TypeError("The combined geometry is not a valid Polygon.") # pragma: no cover @property def steel_volume_per_meter(self) -> M3_M: From b6c15cc8598a88cfda3e0434af6ee38bec616a86 Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Mon, 28 Apr 2025 19:26:16 +0200 Subject: [PATCH 32/70] Update plastic section modulus properties to allow None return type --- blueprints/structural_sections/steel/steel_element.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index 072f184f8..628038fe3 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -91,12 +91,12 @@ def elastic_section_modulus_about_z_negative(self) -> MM3: return self.cross_section.elastic_section_modulus_about_z_negative @property - def plastic_section_modulus_about_y(self) -> MM3: + def plastic_section_modulus_about_y(self) -> MM3 | None: """Plastic section modulus about the y-axis [mm³].""" return self.cross_section.plastic_section_modulus_about_y @property - def plastic_section_modulus_about_z(self) -> MM3: + def plastic_section_modulus_about_z(self) -> MM3 | None: """Plastic section modulus about the z-axis [mm³].""" return self.cross_section.plastic_section_modulus_about_z From 75fb1b3270baa1d04a5a913d1125af1509e635a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Sun, 4 May 2025 10:56:57 +0200 Subject: [PATCH 33/70] Refactor steel cross-section computations using `polygon`. Replaced element-wise computations with a single `polygon`-based approach for area, volume, weight, and centroid calculations. Simplifies the code and ensures consistency by leveraging geometric operations on the composite shape. Removed unnecessary `pragma: no cover` comments for clearer code coverage. --- .../_steel_cross_section.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index 39186ce72..f4b7f29b7 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -29,8 +29,8 @@ def __init__( steel_material : SteelMaterial Material properties of the steel. """ - self.steel_material = steel_material # pragma: no cover - self.elements: list[SteelElement] = [] # pragma: no cover + self.steel_material = steel_material + self.elements: list[SteelElement] = [] @property def polygon(self) -> Polygon: @@ -42,13 +42,13 @@ def polygon(self) -> Polygon: # Ensure the result is a valid Polygon if isinstance(combined_polygon, Polygon): return orient(combined_polygon) # Ensure consistent orientation - raise TypeError("The combined geometry is not a valid Polygon.") # pragma: no cover + raise TypeError("The combined geometry is not a valid Polygon.") @property def steel_volume_per_meter(self) -> M3_M: """Total volume of the reinforced cross-section per meter length [m³/m].""" length = 1000 # mm - return sum(element.area * length * MM3_TO_M3 for element in self.elements) + return self.polygon.area * length * MM3_TO_M3 @property def steel_weight_per_meter(self) -> KG_M: @@ -57,17 +57,13 @@ def steel_weight_per_meter(self) -> KG_M: @property def area(self) -> MM2: - """Total cross sectional area of the steel element [mm²].""" - return sum(element.area for element in self.elements) + """Total cross-sectional area of the steel element [mm²].""" + return self.polygon.area @property def centroid(self) -> Point: """Centroid of the steel cross-section.""" - area_weighted_centroids_x = sum(element.centroid.x * element.area for element in self.elements) - area_weighted_centroids_y = sum(element.centroid.y * element.area for element in self.elements) - centroid_x = area_weighted_centroids_x / self.area - centroid_y = area_weighted_centroids_y / self.area - return Point(centroid_x, centroid_y) + return self.polygon.centroid @property def moment_of_inertia_about_y(self) -> KG_M: From b23d0b6e1540c9ca3557c9872b3c1171d39ec5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 09:52:09 +0200 Subject: [PATCH 34/70] Refactor cross-section methods to remove abstraction. Replaced abstract methods in the cross-section class with concrete implementations using `shapely.Polygon` properties. Added default behavior for the `geometry` method, including mesh size handling and mesh creation. This simplifies usage and aligns the class with its intended functionality. --- blueprints/structural_sections/_cross_section.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/blueprints/structural_sections/_cross_section.py b/blueprints/structural_sections/_cross_section.py index 4cd4f946e..82d6f9f0e 100644 --- a/blueprints/structural_sections/_cross_section.py +++ b/blueprints/structural_sections/_cross_section.py @@ -24,19 +24,19 @@ def polygon(self) -> Polygon: """Shapely Polygon representing the cross-section.""" @property - @abstractmethod def area(self) -> MM2: """Area of the cross-section [mm²].""" + return self.polygon.area @property - @abstractmethod def perimeter(self) -> MM: """Perimeter of the cross-section [mm].""" + return self.polygon.length @property - @abstractmethod def centroid(self) -> Point: """Centroid of the cross-section [mm].""" + return self.polygon.centroid @property @abstractmethod @@ -78,9 +78,8 @@ def plastic_section_modulus_about_y(self) -> MM3 | None: def plastic_section_modulus_about_z(self) -> MM3 | None: """Plastic section modulus about the z-axis [mm³].""" - @abstractmethod def geometry(self, mesh_size: MM | None = None) -> Geometry: - """Abstract method to be implemented by subclasses to return the geometry of the cross-section. + """Geometry of the cross-section. Properties ---------- @@ -88,6 +87,12 @@ def geometry(self, mesh_size: MM | None = None) -> Geometry: Maximum mesh element area to be used within the Geometry-object finite-element mesh. If not provided, a default value will be used. """ + if mesh_size is None: + mesh_size = 2.0 + + geom = Geometry(geom=self.polygon) + geom.create_mesh(mesh_sizes=mesh_size) + return geom def section(self) -> Section: """Section object representing the cross-section.""" From 2a4d9b4f389af05e84118572abc11c0c1df9866f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 10:08:34 +0200 Subject: [PATCH 35/70] Refactor docstring format in HEA profile initializer. Updated the docstring to follow a consistent format with proper sections and parameter descriptions. This improves clarity and aligns with documentation standards. --- .../standard_profiles/hea.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hea.py b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hea.py index 39266d1ca..c0b5de981 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hea.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hea.py @@ -35,13 +35,23 @@ class HEA(Enum): def __init__(self, alias: str, h: MM, b: MM, t_w: MM, t_f: MM, radius: MM) -> None: """Initialize HEA profile. - Args: - alias (str): Profile alias. - h (MM): Total height. - b (MM): Total width. - t_w (MM): Web thickness. - t_f (MM): Flange thickness. - radius (MM): Radius. + + This method sets the profile's alias, dimensions, and radii. + + Parameters + ---------- + alias: str + Profile alias. + h: MM + Total height of the profile. + b: MM + Total width of the profile. + t_w: MM + Web thickness of the profile. + t_f: MM + Flange thickness of the profile. + radius: MM + Radius of the profile. """ self.alias = alias self.top_flange_width = b From 6c1d0562112e5a50b5439da57a8413b11c1ff2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 10:10:12 +0200 Subject: [PATCH 36/70] Update docstrings for HEB, HEM, and IPE profile classes Revised the constructor docstrings across the HEB, HEM, and IPE classes to use the "Parameters" section format for better readability and consistency. This change enhances clarity regarding the attributes and their descriptions. --- .../standard_profiles/heb.py | 24 +++++++++++++------ .../standard_profiles/hem.py | 24 +++++++++++++------ .../standard_profiles/ipe.py | 24 +++++++++++++------ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/heb.py b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/heb.py index ad39343e9..52b4eaa62 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/heb.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/heb.py @@ -35,13 +35,23 @@ class HEB(Enum): def __init__(self, alias: str, h: MM, b: MM, t_w: MM, t_f: MM, radius: MM) -> None: """Initialize HEB profile. - Args: - alias (str): Profile alias. - h (MM): Total height. - b (MM): Total width. - t_w (MM): Web thickness. - t_f (MM): Flange thickness. - radius (MM): Radius. + + This method sets the profile's alias, dimensions, and radii. + + Parameters + ---------- + alias: str + Profile alias. + h: MM + Total height of the profile. + b: MM + Total width of the profile. + t_w: MM + Web thickness of the profile. + t_f: MM + Flange thickness of the profile. + radius: MM + Radius of the profile. """ self.alias = alias self.top_flange_width = b diff --git a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hem.py b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hem.py index b9b47e481..bcdc53590 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hem.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hem.py @@ -35,13 +35,23 @@ class HEM(Enum): def __init__(self, alias: str, h: MM, b: MM, t_w: MM, t_f: MM, radius: MM) -> None: """Initialize HEM profile. - Args: - alias (str): Profile alias. - h (MM): Total height. - b (MM): Total width. - t_w (MM): Web thickness. - t_f (MM): Flange thickness. - radius (MM): Radius. + + This method sets the profile's alias, dimensions, and radii. + + Parameters + ---------- + alias: str + Profile alias. + h: MM + Total height of the profile. + b: MM + Total width of the profile. + t_w: MM + Web thickness of the profile. + t_f: MM + Flange thickness of the profile. + radius: MM + Radius of the profile. """ self.alias = alias self.top_flange_width = b diff --git a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/ipe.py b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/ipe.py index 90e2770d4..afcd9f5b9 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/ipe.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/ipe.py @@ -29,13 +29,23 @@ class IPE(Enum): def __init__(self, alias: str, h: MM, b: MM, t_w: MM, t_f: MM, radius: MM) -> None: """Initialize IPE profile. - Args: - alias (str): Profile alias. - h (MM): Total height. - b (MM): Total width. - t_w (MM): Web thickness. - t_f (MM): Flange thickness. - radius (MM): Radius. + + This method sets the profile's alias, dimensions, and radii. + + Parameters + ---------- + alias: str + Profile alias. + h: MM + Total height of the profile. + b: MM + Total width of the profile. + t_w: MM + Web thickness of the profile. + t_f: MM + Flange thickness of the profile. + radius: MM + Radius of the profile. """ self.alias = alias self.top_flange_width = b From 07db4cd808d91cbae9ee87d5a4f218504a33d619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 10:20:54 +0200 Subject: [PATCH 37/70] Update flange thickness for HEM280 profile Corrected the flange thickness of the HEM280 profile from 33 to 39 to match the accurate specification. --- .../steel/steel_cross_sections/standard_profiles/hem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hem.py b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hem.py index bcdc53590..a39be84bf 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hem.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/standard_profiles/hem.py @@ -17,7 +17,7 @@ class HEM(Enum): HEM220 = ("HEM220", 240, 226, 15.5, 26, 18) HEM240 = ("HEM240", 270, 248, 18, 32, 21) HEM260 = ("HEM260", 290, 268, 18, 32.5, 24) - HEM280 = ("HEM280", 310, 288, 18.5, 33, 24) + HEM280 = ("HEM280", 310, 288, 18.5, 39, 24) HEM300 = ("HEM300", 340, 310, 21, 39, 27) HEM320 = ("HEM320", 359, 309, 21, 40, 27) HEM340 = ("HEM340", 377, 309, 21, 40, 27) From a0b8be0432a76a9e9d181f9aca6a51142f3e76ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 10:54:27 +0200 Subject: [PATCH 38/70] Clarify documentation for nominal thickness in steel elements. Added details regarding the use of nominal thickness for strength calculations and highlighted the lack of an internal check to verify its accuracy against the cross-section. This improves clarity for users and prevents potential misunderstandings. --- blueprints/structural_sections/steel/steel_element.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index 628038fe3..dedfd5c69 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -23,7 +23,11 @@ class SteelElement: material : SteelMaterial The material of the steel element. nominal_thickness : MM - The nominal thickness of the steel element + The nominal thickness of the steel element. + + This is used to calculate the yield and ultimate strength of the steel element. + But be aware that there is no internal check to make sure that the given nominal thickness + is actually the thickness of the cross-section. """ cross_section: CrossSection From 9f2ee367ef723db7618422f12097cf12f14268ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 12:20:33 +0200 Subject: [PATCH 39/70] Remove redundant properties from SteelElement class Eliminated properties that duplicated functionality already provided by the CrossSection class, streamlining the SteelElement implementation. This change reduces code duplication and improves maintainability. --- .../steel/steel_element.py | 75 +------------------ 1 file changed, 1 insertion(+), 74 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index dedfd5c69..f132dc6d0 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -2,12 +2,9 @@ from dataclasses import dataclass -from sectionproperties.pre import Geometry -from shapely import Point, Polygon - from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections._cross_section import CrossSection -from blueprints.type_alias import KG_M, MM, MM2, MM3, MM4, MPA +from blueprints.type_alias import KG_M, MM, MPA from blueprints.unit_conversion import MM2_TO_M2 @@ -34,76 +31,6 @@ class SteelElement: material: SteelMaterial nominal_thickness: MM - @property - def geometry(self) -> Geometry: - """Return the geometry of the steel element.""" - return self.cross_section.geometry() - - @property - def polygon(self) -> Polygon: - """Return the polygon of the steel element.""" - return self.cross_section.polygon - - @property - def name(self) -> str: - """Name of the steel element.""" - return self.cross_section.name - - @property - def area(self) -> MM2: - """Area of the cross-section [mm²].""" - return self.cross_section.area - - @property - def perimeter(self) -> MM: - """Perimeter of the cross-section [mm].""" - return self.cross_section.perimeter - - @property - def centroid(self) -> Point: - """Centroid of the cross-section [mm].""" - return self.cross_section.centroid - - @property - def moment_of_inertia_about_y(self) -> MM4: - """Moments of inertia of the cross-section [mm⁴].""" - return self.cross_section.moment_of_inertia_about_y - - @property - def moment_of_inertia_about_z(self) -> MM4: - """Moments of inertia of the cross-section [mm⁴].""" - return self.cross_section.moment_of_inertia_about_z - - @property - def elastic_section_modulus_about_y_positive(self) -> MM3: - """Elastic section modulus about the y-axis on the positive z side [mm³].""" - return self.cross_section.elastic_section_modulus_about_y_positive - - @property - def elastic_section_modulus_about_y_negative(self) -> MM3: - """Elastic section modulus about the y-axis on the negative z side [mm³].""" - return self.cross_section.elastic_section_modulus_about_y_negative - - @property - def elastic_section_modulus_about_z_positive(self) -> MM3: - """Elastic section modulus about the z-axis on the positive y side [mm³].""" - return self.cross_section.elastic_section_modulus_about_z_positive - - @property - def elastic_section_modulus_about_z_negative(self) -> MM3: - """Elastic section modulus about the z-axis on the negative y side [mm³].""" - return self.cross_section.elastic_section_modulus_about_z_negative - - @property - def plastic_section_modulus_about_y(self) -> MM3 | None: - """Plastic section modulus about the y-axis [mm³].""" - return self.cross_section.plastic_section_modulus_about_y - - @property - def plastic_section_modulus_about_z(self) -> MM3 | None: - """Plastic section modulus about the z-axis [mm³].""" - return self.cross_section.plastic_section_modulus_about_z - @property def weight_per_meter(self) -> KG_M: """ From 0eac108350cc75140f99d39823fa81b7e31b5f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 12:36:46 +0200 Subject: [PATCH 40/70] Clarify nominal thickness description in docstring. Adjusted the docstring for better clarity, specifying that the nominal thickness of the steel element should match the cross-section thickness. This improves the documentation but does not modify any functionality. --- blueprints/structural_sections/steel/steel_element.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_element.py b/blueprints/structural_sections/steel/steel_element.py index f132dc6d0..6ce973e42 100644 --- a/blueprints/structural_sections/steel/steel_element.py +++ b/blueprints/structural_sections/steel/steel_element.py @@ -23,8 +23,8 @@ class SteelElement: The nominal thickness of the steel element. This is used to calculate the yield and ultimate strength of the steel element. - But be aware that there is no internal check to make sure that the given nominal thickness - is actually the thickness of the cross-section. + But be aware that there is no internal check to make sure that the given nominal thickness of this steel element + is actually the same thickness of the cross-section. """ cross_section: CrossSection From 4b6baabe5b304061edd5381c264b8c1b057f9873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 12:37:27 +0200 Subject: [PATCH 41/70] Refactor steel cross-section class for dataclass support Refactored the base steel cross-section class to use `@dataclass` for improved readability and maintainability. Renamed the class to `CombinedSteelCrossSection`, added proper type annotations, and updated methods to better align with the new structure. Improved handling of elements, polygon merging, and strength calculations for compatibility with the updated architecture. --- .../_steel_cross_section.py | 187 +++++++++--------- 1 file changed, 93 insertions(+), 94 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index f4b7f29b7..7f31bab31 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -1,155 +1,154 @@ """Base class of all steel cross-sections.""" from abc import ABC +from collections.abc import Sequence +from dataclasses import dataclass, field -from sectionproperties.analysis import Section -from sectionproperties.post.post import SectionProperties -from sectionproperties.pre import Geometry -from shapely.geometry import Point, Polygon +from shapely.geometry import Polygon from shapely.geometry.base import BaseGeometry -from shapely.geometry.polygon import orient # Add this import for ensuring valid polygons +from shapely.geometry.polygon import orient -from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections._cross_section import CrossSection from blueprints.structural_sections.steel.steel_element import SteelElement -from blueprints.type_alias import KG_M, M3_M, MM, MM2 +from blueprints.type_alias import KG_M, M3_M, MM3, MPA from blueprints.unit_conversion import MM3_TO_M3 -class SteelCrossSection(ABC): - """Base class of all steel cross-sections.""" +@dataclass(kw_only=True) +class CombinedSteelCrossSection(CrossSection, ABC): + """Base class of all steel cross-sections. - def __init__( - self, - steel_material: SteelMaterial, - ) -> None: - """Initialize the steel cross-section. + Parameters + ---------- + elements : Sequence[SteelElement], optional + A sequence of steel elements that make up the cross-section. + Default is an empty list. + name : str, optional + The name of the cross-section. Default is "Combined Steel Cross Section". + """ - Parameters - ---------- - steel_material : SteelMaterial - Material properties of the steel. - """ - self.steel_material = steel_material - self.elements: list[SteelElement] = [] + elements: Sequence[SteelElement] = field(default_factory=list) + name: str = "Combined Steel Cross Section" @property def polygon(self) -> Polygon: """Return the polygon of the steel cross-section.""" - combined_polygon: BaseGeometry = self.elements[0].polygon + # check if there are any elements + if not self.elements: + raise ValueError("No elements have been added to the cross-section.") + + # return the polygon of the first element if there is only one + if len(self.elements) == 1: + return self.elements[0].cross_section.polygon + + # Combine the polygons of all elements if there is multiple + combined_polygon: BaseGeometry = self.elements[0].cross_section.polygon for element in self.elements[1:]: - combined_polygon = combined_polygon.union(element.polygon) + combined_polygon = combined_polygon.union(element.cross_section.polygon) # Ensure the result is a valid Polygon - if isinstance(combined_polygon, Polygon): - return orient(combined_polygon) # Ensure consistent orientation - raise TypeError("The combined geometry is not a valid Polygon.") + if not isinstance(combined_polygon, Polygon): + raise TypeError("The combined geometry is not a valid Polygon.") + + # Ensure consistent orientation + return orient(combined_polygon) @property - def steel_volume_per_meter(self) -> M3_M: + def volume_per_meter(self) -> M3_M: """Total volume of the reinforced cross-section per meter length [m³/m].""" length = 1000 # mm - return self.polygon.area * length * MM3_TO_M3 - - @property - def steel_weight_per_meter(self) -> KG_M: - """Total weight of the steel elements per meter length [kg/m].""" - return self.steel_material.density * self.steel_volume_per_meter + return self.area * length * MM3_TO_M3 @property - def area(self) -> MM2: - """Total cross-sectional area of the steel element [mm²].""" - return self.polygon.area + def weight_per_meter(self) -> KG_M: + """ + Calculate the weight per meter of the steel element. - @property - def centroid(self) -> Point: - """Centroid of the steel cross-section.""" - return self.polygon.centroid + Returns + ------- + KG_M + The weight per meter of the steel element. + """ + return sum(element.weight_per_meter for element in self.elements) @property def moment_of_inertia_about_y(self) -> KG_M: """Moment of inertia about the y-axis per meter length [mm⁴].""" - body_moments_of_inertia = sum(element.moment_of_inertia_about_y for element in self.elements) - parallel_axis_theorem = sum(element.area * (element.centroid.y - self.centroid.y) ** 2 for element in self.elements) + body_moments_of_inertia = sum(element.cross_section.moment_of_inertia_about_y for element in self.elements) + parallel_axis_theorem = sum( + element.cross_section.area * (element.cross_section.centroid.y - self.centroid.y) ** 2 for element in self.elements + ) return body_moments_of_inertia + parallel_axis_theorem @property def moment_of_inertia_about_z(self) -> KG_M: """Moment of inertia about the z-axis per meter length [mm⁴].""" - body_moments_of_inertia = sum(element.moment_of_inertia_about_z for element in self.elements) - parallel_axis_theorem = sum(element.area * (element.centroid.x - self.centroid.x) ** 2 for element in self.elements) + body_moments_of_inertia = sum(element.cross_section.moment_of_inertia_about_z for element in self.elements) + parallel_axis_theorem = sum( + element.cross_section.area * (element.cross_section.centroid.x - self.centroid.x) ** 2 for element in self.elements + ) return body_moments_of_inertia + parallel_axis_theorem @property def elastic_section_modulus_about_y_positive(self) -> KG_M: """Elastic section modulus about the y-axis on the positive z side [mm³].""" - distance_to_top = max(y for element in self.elements for _, y in element.geometry.points) - self.centroid.y + distance_to_top = max(y for _, y in self.polygon.exterior.coords) - self.centroid.y return self.moment_of_inertia_about_y / distance_to_top @property def elastic_section_modulus_about_y_negative(self) -> KG_M: """Elastic section modulus about the y-axis on the negative z side [mm³].""" - distance_to_bottom = self.centroid.y - min(y for element in self.elements for _, y in element.geometry.points) + distance_to_bottom = self.centroid.y - min(y for _, y in self.polygon.exterior.coords) return self.moment_of_inertia_about_y / distance_to_bottom @property def elastic_section_modulus_about_z_positive(self) -> KG_M: """Elastic section modulus about the z-axis on the positive y side [mm³].""" - distance_to_right = max(x for element in self.elements for x, _ in element.geometry.points) - self.centroid.x + distance_to_right = max(x for x, _ in self.polygon.exterior.coords) - self.centroid.x return self.moment_of_inertia_about_z / distance_to_right @property def elastic_section_modulus_about_z_negative(self) -> KG_M: """Elastic section modulus about the z-axis on the negative y side [mm³].""" - distance_to_left = self.centroid.x - min(x for element in self.elements for x, _ in element.geometry.points) + distance_to_left = self.centroid.x - min(x for x, _ in self.polygon.exterior.coords) return self.moment_of_inertia_about_z / distance_to_left - def geometry( - self, - mesh_size: MM | None = None, - ) -> Geometry: - """Return the geometry of the cross-section. + @property + def plastic_section_modulus_about_y(self) -> MM3 | None: + """Plastic section modulus about the y-axis [mm³].""" + return self.section_properties().sxx + + @property + def plastic_section_modulus_about_z(self) -> MM3 | None: + """Plastic section modulus about the z-axis [mm³].""" + return self.section_properties().syy + + @property + def yield_strength(self) -> MPA: + """ + Calculate the yield strength of the steel element. + + This is the minimum yield strength of all elements in the cross-section. - Properties - ---------- - mesh_size : MM - Maximum mesh element area to be used within - the Geometry-object finite-element mesh. If not provided, a default value will be used. + Returns + ------- + MPa + The yield strength of the steel element. + """ + # let's find the minimum yield strength of all elements + return min(element.yield_strength for element in self.elements) + @property + def ultimate_strength(self) -> MPA: """ - if mesh_size is None: - mesh_size = 2.0 - - geom = Geometry(geom=self.polygon) - geom.create_mesh(mesh_sizes=mesh_size) - return geom - - def section(self) -> Section: - """Section object representing the cross-section.""" - return Section(geometry=self.geometry()) - - def section_properties( - self, - geometric: bool = True, - plastic: bool = True, - warping: bool = True, - ) -> SectionProperties: - """Calculate and return the section properties of the cross-section. - - Parameters - ---------- - geometric : bool - Whether to calculate geometric properties. - plastic: bool - Whether to calculate plastic properties. - warping: bool - Whether to calculate warping properties. + Calculate the ultimate strength of the steel element. + + This is the minimum ultimate strength of all elements in the cross-section. + + Returns + ------- + MPa + The ultimate strength of the steel element. """ - section = self.section() - - if any([geometric, plastic, warping]): - section.calculate_geometric_properties() - if warping: - section.calculate_warping_properties() - if plastic: - section.calculate_plastic_properties() - return section.section_props + # let's find the minimum ultimate strength of all elements + return min(element.yield_strength for element in self.elements) From 1f5c86b2a8e5923ebf9e7b4691a9842a85a87bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 13:53:26 +0200 Subject: [PATCH 42/70] Add height and width properties to steel cross-section Introduced `height` and `width` properties to calculate the cross-section's dimensions based on its polygon bounds. This enhances accessibility to geometric properties and improves usability in structural calculations. --- .../steel_cross_sections/_steel_cross_section.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index 7f31bab31..0a8a9c81e 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -10,7 +10,7 @@ from blueprints.structural_sections._cross_section import CrossSection from blueprints.structural_sections.steel.steel_element import SteelElement -from blueprints.type_alias import KG_M, M3_M, MM3, MPA +from blueprints.type_alias import KG_M, M3_M, MM, MM3, MPA from blueprints.unit_conversion import MM3_TO_M3 @@ -53,6 +53,16 @@ def polygon(self) -> Polygon: # Ensure consistent orientation return orient(combined_polygon) + @property + def height(self) -> MM: + """Height of the cross-section [mm].""" + return self.polygon.bounds[3] - self.polygon.bounds[1] + + @property + def width(self) -> MM: + """Width of the cross-section [mm].""" + return self.polygon.bounds[2] - self.polygon.bounds[0] + @property def volume_per_meter(self) -> M3_M: """Total volume of the reinforced cross-section per meter length [m³/m].""" From 480a459a3bac9317d5afe0de6a8625234a5d5ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 13:53:40 +0200 Subject: [PATCH 43/70] Optimize steel section plotter for combined cross-sections Updated the plotter to handle `CombinedSteelCrossSection` with improved dimension labeling and refined annotations. Enhanced legend text formatting and added dynamic offset handling for better visualization. Simplified bounds calculations and streamlined the plotting logic. --- .../plotters/general_steel_plotter.py | 128 +++++++++--------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py index d29cf365b..9b34905c6 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py @@ -1,20 +1,18 @@ """Defines a general steel plotter for cross sections and its characteristics.""" -# ruff: noqa: PLR0913, F821 import matplotlib.pyplot as plt from matplotlib import patches as mplpatches from matplotlib.patches import Polygon as MplPolygon from shapely.geometry import Point -from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import SteelCrossSection -from blueprints.structural_sections.steel.steel_element import SteelElement +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import CombinedSteelCrossSection # Define color STEEL_COLOR = (0.683, 0.0, 0.0) def plot_shapes( - profile: SteelCrossSection, + profile: CombinedSteelCrossSection, figsize: tuple[float, float] = (15.0, 8.0), title: str = "", font_size_title: float = 18.0, @@ -43,34 +41,41 @@ def plot_shapes( for element in profile.elements: # Plot the exterior polygon - x, y = element.polygon.exterior.xy + x, y = element.cross_section.polygon.exterior.xy patch = MplPolygon(xy=list(zip(x, y)), lw=1, fill=True, facecolor=STEEL_COLOR, edgecolor=STEEL_COLOR) ax.add_patch(patch) # Plot the interior polygons (holes) if any - for interior in element.polygon.interiors: + for interior in element.cross_section.polygon.interiors: x, y = interior.xy patch = MplPolygon(xy=list(zip(x, y)), lw=0, fill=True, facecolor="white") ax.add_patch(patch) # Add dimension lines and centroid - _add_dimension_lines(ax, profile.elements, profile.centroid) + _add_dimension_lines(ax=ax, profile=profile, centroid=profile.centroid) ax.plot(profile.centroid.x, profile.centroid.y, "o", color="black") # Add legend text - legend_text = f"Total area: {profile.area:.1f} mm²\n" - legend_text += f"Weight per meter: {profile.steel_weight_per_meter:.1f} kg/m\n" - legend_text += f"Moment of inertia about y: {profile.moment_of_inertia_about_y:.0f} mm⁴\n" - legend_text += f"Moment of inertia about z: {profile.moment_of_inertia_about_z:.0f} mm⁴\n" - legend_text += f"Steel quality: {profile.steel_material.name}\n" - - ax.text( - x=0.05, - y=0.95, - s=legend_text, + legend_text = f""" +{profile.name}\n +Area: {profile.area:.1f} mm² +Weight per meter: {profile.weight_per_meter:.1f} kg/m +Moment of inertia about y: {profile.moment_of_inertia_about_y:.0f} mm⁴ +Moment of inertia about z: {profile.moment_of_inertia_about_z:.0f} mm⁴ +""" + + # Add the steel quality if all elements have the same material + if len({element.material.name for element in profile.elements}) == 1: + legend_text += f"Steel quality: {profile.elements[0].material.name}\n" + + # Get the boundaries of the plot + _, min_y, max_x, _ = profile.polygon.bounds + + # Add the legend text to the plot + ax.annotate( + xy=(max_x + 10, min_y), + text=legend_text, transform=ax.transAxes, - verticalalignment="top", - horizontalalignment="left", fontsize=font_size_legend, ) @@ -85,32 +90,29 @@ def plot_shapes( plt.show() # pragma: no cover assert fig is not None + return fig -def _add_dimension_lines(ax: plt.Axes, elements: list[SteelElement], centroid: Point) -> None: +def _add_dimension_lines(ax: plt.Axes, profile: CombinedSteelCrossSection, centroid: Point) -> None: """Adds dimension lines to show the outer dimensions of the geometry. Parameters ---------- ax : plt.Axes The matplotlib axes to draw on. - elements : tuple[CrossSection, ...] + profile : tuple[CrossSection, ...] The cross-sections to plot. centroid : Point The centroid of the cross-section. """ + # Define the offset for the dimension lines + offset_dimension_lines = max(profile.height, profile.width) / 20 + # Calculate the bounds of all elements in the geometry - min_x, min_y, max_x, max_y = float("inf"), float("inf"), float("-inf"), float("-inf") - for element in elements: - bounds = element.polygon.bounds - min_x = min(min_x, bounds[0]) - min_y = min(min_y, bounds[1]) - max_x = max(max_x, bounds[2]) - max_y = max(max_y, bounds[3]) - - width = max_x - min_x - height = max_y - min_y + min_x, min_y, max_x, max_y = profile.polygon.bounds + + # Calculate the width and height of the geometry relative to the centroid centroid_width = centroid.x - min_x centroid_height = centroid.y - min_y @@ -118,73 +120,73 @@ def _add_dimension_lines(ax: plt.Axes, elements: list[SteelElement], centroid: P diameter_line_style = { "arrowstyle": mplpatches.ArrowStyle(stylename="<->", head_length=0.5, head_width=0.5), } - offset_width = max(height, width) / 20 + + # HORIZONTAL DIMENSION LINES (BELOW THE GEOMETRY) + # Add the width dimension lines (below the geometry) ax.annotate( text="", - xy=(min_x, min_y - offset_width), - xytext=(max_x, min_y - offset_width), + xy=(min_x, min_y - offset_dimension_lines * 2), + xytext=(max_x, min_y - offset_dimension_lines * 2), verticalalignment="center", horizontalalignment="center", arrowprops=diameter_line_style, annotation_clip=False, ) ax.text( - s=f"{width:.1f} mm", + s=f"b= {profile.width:.1f} mm", x=(min_x + max_x) / 2, - y=min_y - offset_width - 1, + y=min_y - offset_dimension_lines * 2 + 6, verticalalignment="top", horizontalalignment="center", fontsize=10, ) - # Add the height dimension line (on the right side of the geometry) - offset_height = offset_width + # Add the height dimension lines (on the left side of the geometry) ax.annotate( text="", - xy=(max_x + offset_height, max_y), - xytext=(max_x + offset_height, min_y), + xy=(min_x, min_y - offset_dimension_lines), + xytext=(centroid.x, min_y - offset_dimension_lines), verticalalignment="center", horizontalalignment="center", arrowprops=diameter_line_style, - rotation=90, annotation_clip=False, ) ax.text( - s=f"{height:.1f} mm", - x=max_x + offset_height + 1 + height / 200, - y=(min_y + max_y) / 2, - verticalalignment="center", - horizontalalignment="left", + s=f"{centroid_width:.1f} mm", + x=(min_x + centroid.x) / 2, + y=min_y - offset_dimension_lines + 6, + verticalalignment="top", + horizontalalignment="center", fontsize=10, - rotation=90, ) - # Add the distance from the left to the centroid (below the geometry, double offset) - offset_centroid_left_bottom = 2 * offset_width + # HEIGHT DIMENSION LINES (ON THE LEFT SIDE OF THE GEOMETRY) + # Add the distance from the bottom to the centroid (on the left side) ax.annotate( text="", - xy=(min_x, min_y - offset_centroid_left_bottom), - xytext=(centroid.x, min_y - offset_centroid_left_bottom), + xy=(-max_x - offset_dimension_lines, min_y), + xytext=(-max_x - offset_dimension_lines, centroid.y), verticalalignment="center", horizontalalignment="center", arrowprops=diameter_line_style, + rotation=90, annotation_clip=False, ) ax.text( - s=f"{centroid_width:.1f} mm", - x=(min_x + centroid.x) / 2, - y=min_y - offset_centroid_left_bottom - 1, - verticalalignment="top", - horizontalalignment="center", + s=f"{centroid_height:.1f} mm", + x=-(max_x + offset_dimension_lines + 6), + y=(min_y + centroid.y) / 2, + verticalalignment="center", + horizontalalignment="left", fontsize=10, + rotation=90, ) - # Add the distance from the bottom to the centroid (on the right side, double offset) - offset_centroid_bottom_right = 2 * offset_height + # # Add the height dimension line (on the left side of the geometry) ax.annotate( text="", - xy=(max_x + offset_centroid_bottom_right, min_y), - xytext=(max_x + offset_centroid_bottom_right, centroid.y), + xy=(-max_x - offset_dimension_lines * 2, max_y), + xytext=(-max_x - offset_dimension_lines * 2, min_y), verticalalignment="center", horizontalalignment="center", arrowprops=diameter_line_style, @@ -192,9 +194,9 @@ def _add_dimension_lines(ax: plt.Axes, elements: list[SteelElement], centroid: P annotation_clip=False, ) ax.text( - s=f"{centroid_height:.1f} mm", - x=max_x + offset_centroid_bottom_right + 1 + height / 200, - y=(min_y + centroid.y) / 2, + s=f"h= {profile.height:.1f} mm", + x=-(max_x + offset_dimension_lines * 2 + 6), + y=(min_y + max_y) / 2, verticalalignment="center", horizontalalignment="left", fontsize=10, From 654446c50d5b7b66084670d13b38b0eec19c62e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 13:54:02 +0200 Subject: [PATCH 44/70] Refactor I-Profile steel section with dataclass and enhancements Transitioned `IProfile` to use `@dataclass` for cleaner initialization and added `from_standard_profile` class method for streamlined creation of standard profiles. Refactored plotting and simplified corrosion handling logic while improving flexibility and readability. --- .../steel/steel_cross_sections/i_profile.py | 252 +++++++++--------- 1 file changed, 121 insertions(+), 131 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py index d22946333..e0a49de02 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py @@ -1,12 +1,15 @@ """I-Profile steel section.""" +from collections.abc import Callable +from dataclasses import dataclass +from typing import Self + from matplotlib import pyplot as plt -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.cross_section_quarter_circular_spandrel import QuarterCircularSpandrelCrossSection from blueprints.structural_sections.cross_section_rectangle import RectangularCrossSection -from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import CombinedSteelCrossSection from blueprints.structural_sections.steel.steel_cross_sections.plotters.general_steel_plotter import plot_shapes from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.hea import HEA from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB @@ -16,11 +19,14 @@ from blueprints.type_alias import MM -class ISteelProfile(SteelCrossSection): +@dataclass(kw_only=True) +class ISteelProfile(CombinedSteelCrossSection): """Representation of an I-Profile steel section. Parameters ---------- + steel_material : SteelMaterial + Steel material properties for the profile. top_flange_width : MM The width of the top flange [mm]. top_flange_thickness : MM @@ -33,57 +39,49 @@ class ISteelProfile(SteelCrossSection): The total height of the profile [mm]. web_thickness : MM The thickness of the web [mm]. - steel_class : SteelStrengthClass - The steel strength class of the profile. top_radius : MM | None The radius of the curved corners of the top flange. Default is None, the corner radius is then taken as the thickness. bottom_radius : MM | None The radius of the curved corners of the bottom flange. Default is None, the corner radius is then taken as the thickness. """ - def __init__( - self, - top_flange_width: MM, - top_flange_thickness: MM, - bottom_flange_width: MM, - bottom_flange_thickness: MM, - total_height: MM, - web_thickness: MM, - steel_class: SteelStrengthClass, - top_radius: MM | None = None, - bottom_radius: MM | None = None, - ) -> None: + steel_material: SteelMaterial + top_flange_width: MM + top_flange_thickness: MM + bottom_flange_width: MM + bottom_flange_thickness: MM + total_height: MM + web_thickness: MM + top_radius: MM | None = None + bottom_radius: MM | None = None + name: str = "I-Profile" + + def __post_init__(self) -> None: """Initialize the I-profile steel section.""" - self.top_flange_width = top_flange_width - self.top_flange_thickness = top_flange_thickness - self.bottom_flange_width = bottom_flange_width - self.bottom_flange_thickness = bottom_flange_thickness - self.total_height = total_height - self.web_thickness = web_thickness - self.top_radius = top_radius if top_radius is not None else top_flange_thickness - self.bottom_radius = bottom_radius if bottom_radius is not None else bottom_flange_thickness + self.top_radius = self.top_radius if self.top_radius is not None else self.top_flange_thickness + self.bottom_radius = self.bottom_radius if self.bottom_radius is not None else self.bottom_flange_thickness # Calculate web height - self.web_height = total_height - top_flange_thickness - bottom_flange_thickness + self.web_height = self.total_height - self.top_flange_thickness - self.bottom_flange_thickness # Create the cross-sections for the flanges and web self.top_flange = RectangularCrossSection( name="Top Flange", - width=top_flange_width, - height=top_flange_thickness, + width=self.top_flange_width, + height=self.top_flange_thickness, x=0, - y=(self.web_height + top_flange_thickness) / 2, + y=(self.web_height + self.top_flange_thickness) / 2, ) self.bottom_flange = RectangularCrossSection( name="Bottom Flange", - width=bottom_flange_width, - height=bottom_flange_thickness, + width=self.bottom_flange_width, + height=self.bottom_flange_thickness, x=0, - y=-(self.web_height + bottom_flange_thickness) / 2, + y=-(self.web_height + self.bottom_flange_thickness) / 2, ) self.web = RectangularCrossSection( name="Web", - width=web_thickness, + width=self.web_thickness, height=self.web_height, x=0, y=0, @@ -93,7 +91,7 @@ def __init__( self.curve_top_right = QuarterCircularSpandrelCrossSection( name="Curve top right", radius=self.top_radius, - x=web_thickness / 2, + x=self.web_thickness / 2, y=self.web_height / 2, mirrored_horizontally=False, mirrored_vertically=True, @@ -101,7 +99,7 @@ def __init__( self.curve_top_left = QuarterCircularSpandrelCrossSection( name="Curve top left", radius=self.top_radius, - x=-web_thickness / 2, + x=-self.web_thickness / 2, y=self.web_height / 2, mirrored_horizontally=True, mirrored_vertically=True, @@ -109,7 +107,7 @@ def __init__( self.curve_bottom_right = QuarterCircularSpandrelCrossSection( name="Curve bottom right", radius=self.bottom_radius, - x=web_thickness / 2, + x=self.web_thickness / 2, y=-self.web_height / 2, mirrored_horizontally=False, mirrored_vertically=False, @@ -117,124 +115,116 @@ def __init__( self.curve_bottom_left = QuarterCircularSpandrelCrossSection( name="Curve bottom left", radius=self.bottom_radius, - x=-web_thickness / 2, + x=-self.web_thickness / 2, y=-self.web_height / 2, mirrored_horizontally=True, mirrored_vertically=False, ) - # material properties - self.steel_material = SteelMaterial(steel_class=steel_class) - # Create the steel elements self.elements = [ - SteelElement(cross_section=self.top_flange, material=self.steel_material, nominal_thickness=top_flange_thickness), - SteelElement(cross_section=self.bottom_flange, material=self.steel_material, nominal_thickness=bottom_flange_thickness), - SteelElement(cross_section=self.web, material=self.steel_material, nominal_thickness=web_thickness), - SteelElement(cross_section=self.curve_top_right, material=self.steel_material, nominal_thickness=top_flange_thickness), - SteelElement(cross_section=self.curve_top_left, material=self.steel_material, nominal_thickness=top_flange_thickness), - SteelElement(cross_section=self.curve_bottom_right, material=self.steel_material, nominal_thickness=bottom_flange_thickness), - SteelElement(cross_section=self.curve_bottom_left, material=self.steel_material, nominal_thickness=bottom_flange_thickness), + SteelElement( + cross_section=self.top_flange, + material=self.steel_material, + nominal_thickness=self.top_flange_thickness, + ), + SteelElement( + cross_section=self.bottom_flange, + material=self.steel_material, + nominal_thickness=self.bottom_flange_thickness, + ), + SteelElement( + cross_section=self.web, + material=self.steel_material, + nominal_thickness=self.web_thickness, + ), + SteelElement( + cross_section=self.curve_top_right, + material=self.steel_material, + nominal_thickness=self.top_flange_thickness, + ), + SteelElement( + cross_section=self.curve_top_left, + material=self.steel_material, + nominal_thickness=self.top_flange_thickness, + ), + SteelElement( + cross_section=self.curve_bottom_right, + material=self.steel_material, + nominal_thickness=self.bottom_flange_thickness, + ), + SteelElement( + cross_section=self.curve_bottom_left, + material=self.steel_material, + nominal_thickness=self.bottom_flange_thickness, + ), ] - def plot(self, *args, **kwargs) -> plt.Figure: - """Plot the cross-section. Making use of the standard plotter. - - Parameters - ---------- - *args - Additional arguments passed to the plotter. - **kwargs - Additional keyword arguments passed to the plotter. - """ - return plot_shapes( - self, - *args, - **kwargs, - ) - - -class LoadStandardIProfile: - r"""Class to load in values for standard I profile. - - Parameters - ---------- - profile: ISteelProfile - Representation of an I-Profile steel section - steel_class: SteelStrengthClass - Enumeration of steel strength classes (default: S355) - """ - - def __init__( - self, + @classmethod + def from_standard_profile( + cls, profile: HEA | HEB | HEM | IPE, - steel_class: SteelStrengthClass = SteelStrengthClass.S355, - ) -> None: - self.profile = profile - self.steel_class = steel_class - - def __str__(self) -> str: - """Return the steel class and profile.""" - return f"Steel class: {self.steel_class}, Profile: {self.profile}" + steel_material: SteelMaterial, + corrosion: MM = 0, + ) -> Self: + """Create an I-profile from a set of standard profiles already defined in Blueprints. - def top_flange_width(self) -> MM: - """Return the top flange width of the I-profile.""" - return self.profile.top_flange_width - - def top_flange_thickness(self) -> MM: - """Return the top flange thickness of the I-profile.""" - return self.profile.top_flange_thickness - - def bottom_flange_width(self) -> MM: - """Return the bottom flange width of the I-profile.""" - return self.profile.bottom_flange_width - - def bottom_flange_thickness(self) -> MM: - """Return the bottom flange thickness of the I-profile.""" - return self.profile.bottom_flange_thickness - - def total_height(self) -> MM: - """Return the total height of the I-profile.""" - return self.profile.total_height - - def web_thickness(self) -> MM: - """Return the web thickness of the I-profile.""" - return self.profile.web_thickness - - def top_radius(self) -> MM: - """Return the top radius of the I-profile.""" - return self.profile.top_radius - - def bottom_radius(self) -> MM: - """Return the bottom radius of the I-profile.""" - return self.profile.bottom_radius - - def get_profile(self, corrosion: MM = 0) -> ISteelProfile: - """Return the CHS profile. + Blueprints offers standard profiles for HEA, HEB, HEM, and IPE. This method allows you to create an I-profile. Parameters ---------- + profile : HEA | HEB | HEM | IPE + Any of the standard profiles defined in Blueprints. + steel_material : SteelMaterial + Steel material properties for the profile. corrosion : MM, optional Corrosion thickness per side (default is 0). """ - top_flange_width = self.top_flange_width() - corrosion * 2 - top_flange_thickness = self.top_flange_thickness() - corrosion * 2 - bottom_flange_width = self.bottom_flange_width() - corrosion * 2 - bottom_flange_thickness = self.bottom_flange_thickness() - corrosion * 2 - total_height = self.total_height() - corrosion * 2 - web_thickness = self.web_thickness() - corrosion * 2 - - if top_flange_thickness <= 0 or bottom_flange_thickness <= 0 or web_thickness <= 0: + top_flange_width = profile.top_flange_width - corrosion * 2 + top_flange_thickness = profile.top_flange_thickness - corrosion * 2 + bottom_flange_width = profile.bottom_flange_width - corrosion * 2 + bottom_flange_thickness = profile.bottom_flange_thickness - corrosion * 2 + total_height = profile.total_height - corrosion * 2 + web_thickness = profile.web_thickness - corrosion * 2 + + if any( + [ + top_flange_thickness < 1e-3, + bottom_flange_thickness < 1e-3, + web_thickness < 1e-3, + ] + ): raise ValueError("The profile has fully corroded.") - return ISteelProfile( + return cls( top_flange_width=top_flange_width, top_flange_thickness=top_flange_thickness, bottom_flange_width=bottom_flange_width, bottom_flange_thickness=bottom_flange_thickness, total_height=total_height, web_thickness=web_thickness, - steel_class=self.steel_class, - top_radius=self.top_radius(), - bottom_radius=self.bottom_radius(), + steel_material=steel_material, + top_radius=profile.top_radius, + bottom_radius=profile.bottom_radius, + name=profile.alias, + ) + + def plot(self, plotter: Callable[..., plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: + """Plot the cross-section. Making use of the standard plotter. + + Parameters + ---------- + plotter : Callable[..., plt.Figure] | None + The plotter function to use. If None, the default Blueprints plotter for steel sections is used. + *args + Additional arguments passed to the plotter. + **kwargs + Additional keyword arguments passed to the plotter. + """ + if plotter is None: + plotter = plot_shapes + return plotter( + self, + *args, + **kwargs, ) From 16c83883dd44f34c7e4c68ced6b5c0b01bac38d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 14:37:14 +0200 Subject: [PATCH 45/70] Adjust text positioning offsets in steel plotter. Replaced hardcoded values with offset calculations for text positioning to improve readability and maintain consistency. This ensures dynamic adjustments based on profile dimensions, enhancing plot accuracy and layout. --- .../plotters/general_steel_plotter.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py index 9b34905c6..c97ccb303 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py @@ -69,11 +69,12 @@ def plot_shapes( legend_text += f"Steel quality: {profile.elements[0].material.name}\n" # Get the boundaries of the plot - _, min_y, max_x, _ = profile.polygon.bounds + _, min_y, _, _ = profile.polygon.bounds + offset = (profile.width / 2) + (profile.width / 20) # Add the legend text to the plot ax.annotate( - xy=(max_x + 10, min_y), + xy=(offset, min_y), text=legend_text, transform=ax.transAxes, fontsize=font_size_legend, @@ -108,6 +109,7 @@ def _add_dimension_lines(ax: plt.Axes, profile: CombinedSteelCrossSection, centr """ # Define the offset for the dimension lines offset_dimension_lines = max(profile.height, profile.width) / 20 + offset_text = offset_dimension_lines / 2 # Calculate the bounds of all elements in the geometry min_x, min_y, max_x, max_y = profile.polygon.bounds @@ -135,7 +137,7 @@ def _add_dimension_lines(ax: plt.Axes, profile: CombinedSteelCrossSection, centr ax.text( s=f"b= {profile.width:.1f} mm", x=(min_x + max_x) / 2, - y=min_y - offset_dimension_lines * 2 + 6, + y=min_y - offset_dimension_lines * 2 + offset_text, verticalalignment="top", horizontalalignment="center", fontsize=10, @@ -154,7 +156,7 @@ def _add_dimension_lines(ax: plt.Axes, profile: CombinedSteelCrossSection, centr ax.text( s=f"{centroid_width:.1f} mm", x=(min_x + centroid.x) / 2, - y=min_y - offset_dimension_lines + 6, + y=min_y - offset_dimension_lines + offset_text, verticalalignment="top", horizontalalignment="center", fontsize=10, @@ -174,7 +176,7 @@ def _add_dimension_lines(ax: plt.Axes, profile: CombinedSteelCrossSection, centr ) ax.text( s=f"{centroid_height:.1f} mm", - x=-(max_x + offset_dimension_lines + 6), + x=-(max_x + offset_dimension_lines + offset_text), y=(min_y + centroid.y) / 2, verticalalignment="center", horizontalalignment="left", @@ -195,7 +197,7 @@ def _add_dimension_lines(ax: plt.Axes, profile: CombinedSteelCrossSection, centr ) ax.text( s=f"h= {profile.height:.1f} mm", - x=-(max_x + offset_dimension_lines * 2 + 6), + x=-(max_x + offset_dimension_lines * 2 + offset_text), y=(min_y + max_y) / 2, verticalalignment="center", horizontalalignment="left", From 119d9dfa0ba5b494d07324b2b744d9aedaa9ac33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 14:37:58 +0200 Subject: [PATCH 46/70] Refine `plot` method type hints and add example usage. Updated type hints for the `plot` method to specify the input type explicitly, improving code clarity and type checking. Added an example usage under `__main__` to demonstrate creating and plotting an `ISteelProfile`. --- .../steel/steel_cross_sections/i_profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py index e0a49de02..ba109bf7e 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py @@ -209,12 +209,12 @@ def from_standard_profile( name=profile.alias, ) - def plot(self, plotter: Callable[..., plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: + def plot(self, plotter: Callable[[CombinedSteelCrossSection], plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: """Plot the cross-section. Making use of the standard plotter. Parameters ---------- - plotter : Callable[..., plt.Figure] | None + plotter : Callable[CombinedSteelCrossSection, plt.Figure] | None The plotter function to use. If None, the default Blueprints plotter for steel sections is used. *args Additional arguments passed to the plotter. From 1e7649bc477d8bbb3e287b9d8ddcb512c9b02f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 14:38:25 +0200 Subject: [PATCH 47/70] Refactor CHSSteelProfile for immutability and enhanced flexibility Converted `CHSSteelProfile` to use a dataclass for immutability and clearer attribute definitions. Introduced a `from_standard_profile` class method for constructing profiles from predefined standards. Simplified plotting functionality and removed redundant `LoadStandardCHS` class. --- .../steel/steel_cross_sections/chs_profile.py | 139 ++++++++---------- 1 file changed, 62 insertions(+), 77 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py index b5f0934c1..90ae9716d 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py @@ -1,40 +1,41 @@ """Circular Hollow Section (CHS) steel profile.""" +from collections.abc import Callable +from dataclasses import dataclass +from typing import Self + from matplotlib import pyplot as plt -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.cross_section_tube import TubeCrossSection -from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import CombinedSteelCrossSection from blueprints.structural_sections.steel.steel_cross_sections.plotters.general_steel_plotter import plot_shapes from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS from blueprints.structural_sections.steel.steel_element import SteelElement from blueprints.type_alias import MM -class CHSSteelProfile(SteelCrossSection): +@dataclass(kw_only=True) +class CHSSteelProfile(CombinedSteelCrossSection): """Representation of a Circular Hollow Section (CHS) steel profile. - Parameters + Attributes ---------- outer_diameter : MM The outer diameter of the CHS profile [mm]. wall_thickness : MM The wall thickness of the CHS profile [mm]. - steel_class : SteelStrengthClass - The steel strength class of the profile. + steel_material : SteelMaterial + The material properties of the steel. """ - def __init__( - self, - outer_diameter: MM, - wall_thickness: MM, - steel_class: SteelStrengthClass, - ) -> None: - """Initialize the CHS steel profile.""" - self.thickness = wall_thickness - self.outer_diameter = outer_diameter - self.inner_diameter = outer_diameter - 2 * wall_thickness + steel_material: SteelMaterial + outer_diameter: MM + wall_thickness: MM + + def __post_init__(self) -> None: + """Initialize the CHS profile.""" + self.inner_diameter = self.outer_diameter - 2 * self.wall_thickness self.chs = TubeCrossSection( name="Ring", @@ -43,67 +44,30 @@ def __init__( x=0, y=0, ) - self.steel_material = SteelMaterial(steel_class=steel_class) - self.elements = [SteelElement(cross_section=self.chs, material=self.steel_material, nominal_thickness=self.thickness)] - - def plot(self, *args, **kwargs) -> plt.Figure: - """Plot the cross-section. Making use of the standard plotter. - - Parameters - ---------- - *args - Additional arguments passed to the plotter. - **kwargs - Additional keyword arguments passed to the plotter. - """ - return plot_shapes( - self, - *args, - **kwargs, - ) - - -class LoadStandardCHS: - r"""Class to load in values for standard CHS profile. - - Parameters - ---------- - profile: CHS - Enumeration of standard CHS profiles - steel_class: SteelStrengthClass - Enumeration of steel strength classes (default: S355) - - """ - - def __init__( - self, + self.elements = [ + SteelElement( + cross_section=self.chs, + material=self.steel_material, + nominal_thickness=self.wall_thickness, + ) + ] + + @classmethod + def from_standard_profile( + cls, profile: CHS, - steel_class: SteelStrengthClass = SteelStrengthClass.S355, - ) -> None: - self.profile = profile - self.steel_class = steel_class - - def __str__(self) -> str: - """Return the steel class and profile.""" - return f"Steel class: {self.steel_class}, Profile: {self.profile}" - - def alias(self) -> str: - """Return the alias of the CHS profile.""" - return self.profile.alias - - def diameter(self) -> MM: - """Return the outer diameter of the CHS profile.""" - return self.profile.diameter - - def thickness(self) -> MM: - """Return the wall thickness of the CHS profile.""" - return self.profile.thickness - - def get_profile(self, corrosion_outside: MM = 0, corrosion_inside: MM = 0) -> CHSSteelProfile: - """Return the CHS profile with optional corrosion adjustments. + steel_material: SteelMaterial, + corrosion_outside: MM = 0, + corrosion_inside: MM = 0, + ) -> Self: + """Create a CHS profile from a set of standard profiles already defined in Blueprints. Parameters ---------- + profile : CHS + Any of the standard CHS profiles defined in Blueprints. + steel_material : SteelMaterial + Steel material properties for the profile. corrosion_outside : MM, optional Corrosion thickness to be subtracted from the outer diameter [mm] (default: 0). corrosion_inside : MM, optional @@ -114,14 +78,35 @@ def get_profile(self, corrosion_outside: MM = 0, corrosion_inside: MM = 0) -> CH CHSSteelProfile The adjusted CHS steel profile considering corrosion effects. """ - adjusted_outer_diameter = self.diameter() - 2 * corrosion_outside - adjusted_thickness = self.thickness() - corrosion_outside - corrosion_inside + adjusted_outer_diameter = profile.diameter - 2 * corrosion_outside + adjusted_thickness = profile.thickness - corrosion_outside - corrosion_inside if adjusted_thickness <= 0: raise ValueError("The profile has fully corroded.") - return CHSSteelProfile( + return cls( outer_diameter=adjusted_outer_diameter, wall_thickness=adjusted_thickness, - steel_class=self.steel_class, + steel_material=steel_material, + name=profile.alias, + ) + + def plot(self, plotter: Callable[[CombinedSteelCrossSection], plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: + """Plot the cross-section. Making use of the standard plotter. + + Parameters + ---------- + plotter : Callable[CombinedSteelCrossSection, plt.Figure] | None + The plotter function to use. If None, the default Blueprints plotter for steel sections is used. + *args + Additional arguments passed to the plotter. + **kwargs + Additional keyword arguments passed to the plotter. + """ + if plotter is None: + plotter = plot_shapes + return plotter( + self, + *args, + **kwargs, ) From 69eddb92369fb86602510e2a32e2fdec727ec773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:09:10 +0200 Subject: [PATCH 48/70] Set x-axis limits in steel profile plotter. Added x-axis limits to the plotter using the profile's polygon bounds with an offset. This ensures consistent visualization and improves clarity in generated plots. --- .../steel/steel_cross_sections/plotters/general_steel_plotter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py index c97ccb303..6ab05af2c 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/plotters/general_steel_plotter.py @@ -86,6 +86,7 @@ def plot_shapes( ax.grid(True) ax.axis("equal") ax.axis("off") + ax.set_xlim(profile.polygon.bounds[0] - offset, profile.polygon.bounds[2] + offset) if show: plt.show() # pragma: no cover From 61e43a5ced6354dc6dc88d471ed6db8145ab8d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:09:39 +0200 Subject: [PATCH 49/70] Reorder class docstring attributes for clarity Rearranged the attribute order in the docstring to improve readability and maintain consistency with typical documentation standards. This ensures better comprehension for users and developers navigating the code. --- .../steel/steel_cross_sections/chs_profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py index 90ae9716d..ad6958faf 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py @@ -21,12 +21,12 @@ class CHSSteelProfile(CombinedSteelCrossSection): Attributes ---------- + steel_material : SteelMaterial + The material properties of the steel. outer_diameter : MM The outer diameter of the CHS profile [mm]. wall_thickness : MM The wall thickness of the CHS profile [mm]. - steel_material : SteelMaterial - The material properties of the steel. """ steel_material: SteelMaterial From c53d9d72b2fe4560175313961cd59cfbad43e02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:10:47 +0200 Subject: [PATCH 50/70] Enhance docstring for ISteelProfile class. Improve class documentation by adding usage examples and clarifying the distinction between custom I-profiles and standard profiles. This update provides better guidance on using the `from_standard_profile` method effectively. --- .../steel/steel_cross_sections/i_profile.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py index ba109bf7e..90f71ca93 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py @@ -22,8 +22,15 @@ @dataclass(kw_only=True) class ISteelProfile(CombinedSteelCrossSection): """Representation of an I-Profile steel section. + This can be used to create a custom I-profile or to create an I-profile from a standard profile. - Parameters + For standard profiles, use the `from_standard_profile` class method. + For example, + ```python + i_profile = ISteelProfile.from_standard_profile(profile=HEA.HEA200, steel_material=SteelMaterial(SteelStrengthClass.S355)) + ``` + + Attributes ---------- steel_material : SteelMaterial Steel material properties for the profile. From 3703ea115e60938ba0adf936185331eed3125535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:11:52 +0200 Subject: [PATCH 51/70] Refactor `StripSteelProfile` and simplify standard profile loading. Converted `StripSteelProfile` to a dataclass with improved initialization and profile handling. Replaced `LoadStandardStrip` with a cleaner `from_standard_profile` class method for creating instances from standard profiles, reducing redundancy and improving clarity. Enhanced the `plot` method with optional custom plotter support. --- .../steel_cross_sections/strip_profile.py | 141 +++++++++--------- 1 file changed, 68 insertions(+), 73 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py index 735d82fca..3449f5142 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py @@ -1,117 +1,112 @@ """Steel Strip Profile.""" +from collections.abc import Callable +from dataclasses import dataclass +from typing import Self + from matplotlib import pyplot as plt -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.cross_section_rectangle import RectangularCrossSection -from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import SteelCrossSection +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import CombinedSteelCrossSection from blueprints.structural_sections.steel.steel_cross_sections.plotters.general_steel_plotter import plot_shapes from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip from blueprints.structural_sections.steel.steel_element import SteelElement from blueprints.type_alias import MM -class StripSteelProfile(SteelCrossSection): +@dataclass(kw_only=True) +class StripSteelProfile(CombinedSteelCrossSection): """Representation of a Steel Strip profile. - Parameters + This class is used to create a custom steel strip profile or to create a steel strip profile from a standard profile. + For standard profiles, use the `from_standard_profile` class method. + For example, + ```python + strip_profile = StripSteelProfile.from_standard_profile(profile=Strip.STRIP160x5, steel_material=SteelMaterial(SteelStrengthClass.S355)) + ``` + + Attributes ---------- + steel_material : SteelMaterial + Steel material properties for the profile. width : MM The width of the strip profile [mm]. height : MM The height (thickness) of the strip profile [mm]. - steel_class : SteelStrengthClass - The steel strength class of the profile. """ - def __init__( - self, - width: MM, - height: MM, - steel_class: SteelStrengthClass, - ) -> None: + steel_material: SteelMaterial + strip_width: MM + strip_height: MM + + def __post_init__(self) -> None: """Initialize the Steel Strip profile.""" - self.width = width - self.height = height - self.thickness = min(width, height) # Nominal thickness is the minimum of width and height + # Nominal thickness is the minimum of width and height + self.thickness = min(self.strip_width, self.strip_height) self.strip = RectangularCrossSection( name="Steel Strip", - width=self.width, - height=self.height, + width=self.strip_width, + height=self.strip_height, x=0, y=0, ) + self.elements = [ + SteelElement( + cross_section=self.strip, + material=self.steel_material, + nominal_thickness=self.thickness, + ) + ] + + @classmethod + def from_standard_profile( + cls, + profile: Strip, + steel_material: SteelMaterial, + corrosion: MM = 0, + ) -> Self: + """Create a strip profile from a set of standard profiles already defined in Blueprints. + + Parameters + ---------- + profile : Strip + Any of the standard strip profiles defined in Blueprints. + steel_material : SteelMaterial + Steel material properties for the profile. + corrosion : MM, optional + Corrosion thickness per side (default is 0). + """ + width = profile.width - corrosion * 2 + height = profile.height - corrosion * 2 - self.steel_material = SteelMaterial(steel_class=steel_class) + if width <= 0 or height <= 0: + raise ValueError("The profile has fully corroded.") - self.elements = [SteelElement(cross_section=self.strip, material=self.steel_material, nominal_thickness=self.thickness)] + return cls( + steel_material=steel_material, + strip_width=width, + strip_height=height, + name=profile.alias, + ) - def plot(self, *args, **kwargs) -> plt.Figure: + def plot(self, plotter: Callable[[CombinedSteelCrossSection], plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: """Plot the cross-section. Making use of the standard plotter. Parameters ---------- + plotter : Callable[CombinedSteelCrossSection, plt.Figure] | None + The plotter function to use. If None, the default Blueprints plotter for steel sections is used. *args Additional arguments passed to the plotter. **kwargs Additional keyword arguments passed to the plotter. """ - return plot_shapes( + if plotter is None: + plotter = plot_shapes + return plotter( self, *args, **kwargs, ) - - -class LoadStandardStrip: - r"""Class to load in values for standard Strip profile. - - Parameters - ---------- - profile: Strip - Enumeration of standard steel strip profiles - steel_class: SteelStrengthClass - Enumeration of steel strength classes (default: S355) - """ - - def __init__( - self, - profile: Strip, - steel_class: SteelStrengthClass = SteelStrengthClass.S355, - ) -> None: - self.profile = profile - self.steel_class = steel_class - - def __str__(self) -> str: - """Return the steel class and profile.""" - return f"Steel class: {self.steel_class}, Profile: {self.profile}" - - def alias(self) -> str: - """Return the code of the strip profile.""" - return self.profile.alias - - def width(self) -> MM: - """Return the width of the strip profile.""" - return self.profile.width - - def height(self) -> MM: - """Return the height (thickness) of the strip profile.""" - return self.profile.height - - def get_profile(self, corrosion: MM = 0) -> StripSteelProfile: - """Return the strip profile. - - Parameters - ---------- - corrosion : MM, optional - Corrosion thickness per side (default is 0). - """ - width = self.width() - corrosion * 2 - height = self.height() - corrosion * 2 - - if width <= 0 or height <= 0: - raise ValueError("The profile has fully corroded.") - - return StripSteelProfile(width=width, height=height, steel_class=self.steel_class) From 7655596fe344db3fbac28a2e984f9ce18de52465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:18:41 +0200 Subject: [PATCH 52/70] Refactor steel profile fixtures to use `from_standard_profile`. Replaced `LoadStandard*` classes with direct calls to `from_standard_profile` on steel profile classes, improving clarity and consistency. Updated imports to reflect the changes and streamlined the use of `SteelMaterial` for profile creation. --- .../steel/steel_cross_sections/conftest.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/structural_sections/steel/steel_cross_sections/conftest.py b/tests/structural_sections/steel/steel_cross_sections/conftest.py index aa5e30717..df3c3249e 100644 --- a/tests/structural_sections/steel/steel_cross_sections/conftest.py +++ b/tests/structural_sections/steel/steel_cross_sections/conftest.py @@ -3,12 +3,13 @@ import pytest from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass -from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile, LoadStandardCHS -from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile, LoadStandardIProfile +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile +from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip -from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import LoadStandardStrip, StripSteelProfile +from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import StripSteelProfile @pytest.fixture @@ -16,7 +17,7 @@ def strip_profile() -> StripSteelProfile: """Fixture to set up a Strip profile for testing.""" profile = Strip.STRIP160x5 steel_class = SteelStrengthClass.S355 - return LoadStandardStrip(profile=profile, steel_class=steel_class).get_profile() + return StripSteelProfile.from_standard_profile(profile=profile, steel_material=SteelMaterial(steel_class)) @pytest.fixture @@ -24,7 +25,7 @@ def chs_profile() -> CHSSteelProfile: """Fixture to set up a CHS profile for testing.""" profile: CHS = CHS.CHS508x16 steel_class: SteelStrengthClass = SteelStrengthClass.S355 - return LoadStandardCHS(profile=profile, steel_class=steel_class).get_profile() + return CHSSteelProfile.from_standard_profile(profile=profile, steel_material=SteelMaterial(steel_class)) @pytest.fixture @@ -32,4 +33,4 @@ def i_profile() -> ISteelProfile: """Fixture to set up an I-profile for testing.""" profile = HEB.HEB360 steel_class = SteelStrengthClass.S355 - return LoadStandardIProfile(profile=profile, steel_class=steel_class).get_profile() + return ISteelProfile.from_standard_profile(profile=profile, steel_material=SteelMaterial(steel_class)) From 962bef8146af0f959fa041784694314217e2218b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:18:58 +0200 Subject: [PATCH 53/70] Refactor strip profile tests for streamlined setup and logic Replaced redundant initialization with `StripSteelProfile` factory methods to simplify test setup. Updated test logic to align with refactored attribute names and improved overall readability and maintainability of the test suite. --- .../test_strip_profile.py | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index b0daad827..d02c76315 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -4,38 +4,28 @@ import pytest from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip -from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import LoadStandardStrip, StripSteelProfile +from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import StripSteelProfile class TestStripSteelProfile: """Test suite for StripSteelProfile.""" - def test_str(self) -> None: - """Test the string representation of the Strip profile.""" - profile = Strip.STRIP160x5 - steel_class = SteelStrengthClass.S355 - desc = LoadStandardStrip(profile=profile, steel_class=steel_class).__str__() - expected_str = "Steel class: SteelStrengthClass.S355, Profile: Strip.STRIP160x5" - assert desc == expected_str - - def test_code(self) -> None: + def test_code(self, strip_profile: StripSteelProfile) -> None: """Test the code of the Strip profile.""" - profile = Strip.STRIP160x5 - steel_class = SteelStrengthClass.S355 - alias = LoadStandardStrip(profile=profile, steel_class=steel_class).alias() expected_alias = "160x5" - assert alias == expected_alias + assert strip_profile.name == expected_alias def test_steel_volume_per_meter(self, strip_profile: StripSteelProfile) -> None: """Test the steel volume per meter.""" expected_volume = 0.160 * 0.005 # m³/m - assert pytest.approx(strip_profile.steel_volume_per_meter, rel=1e-6) == expected_volume + assert pytest.approx(strip_profile.volume_per_meter, rel=1e-6) == expected_volume def test_steel_weight_per_meter(self, strip_profile: StripSteelProfile) -> None: """Test the steel weight per meter.""" expected_weight = 0.160 * 0.005 * 7850 # kg/m - assert pytest.approx(strip_profile.steel_weight_per_meter, rel=1e-6) == expected_weight + assert pytest.approx(strip_profile.weight_per_meter, rel=1e-6) == expected_weight def test_area(self, strip_profile: StripSteelProfile) -> None: """Test the steel cross-sectional area.""" @@ -103,11 +93,10 @@ def test_section_properties(self, strip_profile: StripSteelProfile) -> None: def test_get_profile_with_corrosion(self) -> None: """Test the Strip profile with 2 mm corrosion applied.""" - profile: Strip = Strip.STRIP160x5 - steel_class: SteelStrengthClass = SteelStrengthClass.S355 - - loader = LoadStandardStrip(profile=profile, steel_class=steel_class) - # Ensure the profile raises an error if fully corroded with pytest.raises(ValueError, match="The profile has fully corroded."): - loader.get_profile(corrosion=2.5) + StripSteelProfile.from_standard_profile( + profile=Strip.STRIP160x5, + steel_material=SteelMaterial(SteelStrengthClass.S355), + corrosion=2.5, + ) From 65f7e6bd1a937c2751e7482b25f79a89f58b7de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:21:21 +0200 Subject: [PATCH 54/70] Refactor strip profile tests for streamlined setup and logic Replaced redundant initialization with `StripSteelProfile` factory methods to simplify test setup. Updated test logic to align with refactored attribute names and improved overall readability and maintainability of the test suite. --- .../steel_cross_sections/test_i_profile.py | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py index 16b69c7fe..f830994b6 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py @@ -5,36 +5,28 @@ from matplotlib.figure import Figure from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass -from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile, LoadStandardIProfile +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB class TestISteelProfile: """Test suite for ISteelProfile.""" - def test_str(self) -> None: - """Test the string representation of the I-profile.""" - profile = HEB.HEB360 - steel_class = SteelStrengthClass.S355 - expected_str = "Steel class: SteelStrengthClass.S355, Profile: HEB.HEB360" - assert LoadStandardIProfile(profile=profile, steel_class=steel_class).__str__() == expected_str - - def test_alias(self) -> None: + def test_alias(self, i_profile: ISteelProfile) -> None: """Test the alias of the I-profile.""" - profile = HEB.HEB360 - alias = profile.alias expected_alias = "HEB360" - assert alias == expected_alias + assert i_profile.name == expected_alias def test_steel_volume_per_meter(self, i_profile: ISteelProfile) -> None: """Test the steel volume per meter.""" expected_volume = 1.806e-2 # m³/m - assert pytest.approx(i_profile.steel_volume_per_meter, rel=1e-2) == expected_volume + assert pytest.approx(i_profile.volume_per_meter, rel=1e-2) == expected_volume def test_steel_weight_per_meter(self, i_profile: ISteelProfile) -> None: """Test the steel weight per meter.""" expected_weight = 1.806e-2 * 7850 # kg/m - assert pytest.approx(i_profile.steel_weight_per_meter, rel=1e-2) == expected_weight + assert pytest.approx(i_profile.weight_per_meter, rel=1e-2) == expected_weight def test_steel_area(self, i_profile: ISteelProfile) -> None: """Test the steel cross-sectional area.""" @@ -102,11 +94,10 @@ def test_section_properties(self, i_profile: ISteelProfile) -> None: def test_get_profile_with_corrosion(self) -> None: """Test the EHB profile with 20 mm corrosion applied.""" - profile: HEB = HEB.HEB360 - steel_class: SteelStrengthClass = SteelStrengthClass.S355 - - loader = LoadStandardIProfile(profile=profile, steel_class=steel_class) - # Ensure the profile raises an error if fully corroded with pytest.raises(ValueError, match="The profile has fully corroded."): - loader.get_profile(corrosion=20.0) + ISteelProfile.from_standard_profile( + profile=HEB.HEB360, + steel_material=SteelMaterial(SteelStrengthClass.S355), + corrosion=20, # mm + ) From 0294e05c3d524191d316472d586289f4fdb8f4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:22:21 +0200 Subject: [PATCH 55/70] Add icecream library to development dependencies The icecream library was added to `requirements_dev.txt` to improve debugging during development. It provides a simple and intuitive way to inspect variables and program flow. --- requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index c6ba7d776..2f90dd204 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -7,3 +7,4 @@ pytest-describe==2.2.0 pytest-pspec==0.0.4 pytest-raises==0.11 types-shapely==2.1.0.20250418 +icecream==2.1.4 From adb7de03095401f206ab6aca3937456e2acc8fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 15:24:53 +0200 Subject: [PATCH 56/70] Refactor CHS profile tests for updated class interface Replaced `LoadStandardCHS` with the updated `CHSSteelProfile` class interface. Enhanced test methods to use streamlined input handling and removed redundant code. Improved accuracy and clarity of test expectations. --- .../steel_cross_sections/test_chs_profile.py | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index 90063bc02..574eedab8 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -5,37 +5,28 @@ from matplotlib.figure import Figure from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass -from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile, LoadStandardCHS +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS class TestCHSSteelProfile: """Test suite for CHSSteelProfile.""" - def test_str(self) -> None: - """Test the string representation of the CHS profile.""" - profile: CHS = CHS.CHS508x16 - steel_class: SteelStrengthClass = SteelStrengthClass.S355 - expected_str: str = "Steel class: SteelStrengthClass.S355, Profile: CHS.CHS508x16" - assert LoadStandardCHS(profile=profile, steel_class=steel_class).__str__() == expected_str - - def test_code(self) -> None: + def test_code(self, chs_profile: CHSSteelProfile) -> None: """Test the code of the CHS profile.""" - profile: CHS = CHS.CHS508x16 - steel_class: SteelStrengthClass = SteelStrengthClass.S355 - alias: str = LoadStandardCHS(profile=profile, steel_class=steel_class).alias() expected_alias: str = "CHS 508x16" - assert alias == expected_alias + assert chs_profile.name == expected_alias def test_steel_volume_per_meter(self, chs_profile: CHSSteelProfile) -> None: """Test the steel volume per meter.""" expected_volume: float = 2.47e-2 # m³/m - assert pytest.approx(chs_profile.steel_volume_per_meter, rel=1e-2) == expected_volume + assert pytest.approx(chs_profile.volume_per_meter, rel=1e-2) == expected_volume def test_steel_weight_per_meter(self, chs_profile: CHSSteelProfile) -> None: """Test the steel weight per meter.""" expected_weight: float = 2.47e-2 * 7850 # kg/m - assert pytest.approx(chs_profile.steel_weight_per_meter, rel=1e-2) == expected_weight + assert pytest.approx(chs_profile.weight_per_meter, rel=1e-2) == expected_weight def test_area(self, chs_profile: CHSSteelProfile) -> None: """Test the steel cross-sectional area.""" @@ -103,11 +94,11 @@ def test_section_properties(self, chs_profile: CHSSteelProfile) -> None: def test_get_profile_with_corrosion(self) -> None: """Test the CHS profile with 20 mm corrosion applied.""" - profile: CHS = CHS.CHS508x16 - steel_class: SteelStrengthClass = SteelStrengthClass.S355 - - loader = LoadStandardCHS(profile=profile, steel_class=steel_class) - # Ensure the profile raises an error if fully corroded with pytest.raises(ValueError, match="The profile has fully corroded."): - loader.get_profile(corrosion_outside=5, corrosion_inside=11) + CHSSteelProfile.from_standard_profile( + profile=CHS.CHS508x16, + steel_material=SteelMaterial(SteelStrengthClass.S355), + corrosion_outside=5, # mm + corrosion_inside=11, # mm + ) From 3a2d68a89ae4b4f3c6161e9cd6267dc91406ecef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 16:06:46 +0200 Subject: [PATCH 57/70] Refactor test assertions to access cross_section directly Updated test assertions to reference attributes through `steel_element.cross_section` instead of directly accessing properties on `steel_element`. This improves consistency and aligns with the updated internal structure of the `SteelElement` class. --- .../steel/test_steel_element.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/structural_sections/steel/test_steel_element.py b/tests/structural_sections/steel/test_steel_element.py index 80823e964..873c1d502 100644 --- a/tests/structural_sections/steel/test_steel_element.py +++ b/tests/structural_sections/steel/test_steel_element.py @@ -9,62 +9,62 @@ def test_name(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement name matches the mock cross-section name.""" - assert steel_element.name == mock_cross_section.name + assert steel_element.cross_section.name == mock_cross_section.name def test_area(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement area matches the mock cross-section area.""" - assert steel_element.area == mock_cross_section.area + assert steel_element.cross_section.area == mock_cross_section.area def test_perimeter(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement perimeter matches the mock cross-section perimeter.""" - assert steel_element.perimeter == mock_cross_section.perimeter + assert steel_element.cross_section.perimeter == mock_cross_section.perimeter def test_centroid(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement centroid matches the mock cross-section centroid.""" - assert steel_element.centroid == mock_cross_section.centroid + assert steel_element.cross_section.centroid == mock_cross_section.centroid def test_moment_of_inertia_about_y(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement moment of inertia about Y matches the mock cross-section.""" - assert steel_element.moment_of_inertia_about_y == mock_cross_section.moment_of_inertia_about_y + assert steel_element.cross_section.moment_of_inertia_about_y == mock_cross_section.moment_of_inertia_about_y def test_moment_of_inertia_about_z(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement moment of inertia about Z matches the mock cross-section.""" - assert steel_element.moment_of_inertia_about_z == mock_cross_section.moment_of_inertia_about_z + assert steel_element.cross_section.moment_of_inertia_about_z == mock_cross_section.moment_of_inertia_about_z def test_elastic_section_modulus_about_y_positive(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement elastic section modulus about Y+ matches the mock cross-section.""" - assert steel_element.elastic_section_modulus_about_y_positive == mock_cross_section.elastic_section_modulus_about_y_positive + assert steel_element.cross_section.elastic_section_modulus_about_y_positive == mock_cross_section.elastic_section_modulus_about_y_positive def test_elastic_section_modulus_about_y_negative(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement elastic section modulus about Y- matches the mock cross-section.""" - assert steel_element.elastic_section_modulus_about_y_negative == mock_cross_section.elastic_section_modulus_about_y_negative + assert steel_element.cross_section.elastic_section_modulus_about_y_negative == mock_cross_section.elastic_section_modulus_about_y_negative def test_elastic_section_modulus_about_z_positive(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement elastic section modulus about Z+ matches the mock cross-section.""" - assert steel_element.elastic_section_modulus_about_z_positive == mock_cross_section.elastic_section_modulus_about_z_positive + assert steel_element.cross_section.elastic_section_modulus_about_z_positive == mock_cross_section.elastic_section_modulus_about_z_positive def test_elastic_section_modulus_about_z_negative(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement elastic section modulus about Z- matches the mock cross-section.""" - assert steel_element.elastic_section_modulus_about_z_negative == mock_cross_section.elastic_section_modulus_about_z_negative + assert steel_element.cross_section.elastic_section_modulus_about_z_negative == mock_cross_section.elastic_section_modulus_about_z_negative def test_plastic_section_modulus_about_y(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement plastic section modulus about Y matches the mock cross-section.""" - assert steel_element.plastic_section_modulus_about_y == mock_cross_section.plastic_section_modulus_about_y + assert steel_element.cross_section.plastic_section_modulus_about_y == mock_cross_section.plastic_section_modulus_about_y def test_plastic_section_modulus_about_z(steel_element: SteelElement, mock_cross_section: Mock) -> None: """Test that the SteelElement plastic section modulus about Z matches the mock cross-section.""" - assert steel_element.plastic_section_modulus_about_z == mock_cross_section.plastic_section_modulus_about_z + assert steel_element.cross_section.plastic_section_modulus_about_z == mock_cross_section.plastic_section_modulus_about_z def test_weight_per_meter(steel_element: SteelElement, mock_cross_section: Mock, mock_material: Mock) -> None: From 7843551bc85a8d362db2161693bfae8a4e6a0c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 16:27:03 +0200 Subject: [PATCH 58/70] Fix incorrect property usage in ultimate strength calculation. The function was incorrectly returning the minimum yield strength instead of the minimum ultimate strength of steel elements. This fix ensures the method aligns with its intended purpose and documentation. --- .../steel/steel_cross_sections/_steel_cross_section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py index 0a8a9c81e..c3eb429f9 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/_steel_cross_section.py @@ -161,4 +161,4 @@ def ultimate_strength(self) -> MPA: The ultimate strength of the steel element. """ # let's find the minimum ultimate strength of all elements - return min(element.yield_strength for element in self.elements) + return min(element.ultimate_strength for element in self.elements) From 14ee66172e7948f13cc049d7559a48fd9cf3defe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 16:27:23 +0200 Subject: [PATCH 59/70] Refine area property docstring for clarity and accuracy. Clarified the area property's docstring to explain how the area is approximated for circular cross-sections and calculated using Shapely's Polygon. Added guidance for overriding the method in derived classes for exact calculations. --- blueprints/structural_sections/_cross_section.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/blueprints/structural_sections/_cross_section.py b/blueprints/structural_sections/_cross_section.py index 82d6f9f0e..098b3139d 100644 --- a/blueprints/structural_sections/_cross_section.py +++ b/blueprints/structural_sections/_cross_section.py @@ -25,7 +25,13 @@ def polygon(self) -> Polygon: @property def area(self) -> MM2: - """Area of the cross-section [mm²].""" + """Area of the cross-section [mm²]. + + When using circular cross-sections, the area is an approximation of the area of the polygon. + The area is calculated using the `area` property of the Shapely Polygon. + + In case you need an exact answer then you need to override this method in the derived class. + """ return self.polygon.area @property From a97bb287e593984f0f2055eb88da282fe540cd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 16:27:48 +0200 Subject: [PATCH 60/70] Remove unused properties and redundant imports from cross_section_rectangle.py The `area`, `perimeter`, and `centroid` properties were removed as they are unused. Additionally, the unnecessary import of `Point` and `MM2` was cleaned up. This reduces code clutter and improves maintainability. --- .../cross_section_rectangle.py | 40 +------------------ 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/blueprints/structural_sections/cross_section_rectangle.py b/blueprints/structural_sections/cross_section_rectangle.py index 5ae300456..9c56513d8 100644 --- a/blueprints/structural_sections/cross_section_rectangle.py +++ b/blueprints/structural_sections/cross_section_rectangle.py @@ -3,10 +3,10 @@ from dataclasses import dataclass from sectionproperties.pre import Geometry -from shapely import Point, Polygon +from shapely import Polygon from blueprints.structural_sections._cross_section import CrossSection -from blueprints.type_alias import MM, MM2, MM3, MM4 +from blueprints.type_alias import MM, MM3, MM4 @dataclass(frozen=True) @@ -67,42 +67,6 @@ def polygon(self) -> Polygon: ] ) - @property - def area(self) -> MM2: - """ - Calculate the area of the rectangular cross-section. - - Returns - ------- - MM2 - The area of the rectangle. - """ - return self.width * self.height - - @property - def perimeter(self) -> MM: - """ - Calculate the perimeter of the rectangular cross-section. - - Returns - ------- - MM - The perimeter of the rectangle. - """ - return 2 * (self.width + self.height) - - @property - def centroid(self) -> Point: - """ - Get the centroid of the rectangular cross-section. - - Returns - ------- - Point - The centroid of the rectangle. - """ - return Point(self.x, self.y) - @property def moment_of_inertia_about_y(self) -> MM4: """ From 2168b5d1ad0dd8c7d765fbc2f5e741a812113c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 16:28:05 +0200 Subject: [PATCH 61/70] Add new tests for strength and plastic section moduli Added tests for yield and ultimate strength of the Strip profile. Additionally, included tests for plastic section modulus about both the y- and z-axes to enhance coverage and ensure accuracy. --- .../steel_cross_sections/test_strip_profile.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index d02c76315..bf2c113c1 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -91,6 +91,24 @@ def test_section_properties(self, strip_profile: StripSteelProfile) -> None: assert section_properties.zxx_minus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_y_negative, rel=1e-2) assert section_properties.zyy_minus == pytest.approx(expected=strip_profile.elastic_section_modulus_about_z_negative, rel=1e-2) + def test_yield_strength(self, strip_profile: StripSteelProfile) -> None: + """Test the yield strength of the Strip profile.""" + assert strip_profile.yield_strength == 355 + + def test_ultimate_strength(self, strip_profile: StripSteelProfile) -> None: + """Test the ultimate strength of the Strip profile.""" + assert strip_profile.ultimate_strength == 490 + + def test_plastic_section_modulus_about_y(self, strip_profile: StripSteelProfile) -> None: + """Test the plastic section modulus about the y-axis.""" + expected_plastic_modulus_y = 1 / 4 * 160 * 5**2 + assert pytest.approx(strip_profile.plastic_section_modulus_about_y, rel=1e-6) == expected_plastic_modulus_y + + def test_plastic_section_modulus_about_z(self, strip_profile: StripSteelProfile) -> None: + """Test the plastic section modulus about the z-axis.""" + expected_plastic_modulus_z = 1 / 4 * 5 * 160**2 + assert pytest.approx(strip_profile.plastic_section_modulus_about_z, rel=1e-6) == expected_plastic_modulus_z + def test_get_profile_with_corrosion(self) -> None: """Test the Strip profile with 2 mm corrosion applied.""" # Ensure the profile raises an error if fully corroded From 53cc3e90dd9260aa15a449b6ade536de950b6115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 7 May 2025 16:38:35 +0200 Subject: [PATCH 62/70] Add tests for CombinedSteelCrossSection functionality Introduced unit tests for `CombinedSteelCrossSection` to validate edge cases, including handling of empty configurations and invalid elements. Added a new pytest fixture in `conftest.py` for setting up empty combined steel cross-section instances. --- .../steel/steel_cross_sections/conftest.py | 9 +++++ .../test_steel_cross_section.py | 37 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/structural_sections/steel/steel_cross_sections/test_steel_cross_section.py diff --git a/tests/structural_sections/steel/steel_cross_sections/conftest.py b/tests/structural_sections/steel/steel_cross_sections/conftest.py index df3c3249e..dc2ea50e8 100644 --- a/tests/structural_sections/steel/steel_cross_sections/conftest.py +++ b/tests/structural_sections/steel/steel_cross_sections/conftest.py @@ -4,6 +4,7 @@ from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import CombinedSteelCrossSection from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS @@ -34,3 +35,11 @@ def i_profile() -> ISteelProfile: profile = HEB.HEB360 steel_class = SteelStrengthClass.S355 return ISteelProfile.from_standard_profile(profile=profile, steel_material=SteelMaterial(steel_class)) + + +@pytest.fixture +def empty_combined_steel_cross_section() -> CombinedSteelCrossSection: + """Fixture to set up a combined steel cross-section for testing.""" + return CombinedSteelCrossSection( + name="Empty Combined Steel Cross Section", + ) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_steel_cross_section.py b/tests/structural_sections/steel/steel_cross_sections/test_steel_cross_section.py new file mode 100644 index 000000000..e374b680f --- /dev/null +++ b/tests/structural_sections/steel/steel_cross_sections/test_steel_cross_section.py @@ -0,0 +1,37 @@ +"""Test suite for CombinedSteelCrossSection.""" + +import pytest + +from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.materials.steel import SteelMaterial +from blueprints.structural_sections.cross_section_rectangle import RectangularCrossSection +from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import CombinedSteelCrossSection +from blueprints.structural_sections.steel.steel_element import SteelElement + + +class TestCombinedSteelCrossSection: + """Test suite for CombinedSteelCrossSection.""" + + def test_empty_combined_steel_cross_section(self, empty_combined_steel_cross_section: CombinedSteelCrossSection) -> None: + """Test the code of the combined steel cross-section.""" + with pytest.raises(ValueError): + _ = empty_combined_steel_cross_section.polygon + + def test_invalid_combined_elements(self, empty_combined_steel_cross_section: CombinedSteelCrossSection) -> None: + """Test the code of the combined steel cross-section.""" + steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) + empty_combined_steel_cross_section.elements = [ + SteelElement( + cross_section=RectangularCrossSection(width=500, height=500, x=0, y=0), + material=steel_material, + nominal_thickness=500, + ), + SteelElement( + cross_section=RectangularCrossSection(width=250, height=250, x=1000, y=0), + material=steel_material, + nominal_thickness=250, + ), + ] + + with pytest.raises(TypeError): + _ = empty_combined_steel_cross_section.polygon From 27583f776acbe3528ae76315b825b18fef45ec6c Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 3 Jun 2025 20:35:51 +0200 Subject: [PATCH 63/70] Update types-shapely version to 2.1.0.20250512 in requirements_dev.txt --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 2f90dd204..a9c35f110 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,5 +6,5 @@ pytest-cov==6.1.1 pytest-describe==2.2.0 pytest-pspec==0.0.4 pytest-raises==0.11 -types-shapely==2.1.0.20250418 +types-shapely==2.1.0.20250512 icecream==2.1.4 From 3d2a038b0b2e80ba4f303fd02f9a9c115e8c156a Mon Sep 17 00:00:00 2001 From: GerjanDorgelo Date: Tue, 10 Jun 2025 14:26:47 +0200 Subject: [PATCH 64/70] Update blueprints/structural_sections/cross_section_quarter_circular_spandrel.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../cross_section_quarter_circular_spandrel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/blueprints/structural_sections/cross_section_quarter_circular_spandrel.py b/blueprints/structural_sections/cross_section_quarter_circular_spandrel.py index b639b4c48..7d450444f 100644 --- a/blueprints/structural_sections/cross_section_quarter_circular_spandrel.py +++ b/blueprints/structural_sections/cross_section_quarter_circular_spandrel.py @@ -51,7 +51,10 @@ def polygon(self) -> Polygon: """ left_lower = (self.x, self.y) - # Approximate the quarter circle with 25 straight lines + # Approximate the quarter circle with 25 straight lines. + # This resolution was chosen to balance performance and accuracy. + # Increasing the number of segments (e.g., to 50) would improve accuracy but at the cost of computational performance. + # Ensure this resolution meets the requirements of your specific application before using. quarter_circle_points = [ (self.x + self.radius - self.radius * math.cos(math.pi / 2 * i / 25), self.y + self.radius - self.radius * math.sin(math.pi / 2 * i / 25)) for i in range(26) From 85f03a8daa67c227d19d1908d00c746ad23c75fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 11 Jun 2025 15:05:44 +0200 Subject: [PATCH 65/70] Refactor steel profile shapes to use SteelMaterial class and update documentation --- docs/examples/_code/steel_profile_shapes.py | 46 ++++++++++++++------- docs/examples/steel_profile_shapes.md | 16 +++---- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/docs/examples/_code/steel_profile_shapes.py b/docs/examples/_code/steel_profile_shapes.py index 5c9abe64b..d5b2ea6c9 100644 --- a/docs/examples/_code/steel_profile_shapes.py +++ b/docs/examples/_code/steel_profile_shapes.py @@ -2,21 +2,27 @@ This example demonstrates how to create and visualize different steel profile shapes using the Blueprints library. """ -# ruff: noqa: T201 +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.materials.steel import SteelMaterial -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass -from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile, LoadStandardCHS -from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile, LoadStandardIProfile +# ruff: noqa: T201 +from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile +from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip -from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import LoadStandardStrip, StripSteelProfile +from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import StripSteelProfile # Define steel class -steel_class = SteelStrengthClass.S355 +steel_material = SteelMaterial(steel_class=SteelStrengthClass.S355) # Example usage for CHS profile -chs_profile = LoadStandardCHS(profile=CHS.CHS273x5, steel_class=steel_class).get_profile(corrosion_inside=0, corrosion_outside=1) +chs_profile = CHSSteelProfile.from_standard_profile( + profile=CHS.CHS273x5, + steel_material=steel_material, + corrosion_inside=0, # mm + corrosion_outside=4, # mm +) chs_profile.plot(show=True) print(f"Steel class: {chs_profile.steel_material}") print(f"Moment of inertia about y-axis: {chs_profile.moment_of_inertia_about_y} mm⁴") @@ -26,23 +32,35 @@ print(f"Area: {chs_profile.area} mm²") # Example usage for custom CHS profile -custom_chs_profile = CHSSteelProfile(outer_diameter=150, wall_thickness=10, steel_class=steel_class) +custom_chs_profile = CHSSteelProfile( + outer_diameter=150, + wall_thickness=10, + steel_material=steel_material, +) custom_chs_profile.plot(show=True) # Example usage for Strip profile -strip_profile = LoadStandardStrip(profile=Strip.STRIP160x5, steel_class=steel_class).get_profile(corrosion=1) +strip_profile = StripSteelProfile.from_standard_profile( + profile=Strip.STRIP160x5, + steel_material=steel_material, + corrosion=1, # mm +) strip_profile.plot(show=True) # Example usage for custom Strip profile custom_strip_profile = StripSteelProfile( - width=100, - height=41, - steel_class=steel_class, + strip_width=100, + strip_height=41, + steel_material=steel_material, ) custom_strip_profile.plot(show=True) # Example usage for HEB600 profile -heb_profile = LoadStandardIProfile(profile=HEB.HEB600, steel_class=steel_class).get_profile(corrosion=7) +heb_profile = ISteelProfile.from_standard_profile( + profile=HEB.HEB600, + steel_material=steel_material, + corrosion=7, # mm +) heb_profile.plot(show=True) # Example usage for custom I profile @@ -53,7 +71,7 @@ bottom_flange_thickness=10, # mm total_height=600, # mm web_thickness=10, # mm - steel_class=steel_class, + steel_material=steel_material, top_radius=15, # mm bottom_radius=8, # mm ) diff --git a/docs/examples/steel_profile_shapes.md b/docs/examples/steel_profile_shapes.md index 4dfc8904b..f1f83fd29 100644 --- a/docs/examples/steel_profile_shapes.md +++ b/docs/examples/steel_profile_shapes.md @@ -10,10 +10,10 @@ Follow the steps below to explore the usage of different steel profile shapes (o ## Define the Steel Class -Start by defining the steel class to be used for the profiles: +Start by defining the steel material to be used for the profiles: ```python ---8<-- "examples/_code/steel_profile_shapes.py:17:18" +--8<-- "examples/_code/steel_profile_shapes.py:15:16" ``` ## Circular Hollow Section (CHS) Profiles @@ -23,7 +23,7 @@ Start by defining the steel class to be used for the profiles: Structual parameters are automatically calculated and can be obtained with: ```python ---8<-- "examples/_code/steel_profile_shapes.py:20:28" +--8<-- "examples/_code/steel_profile_shapes.py:18:31" ``` ### Custom CHS Profile @@ -31,7 +31,7 @@ Structual parameters are automatically calculated and can be obtained with: Alternatively, define a custom CHS profile by specifying its dimensions: ```python ---8<-- "examples/_code/steel_profile_shapes.py:30:32" +--8<-- "examples/_code/steel_profile_shapes.py:33:39" ``` ## Strip Profiles @@ -41,7 +41,7 @@ Alternatively, define a custom CHS profile by specifying its dimensions: Predefined strip profiles are also available: ```python ---8<-- "examples/_code/steel_profile_shapes.py:30:32" +--8<-- "examples/_code/steel_profile_shapes.py:41:47" ``` ### Custom Strip Profile @@ -49,7 +49,7 @@ Predefined strip profiles are also available: Define a custom strip profile by specifying its width and height: ```python ---8<-- "examples/_code/steel_profile_shapes.py:34:40" +--8<-- "examples/_code/steel_profile_shapes.py:49:55" ``` ## Strip Profiles @@ -59,7 +59,7 @@ Define a custom strip profile by specifying its width and height: Predefined I profiles are also available: ```python ---8<-- "examples/_code/steel_profile_shapes.py:40:42" +--8<-- "examples/_code/steel_profile_shapes.py:57:63" ``` ### Custom Strip Profile @@ -67,7 +67,7 @@ Predefined I profiles are also available: Define a custom strip profile by specifying its width and height: ```python ---8<-- "examples/_code/steel_profile_shapes.py:44:61" +--8<-- "examples/_code/steel_profile_shapes.py:65:77" ``` ## Visualizing Profiles From 4fee673860c45cfb75a87f2d852ce562659b54d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 11 Jun 2025 15:28:45 +0200 Subject: [PATCH 66/70] Enhance profile naming to include corrosion details in chs_profile, i_profile, and strip_profile --- .../steel/steel_cross_sections/chs_profile.py | 10 +++++++++- .../steel/steel_cross_sections/i_profile.py | 6 +++++- .../steel/steel_cross_sections/strip_profile.py | 6 +++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py index ad6958faf..c5fecf181 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/chs_profile.py @@ -84,11 +84,19 @@ def from_standard_profile( if adjusted_thickness <= 0: raise ValueError("The profile has fully corroded.") + name = profile.alias + if corrosion_inside or corrosion_outside: + name += ( + f" (corrosion {'' if not corrosion_inside else f' in: {corrosion_inside} mm'}" + f"{', ' if corrosion_inside and corrosion_outside else ''}" + f"{'' if not corrosion_outside else f'out: {corrosion_outside} mm'})" + ) + return cls( outer_diameter=adjusted_outer_diameter, wall_thickness=adjusted_thickness, steel_material=steel_material, - name=profile.alias, + name=name, ) def plot(self, plotter: Callable[[CombinedSteelCrossSection], plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: diff --git a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py index 90f71ca93..879eed7d3 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/i_profile.py @@ -203,6 +203,10 @@ def from_standard_profile( ): raise ValueError("The profile has fully corroded.") + name = profile.alias + if corrosion: + name += f" (corrosion: {corrosion} mm)" + return cls( top_flange_width=top_flange_width, top_flange_thickness=top_flange_thickness, @@ -213,7 +217,7 @@ def from_standard_profile( steel_material=steel_material, top_radius=profile.top_radius, bottom_radius=profile.bottom_radius, - name=profile.alias, + name=name, ) def plot(self, plotter: Callable[[CombinedSteelCrossSection], plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: diff --git a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py index 3449f5142..5915e1345 100644 --- a/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py +++ b/blueprints/structural_sections/steel/steel_cross_sections/strip_profile.py @@ -84,11 +84,15 @@ def from_standard_profile( if width <= 0 or height <= 0: raise ValueError("The profile has fully corroded.") + name = profile.alias + if corrosion: + name += f" (corrosion: {corrosion} mm)" + return cls( steel_material=steel_material, strip_width=width, strip_height=height, - name=profile.alias, + name=name, ) def plot(self, plotter: Callable[[CombinedSteelCrossSection], plt.Figure] | None = None, *args, **kwargs) -> plt.Figure: From 177a9b8d5e75f96c227ef8e7b49b1a1c462c0c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 11 Jun 2025 15:35:23 +0200 Subject: [PATCH 67/70] Update references from NEN-EN 1993-1-1:2016 to EN 1993-1-1:2005 in steel profile tests --- .../structural_sections/steel/steel_cross_sections/conftest.py | 2 +- .../steel/steel_cross_sections/test_chs_profile.py | 2 +- .../steel/steel_cross_sections/test_i_profile.py | 2 +- .../steel/steel_cross_sections/test_steel_cross_section.py | 2 +- .../steel/steel_cross_sections/test_strip_profile.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/structural_sections/steel/steel_cross_sections/conftest.py b/tests/structural_sections/steel/steel_cross_sections/conftest.py index dc2ea50e8..fd536756c 100644 --- a/tests/structural_sections/steel/steel_cross_sections/conftest.py +++ b/tests/structural_sections/steel/steel_cross_sections/conftest.py @@ -2,7 +2,7 @@ import pytest -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import CombinedSteelCrossSection from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index 574eedab8..98f19e651 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -4,7 +4,7 @@ from matplotlib import pyplot as plt from matplotlib.figure import Figure -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_cross_sections.chs_profile import CHSSteelProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.chs import CHS diff --git a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py index f830994b6..cc558dbe3 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py @@ -4,7 +4,7 @@ from matplotlib import pyplot as plt from matplotlib.figure import Figure -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_cross_sections.i_profile import ISteelProfile from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.heb import HEB diff --git a/tests/structural_sections/steel/steel_cross_sections/test_steel_cross_section.py b/tests/structural_sections/steel/steel_cross_sections/test_steel_cross_section.py index e374b680f..ecfc00297 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_steel_cross_section.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_steel_cross_section.py @@ -2,7 +2,7 @@ import pytest -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.cross_section_rectangle import RectangularCrossSection from blueprints.structural_sections.steel.steel_cross_sections._steel_cross_section import CombinedSteelCrossSection diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index bf2c113c1..e3b398bf1 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -3,7 +3,7 @@ import matplotlib.pyplot as plt import pytest -from blueprints.codes.eurocode.nen_en_1993_1_1_c2_a1_2016.chapter_3_materials.table_3_1 import SteelStrengthClass +from blueprints.codes.eurocode.en_1993_1_1_2005.chapter_3_materials.table_3_1 import SteelStrengthClass from blueprints.materials.steel import SteelMaterial from blueprints.structural_sections.steel.steel_cross_sections.standard_profiles.strip import Strip from blueprints.structural_sections.steel.steel_cross_sections.strip_profile import StripSteelProfile From baf6b05d3424e9e56a222f27d85403c3954484a2 Mon Sep 17 00:00:00 2001 From: Gerjan Dorgelo Date: Wed, 11 Jun 2025 15:46:41 +0200 Subject: [PATCH 68/70] Add test for corrosion inclusion in StripSteelProfile name --- .../steel/steel_cross_sections/test_strip_profile.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py index e3b398bf1..76bb17d36 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_strip_profile.py @@ -118,3 +118,13 @@ def test_get_profile_with_corrosion(self) -> None: steel_material=SteelMaterial(SteelStrengthClass.S355), corrosion=2.5, ) + + def test_corrosion_in_name(self, strip_profile: StripSteelProfile) -> None: + """Test that the corrosion is included in the profile name.""" + profile_with_corrosion = StripSteelProfile.from_standard_profile( + profile=Strip.STRIP160x5, + steel_material=SteelMaterial(SteelStrengthClass.S355), + corrosion=1, + ) + expected_name = f"{strip_profile.name} (corrosion: 1 mm)" + assert profile_with_corrosion.name == expected_name From 9bf7f4131003099e08a5ac3719df81bf2861f6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 11 Jun 2025 15:46:34 +0200 Subject: [PATCH 69/70] Add tests for CHSSteelProfile name and corrosion details --- .../steel_cross_sections/test_chs_profile.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py index 98f19e651..8942e7255 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_chs_profile.py @@ -13,6 +13,11 @@ class TestCHSSteelProfile: """Test suite for CHSSteelProfile.""" + def test_name(self, chs_profile: CHSSteelProfile) -> None: + """Test the name of the CHS profile.""" + expected_name: str = "CHS 508x16" + assert chs_profile.name == expected_name + def test_code(self, chs_profile: CHSSteelProfile) -> None: """Test the code of the CHS profile.""" expected_alias: str = "CHS 508x16" @@ -102,3 +107,14 @@ def test_get_profile_with_corrosion(self) -> None: corrosion_outside=5, # mm corrosion_inside=11, # mm ) + + def test_corrosion_in_name(self) -> None: + """Test that the name includes corrosion information.""" + chs_profile_with_corrosion = CHSSteelProfile.from_standard_profile( + profile=CHS.CHS508x16, + steel_material=SteelMaterial(SteelStrengthClass.S355), + corrosion_outside=1, # mm + corrosion_inside=2, # mm + ) + expected_name_with_corrosion = "CHS 508x16 (corrosion in: 2 mm, out: 1 mm)" + assert chs_profile_with_corrosion.name == expected_name_with_corrosion From 5dd78069deedff6cf64c3b022721efb2cd92aa86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa?= Date: Wed, 11 Jun 2025 15:49:14 +0200 Subject: [PATCH 70/70] Add tests for ISteelProfile name and corrosion details --- .../steel/steel_cross_sections/test_i_profile.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py index cc558dbe3..766249629 100644 --- a/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py +++ b/tests/structural_sections/steel/steel_cross_sections/test_i_profile.py @@ -101,3 +101,13 @@ def test_get_profile_with_corrosion(self) -> None: steel_material=SteelMaterial(SteelStrengthClass.S355), corrosion=20, # mm ) + + def test_corrosion_in_name(self) -> None: + """Test that the name includes corrosion information.""" + i_profile_with_corrosion = ISteelProfile.from_standard_profile( + profile=HEB.HEB360, + steel_material=SteelMaterial(SteelStrengthClass.S355), + corrosion=2, # mm + ) + expected_name_with_corrosion = "HEB360 (corrosion: 2 mm)" + assert i_profile_with_corrosion.name == expected_name_with_corrosion