Skip to content

Commit e7c2f5f

Browse files
Merge pull request #572 from Blueprints-org/571-feature-request-add-basis-for-complex-steel-shapes
2 parents f7686b6 + 5dd7806 commit e7c2f5f

26 files changed

+1778
-80
lines changed

blueprints/structural_sections/_cross_section.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,25 @@ def polygon(self) -> Polygon:
2424
"""Shapely Polygon representing the cross-section."""
2525

2626
@property
27-
@abstractmethod
2827
def area(self) -> MM2:
29-
"""Area of the cross-section [mm²]."""
28+
"""Area of the cross-section [mm²].
29+
30+
When using circular cross-sections, the area is an approximation of the area of the polygon.
31+
The area is calculated using the `area` property of the Shapely Polygon.
32+
33+
In case you need an exact answer then you need to override this method in the derived class.
34+
"""
35+
return self.polygon.area
3036

3137
@property
32-
@abstractmethod
3338
def perimeter(self) -> MM:
3439
"""Perimeter of the cross-section [mm]."""
40+
return self.polygon.length
3541

3642
@property
37-
@abstractmethod
3843
def centroid(self) -> Point:
3944
"""Centroid of the cross-section [mm]."""
45+
return self.polygon.centroid
4046

4147
@property
4248
@abstractmethod
@@ -78,16 +84,21 @@ def plastic_section_modulus_about_y(self) -> MM3 | None:
7884
def plastic_section_modulus_about_z(self) -> MM3 | None:
7985
"""Plastic section modulus about the z-axis [mm³]."""
8086

81-
@abstractmethod
8287
def geometry(self, mesh_size: MM | None = None) -> Geometry:
83-
"""Abstract method to be implemented by subclasses to return the geometry of the cross-section.
88+
"""Geometry of the cross-section.
8489
8590
Properties
8691
----------
8792
mesh_size : MM
8893
Maximum mesh element area to be used within
8994
the Geometry-object finite-element mesh. If not provided, a default value will be used.
9095
"""
96+
if mesh_size is None:
97+
mesh_size = 2.0
98+
99+
geom = Geometry(geom=self.polygon)
100+
geom.create_mesh(mesh_sizes=mesh_size)
101+
return geom
91102

92103
def section(self) -> Section:
93104
"""Section object representing the cross-section."""

blueprints/structural_sections/cross_section_quarter_circular_spandrel.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@ def polygon(self) -> Polygon:
5151
"""
5252
left_lower = (self.x, self.y)
5353

54-
# Approximate the quarter circle with 50 straight lines
54+
# Approximate the quarter circle with 25 straight lines.
55+
# This resolution was chosen to balance performance and accuracy.
56+
# Increasing the number of segments (e.g., to 50) would improve accuracy but at the cost of computational performance.
57+
# Ensure this resolution meets the requirements of your specific application before using.
5558
quarter_circle_points = [
56-
(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))
57-
for i in range(51)
59+
(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))
60+
for i in range(26)
5861
]
59-
for i in range(51):
62+
for i in range(26):
6063
if self.mirrored_horizontally:
6164
quarter_circle_points[i] = (2 * left_lower[0] - quarter_circle_points[i][0], quarter_circle_points[i][1])
6265
if self.mirrored_vertically:

blueprints/structural_sections/cross_section_rectangle.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from dataclasses import dataclass
44

55
from sectionproperties.pre import Geometry
6-
from shapely import Point, Polygon
6+
from shapely import Polygon
77

88
from blueprints.structural_sections._cross_section import CrossSection
9-
from blueprints.type_alias import MM, MM2, MM3, MM4
9+
from blueprints.type_alias import MM, MM3, MM4
1010

1111

1212
@dataclass(frozen=True)
@@ -67,42 +67,6 @@ def polygon(self) -> Polygon:
6767
]
6868
)
6969

70-
@property
71-
def area(self) -> MM2:
72-
"""
73-
Calculate the area of the rectangular cross-section.
74-
75-
Returns
76-
-------
77-
MM2
78-
The area of the rectangle.
79-
"""
80-
return self.width * self.height
81-
82-
@property
83-
def perimeter(self) -> MM:
84-
"""
85-
Calculate the perimeter of the rectangular cross-section.
86-
87-
Returns
88-
-------
89-
MM
90-
The perimeter of the rectangle.
91-
"""
92-
return 2 * (self.width + self.height)
93-
94-
@property
95-
def centroid(self) -> Point:
96-
"""
97-
Get the centroid of the rectangular cross-section.
98-
99-
Returns
100-
-------
101-
Point
102-
The centroid of the rectangle.
103-
"""
104-
return Point(self.x, self.y)
105-
10670
@property
10771
def moment_of_inertia_about_y(self) -> MM4:
10872
"""
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Base class of all steel cross-sections."""
2+
3+
from abc import ABC
4+
from collections.abc import Sequence
5+
from dataclasses import dataclass, field
6+
7+
from shapely.geometry import Polygon
8+
from shapely.geometry.base import BaseGeometry
9+
from shapely.geometry.polygon import orient
10+
11+
from blueprints.structural_sections._cross_section import CrossSection
12+
from blueprints.structural_sections.steel.steel_element import SteelElement
13+
from blueprints.type_alias import KG_M, M3_M, MM, MM3, MPA
14+
from blueprints.unit_conversion import MM3_TO_M3
15+
16+
17+
@dataclass(kw_only=True)
18+
class CombinedSteelCrossSection(CrossSection, ABC):
19+
"""Base class of all steel cross-sections.
20+
21+
Parameters
22+
----------
23+
elements : Sequence[SteelElement], optional
24+
A sequence of steel elements that make up the cross-section.
25+
Default is an empty list.
26+
name : str, optional
27+
The name of the cross-section. Default is "Combined Steel Cross Section".
28+
"""
29+
30+
elements: Sequence[SteelElement] = field(default_factory=list)
31+
name: str = "Combined Steel Cross Section"
32+
33+
@property
34+
def polygon(self) -> Polygon:
35+
"""Return the polygon of the steel cross-section."""
36+
# check if there are any elements
37+
if not self.elements:
38+
raise ValueError("No elements have been added to the cross-section.")
39+
40+
# return the polygon of the first element if there is only one
41+
if len(self.elements) == 1:
42+
return self.elements[0].cross_section.polygon
43+
44+
# Combine the polygons of all elements if there is multiple
45+
combined_polygon: BaseGeometry = self.elements[0].cross_section.polygon
46+
for element in self.elements[1:]:
47+
combined_polygon = combined_polygon.union(element.cross_section.polygon)
48+
49+
# Ensure the result is a valid Polygon
50+
if not isinstance(combined_polygon, Polygon):
51+
raise TypeError("The combined geometry is not a valid Polygon.")
52+
53+
# Ensure consistent orientation
54+
return orient(combined_polygon)
55+
56+
@property
57+
def height(self) -> MM:
58+
"""Height of the cross-section [mm]."""
59+
return self.polygon.bounds[3] - self.polygon.bounds[1]
60+
61+
@property
62+
def width(self) -> MM:
63+
"""Width of the cross-section [mm]."""
64+
return self.polygon.bounds[2] - self.polygon.bounds[0]
65+
66+
@property
67+
def volume_per_meter(self) -> M3_M:
68+
"""Total volume of the reinforced cross-section per meter length [m³/m]."""
69+
length = 1000 # mm
70+
return self.area * length * MM3_TO_M3
71+
72+
@property
73+
def weight_per_meter(self) -> KG_M:
74+
"""
75+
Calculate the weight per meter of the steel element.
76+
77+
Returns
78+
-------
79+
KG_M
80+
The weight per meter of the steel element.
81+
"""
82+
return sum(element.weight_per_meter for element in self.elements)
83+
84+
@property
85+
def moment_of_inertia_about_y(self) -> KG_M:
86+
"""Moment of inertia about the y-axis per meter length [mm⁴]."""
87+
body_moments_of_inertia = sum(element.cross_section.moment_of_inertia_about_y for element in self.elements)
88+
parallel_axis_theorem = sum(
89+
element.cross_section.area * (element.cross_section.centroid.y - self.centroid.y) ** 2 for element in self.elements
90+
)
91+
return body_moments_of_inertia + parallel_axis_theorem
92+
93+
@property
94+
def moment_of_inertia_about_z(self) -> KG_M:
95+
"""Moment of inertia about the z-axis per meter length [mm⁴]."""
96+
body_moments_of_inertia = sum(element.cross_section.moment_of_inertia_about_z for element in self.elements)
97+
parallel_axis_theorem = sum(
98+
element.cross_section.area * (element.cross_section.centroid.x - self.centroid.x) ** 2 for element in self.elements
99+
)
100+
return body_moments_of_inertia + parallel_axis_theorem
101+
102+
@property
103+
def elastic_section_modulus_about_y_positive(self) -> KG_M:
104+
"""Elastic section modulus about the y-axis on the positive z side [mm³]."""
105+
distance_to_top = max(y for _, y in self.polygon.exterior.coords) - self.centroid.y
106+
return self.moment_of_inertia_about_y / distance_to_top
107+
108+
@property
109+
def elastic_section_modulus_about_y_negative(self) -> KG_M:
110+
"""Elastic section modulus about the y-axis on the negative z side [mm³]."""
111+
distance_to_bottom = self.centroid.y - min(y for _, y in self.polygon.exterior.coords)
112+
return self.moment_of_inertia_about_y / distance_to_bottom
113+
114+
@property
115+
def elastic_section_modulus_about_z_positive(self) -> KG_M:
116+
"""Elastic section modulus about the z-axis on the positive y side [mm³]."""
117+
distance_to_right = max(x for x, _ in self.polygon.exterior.coords) - self.centroid.x
118+
return self.moment_of_inertia_about_z / distance_to_right
119+
120+
@property
121+
def elastic_section_modulus_about_z_negative(self) -> KG_M:
122+
"""Elastic section modulus about the z-axis on the negative y side [mm³]."""
123+
distance_to_left = self.centroid.x - min(x for x, _ in self.polygon.exterior.coords)
124+
return self.moment_of_inertia_about_z / distance_to_left
125+
126+
@property
127+
def plastic_section_modulus_about_y(self) -> MM3 | None:
128+
"""Plastic section modulus about the y-axis [mm³]."""
129+
return self.section_properties().sxx
130+
131+
@property
132+
def plastic_section_modulus_about_z(self) -> MM3 | None:
133+
"""Plastic section modulus about the z-axis [mm³]."""
134+
return self.section_properties().syy
135+
136+
@property
137+
def yield_strength(self) -> MPA:
138+
"""
139+
Calculate the yield strength of the steel element.
140+
141+
This is the minimum yield strength of all elements in the cross-section.
142+
143+
Returns
144+
-------
145+
MPa
146+
The yield strength of the steel element.
147+
"""
148+
# let's find the minimum yield strength of all elements
149+
return min(element.yield_strength for element in self.elements)
150+
151+
@property
152+
def ultimate_strength(self) -> MPA:
153+
"""
154+
Calculate the ultimate strength of the steel element.
155+
156+
This is the minimum ultimate strength of all elements in the cross-section.
157+
158+
Returns
159+
-------
160+
MPa
161+
The ultimate strength of the steel element.
162+
"""
163+
# let's find the minimum ultimate strength of all elements
164+
return min(element.ultimate_strength for element in self.elements)

0 commit comments

Comments
 (0)