structuralcodes 0.5.0__py3-none-any.whl → 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of structuralcodes might be problematic. Click here for more details.
- structuralcodes/__init__.py +1 -1
- structuralcodes/codes/ec2_2004/shear.py +3 -2
- structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +2 -2
- structuralcodes/core/base.py +138 -12
- structuralcodes/geometry/__init__.py +2 -8
- structuralcodes/geometry/_geometry.py +103 -19
- structuralcodes/geometry/_reinforcement.py +1 -3
- structuralcodes/geometry/profiles/__init__.py +33 -0
- structuralcodes/geometry/profiles/_base_profile.py +305 -0
- structuralcodes/geometry/profiles/_common_functions.py +307 -0
- structuralcodes/geometry/profiles/_hd.py +374 -0
- structuralcodes/geometry/profiles/_he.py +192 -0
- structuralcodes/geometry/profiles/_hp.py +319 -0
- structuralcodes/geometry/profiles/_ipe.py +130 -0
- structuralcodes/geometry/profiles/_ipn.py +329 -0
- structuralcodes/geometry/profiles/_l.py +528 -0
- structuralcodes/geometry/profiles/_li.py +217 -0
- structuralcodes/geometry/profiles/_u.py +173 -0
- structuralcodes/geometry/profiles/_ub.py +1356 -0
- structuralcodes/geometry/profiles/_ubp.py +227 -0
- structuralcodes/geometry/profiles/_uc.py +276 -0
- structuralcodes/geometry/profiles/_upe.py +133 -0
- structuralcodes/geometry/profiles/_upn.py +315 -0
- structuralcodes/geometry/profiles/_w.py +2157 -0
- structuralcodes/materials/basic/_elastic.py +18 -1
- structuralcodes/materials/basic/_elasticplastic.py +18 -1
- structuralcodes/materials/basic/_generic.py +18 -1
- structuralcodes/materials/concrete/__init__.py +3 -0
- structuralcodes/materials/concrete/_concrete.py +10 -1
- structuralcodes/materials/concrete/_concreteEC2_2004.py +15 -1
- structuralcodes/materials/concrete/_concreteEC2_2023.py +15 -1
- structuralcodes/materials/concrete/_concreteMC2010.py +20 -1
- structuralcodes/materials/constitutive_laws/__init__.py +3 -0
- structuralcodes/materials/constitutive_laws/_elasticplastic.py +2 -2
- structuralcodes/materials/constitutive_laws/_initial_strain.py +130 -0
- structuralcodes/materials/reinforcement/__init__.py +6 -0
- structuralcodes/materials/reinforcement/_reinforcement.py +10 -1
- structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +14 -0
- structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +14 -0
- structuralcodes/materials/reinforcement/_reinforcementMC2010.py +14 -0
- structuralcodes/sections/section_integrators/__init__.py +3 -1
- structuralcodes/sections/section_integrators/_marin_integrator.py +1 -1
- {structuralcodes-0.5.0.dist-info → structuralcodes-0.6.1.dist-info}/METADATA +2 -2
- {structuralcodes-0.5.0.dist-info → structuralcodes-0.6.1.dist-info}/RECORD +47 -30
- structuralcodes/geometry/_steel_sections.py +0 -2155
- /structuralcodes/{sections/section_integrators → core}/_marin_integration.py +0 -0
- {structuralcodes-0.5.0.dist-info → structuralcodes-0.6.1.dist-info}/WHEEL +0 -0
- {structuralcodes-0.5.0.dist-info → structuralcodes-0.6.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Base class for profiles."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from shapely import (
|
|
5
|
+
LinearRing,
|
|
6
|
+
LineString,
|
|
7
|
+
Polygon,
|
|
8
|
+
)
|
|
9
|
+
from shapely.affinity import rotate, translate
|
|
10
|
+
from shapely.ops import split
|
|
11
|
+
|
|
12
|
+
from structuralcodes.core._marin_integration import marin_integration
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseProfile:
|
|
16
|
+
"""Base class representing a profile.
|
|
17
|
+
|
|
18
|
+
Contains the common code for all profiles.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Creates an empty base profile."""
|
|
23
|
+
self._polygon: Polygon = None
|
|
24
|
+
self._A: float = None
|
|
25
|
+
self._Iy: float = None
|
|
26
|
+
self._Iz: float = None
|
|
27
|
+
self._Icsi: float = None
|
|
28
|
+
self._Ieta: float = None
|
|
29
|
+
self._Iyz: float = None
|
|
30
|
+
self._Wely: float = None
|
|
31
|
+
self._Welz: float = None
|
|
32
|
+
self._iy: float = None
|
|
33
|
+
self._iz: float = None
|
|
34
|
+
self._Wply: float = None
|
|
35
|
+
self._Wplz: float = None
|
|
36
|
+
|
|
37
|
+
def _check_polygon_defined(self):
|
|
38
|
+
"""Just checks if polygon attribute is defined.
|
|
39
|
+
|
|
40
|
+
If the polygon is not defined (it should never happen), an exception
|
|
41
|
+
is Raised.
|
|
42
|
+
"""
|
|
43
|
+
# The polygon attribute should be already defined
|
|
44
|
+
if self._polygon is None:
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
'The polygon for some reason was not correctly defined.'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _find_plastic_neutral_axis_y(self) -> float:
|
|
50
|
+
"""Find posizion z of plastic neutral axes parallel to y.
|
|
51
|
+
|
|
52
|
+
We use bisection algorithm within the section limits.
|
|
53
|
+
"""
|
|
54
|
+
bounds = self.polygon.bounds
|
|
55
|
+
zmin, zmax = bounds[1], bounds[3]
|
|
56
|
+
|
|
57
|
+
zA = zmin
|
|
58
|
+
zB = zmax
|
|
59
|
+
|
|
60
|
+
daA = self._find_delta_area_above_minus_below(z=zA)
|
|
61
|
+
|
|
62
|
+
ITMAX = 200
|
|
63
|
+
it = 0
|
|
64
|
+
|
|
65
|
+
while (it < ITMAX) and (abs(zB - zA) > (zmax - zmin) * 1e-10):
|
|
66
|
+
zC = (zA + zB) / 2.0
|
|
67
|
+
daC = self._find_delta_area_above_minus_below(z=zC)
|
|
68
|
+
if abs(daC) < 1e-10:
|
|
69
|
+
break
|
|
70
|
+
if daA * daC < 0:
|
|
71
|
+
# The solution is between A and C
|
|
72
|
+
zB = zC
|
|
73
|
+
else:
|
|
74
|
+
# The solution is between C and B
|
|
75
|
+
zA = zC
|
|
76
|
+
daA = daC
|
|
77
|
+
it += 1
|
|
78
|
+
if it >= ITMAX:
|
|
79
|
+
s = f'Last iteration reached a unbalance of {daC}'
|
|
80
|
+
raise ValueError(f'Maximum number of iterations reached.\n{s}')
|
|
81
|
+
|
|
82
|
+
return zC
|
|
83
|
+
|
|
84
|
+
def _find_delta_area_above_minus_below(self, z: float) -> float:
|
|
85
|
+
"""Returns area difference between above and below parts.
|
|
86
|
+
|
|
87
|
+
Above and below parts are computed splitting the polygon with a line
|
|
88
|
+
parallel to Y axes at z coordinate.
|
|
89
|
+
"""
|
|
90
|
+
bounds = self._polygon.bounds
|
|
91
|
+
xmax = max(abs(bounds[0]), bounds[2])
|
|
92
|
+
line = LineString([[-xmax * 1.05, z], [xmax * 1.05, z]])
|
|
93
|
+
|
|
94
|
+
area_above = 0
|
|
95
|
+
area_below = 0
|
|
96
|
+
# divide polygons "above" and "below" line
|
|
97
|
+
if line.intersects(self._polygon):
|
|
98
|
+
result = split(self._polygon, line)
|
|
99
|
+
# divide polygons "above" and "below" line
|
|
100
|
+
for geom in result.geoms:
|
|
101
|
+
if LinearRing(
|
|
102
|
+
(line.coords[0], line.coords[1], geom.centroid.coords[0])
|
|
103
|
+
).is_ccw:
|
|
104
|
+
area_above += geom.area
|
|
105
|
+
else:
|
|
106
|
+
area_below += geom.area
|
|
107
|
+
else:
|
|
108
|
+
# not intersecting, all the polygon is above or below the line
|
|
109
|
+
geom = self.polygon
|
|
110
|
+
if LinearRing(
|
|
111
|
+
(line.coords[0], line.coords[1], geom.centroid.coords[0])
|
|
112
|
+
).is_ccw:
|
|
113
|
+
area_above += geom.area
|
|
114
|
+
else:
|
|
115
|
+
area_below += geom.area
|
|
116
|
+
return area_above - area_below
|
|
117
|
+
|
|
118
|
+
def _find_principals_direction_and_moments(self):
|
|
119
|
+
"""Computes principal direction and second area moments."""
|
|
120
|
+
eigres = np.linalg.eig(
|
|
121
|
+
np.array([[self.Iy, self.Iyz], [self.Iyz, self.Iz]])
|
|
122
|
+
)
|
|
123
|
+
max_idx = np.argmax(eigres[0])
|
|
124
|
+
min_idx = 0 if max_idx == 1 else 1
|
|
125
|
+
self._Icsi = eigres[0][max_idx]
|
|
126
|
+
self._Ieta = eigres[0][min_idx]
|
|
127
|
+
self._theta = np.arccos(
|
|
128
|
+
np.dot(np.array([1, 0]), eigres[1][:, max_idx])
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def A(self) -> float:
|
|
133
|
+
"""Returns area of profile."""
|
|
134
|
+
if self._A is None:
|
|
135
|
+
# Check if the polygon is defined
|
|
136
|
+
self._check_polygon_defined()
|
|
137
|
+
# Get the polygon coordinates:
|
|
138
|
+
xy = self._polygon.exterior.coords.xy
|
|
139
|
+
# Compute area
|
|
140
|
+
self._A = marin_integration(xy[0], xy[1], 0, 0)
|
|
141
|
+
return self._A
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def Iy(self) -> float:
|
|
145
|
+
"""Returns second moment of area around y axis."""
|
|
146
|
+
if self._Iy is None:
|
|
147
|
+
# Check if the polygon is defined
|
|
148
|
+
self._check_polygon_defined()
|
|
149
|
+
# Get the polygon coordinates:
|
|
150
|
+
xy = self._polygon.exterior.coords.xy
|
|
151
|
+
# Compute second moments of inertia
|
|
152
|
+
self._Iy = marin_integration(xy[0], xy[1], 0, 2)
|
|
153
|
+
return self._Iy
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def Iz(self) -> float:
|
|
157
|
+
"""Returns second moment of area around z axis."""
|
|
158
|
+
if self._Iz is None:
|
|
159
|
+
# Check if the polygon is defined
|
|
160
|
+
self._check_polygon_defined()
|
|
161
|
+
# Get the polygon coordinates:
|
|
162
|
+
xy = self._polygon.exterior.coords.xy
|
|
163
|
+
# Compute second moments of inertia
|
|
164
|
+
self._Iz = marin_integration(xy[0], xy[1], 2, 0)
|
|
165
|
+
return self._Iz
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def Iyz(self) -> float:
|
|
169
|
+
"""Returns product moment of inertia."""
|
|
170
|
+
if self._Iyz is None:
|
|
171
|
+
# Check if the polygon is defined
|
|
172
|
+
self._check_polygon_defined()
|
|
173
|
+
# Get the polygon coordinates:
|
|
174
|
+
xy = self._polygon.exterior.coords.xy
|
|
175
|
+
# Compute product moment of area
|
|
176
|
+
self._Iyz = marin_integration(xy[0], xy[1], 1, 1)
|
|
177
|
+
return self._Iyz
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def Icsi(self) -> float:
|
|
181
|
+
"""Returns second moment of area around principal csi axis.
|
|
182
|
+
|
|
183
|
+
It is assumed that Icsi is maximum second moment, while Ieta is the
|
|
184
|
+
minimum one.
|
|
185
|
+
"""
|
|
186
|
+
if self._Icsi is None:
|
|
187
|
+
self._find_principals_direction_and_moments()
|
|
188
|
+
return self._Icsi
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def Ieta(self) -> float:
|
|
192
|
+
"""Returns second moment of area around principal eta axis.
|
|
193
|
+
|
|
194
|
+
It is assumed that Icsi is maximum second moment, while Ieta is the
|
|
195
|
+
minimum one.
|
|
196
|
+
"""
|
|
197
|
+
if self._Ieta is None:
|
|
198
|
+
self._find_principals_direction_and_moments()
|
|
199
|
+
return self._Ieta
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def theta(self) -> float:
|
|
203
|
+
"""Returns angle between x and principal eta axis.
|
|
204
|
+
|
|
205
|
+
It is assumed that Icsi is maximum second moment, while Ieta is the
|
|
206
|
+
minimum one.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
float: The angle in radians.
|
|
210
|
+
"""
|
|
211
|
+
if self._theta is None:
|
|
212
|
+
self._find_principals_direction_and_moments()
|
|
213
|
+
return self._theta
|
|
214
|
+
|
|
215
|
+
def _compute_elastic_moduli(self):
|
|
216
|
+
"""Compute elastic moduli Wely and Welz."""
|
|
217
|
+
# Check if the polygon is defined
|
|
218
|
+
self._check_polygon_defined()
|
|
219
|
+
# For computing section modulus get bounds
|
|
220
|
+
bounds = self._polygon.bounds
|
|
221
|
+
xmax = max(abs(bounds[0]), bounds[2])
|
|
222
|
+
ymax = max(abs(bounds[1]), bounds[3])
|
|
223
|
+
# Then compute section modulus
|
|
224
|
+
self._Wely = self.Iy / ymax
|
|
225
|
+
self._Welz = self.Iz / xmax
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def Wely(self) -> float:
|
|
229
|
+
"""Returns section modulus in y direction."""
|
|
230
|
+
if self._Wely is None:
|
|
231
|
+
# Compute elastic moduli
|
|
232
|
+
self._compute_elastic_moduli()
|
|
233
|
+
return self._Wely
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def Welz(self) -> float:
|
|
237
|
+
"""Returns section modulus in z direction."""
|
|
238
|
+
if self._Welz is None:
|
|
239
|
+
# Compute elastic moduli
|
|
240
|
+
self._compute_elastic_moduli()
|
|
241
|
+
return self._Welz
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def Wply(self) -> float:
|
|
245
|
+
"""Returns plastic section modulus in y direction."""
|
|
246
|
+
if self._Wply is None:
|
|
247
|
+
# Check if the polygon is defined
|
|
248
|
+
self._check_polygon_defined()
|
|
249
|
+
# For computing section modulus get bounds
|
|
250
|
+
bounds = self._polygon.bounds
|
|
251
|
+
xmax = max(abs(bounds[0]), bounds[2])
|
|
252
|
+
# Compute plastic section modulus
|
|
253
|
+
# find plastic neutral axis parallel to y
|
|
254
|
+
self._Wply = 0
|
|
255
|
+
z_pna = self._find_plastic_neutral_axis_y()
|
|
256
|
+
poly = translate(self._polygon, xoff=0, yoff=-z_pna)
|
|
257
|
+
result = split(
|
|
258
|
+
poly,
|
|
259
|
+
LineString([[-xmax * 1.05, 0], [xmax * 1.05, 0]]),
|
|
260
|
+
)
|
|
261
|
+
for poly in result.geoms:
|
|
262
|
+
xy = poly.exterior.coords.xy
|
|
263
|
+
self._Wply += abs(marin_integration(xy[0], xy[1], 0, 1))
|
|
264
|
+
return self._Wply
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def Wplz(self) -> float:
|
|
268
|
+
"""Returns plastic section modulus in z direction."""
|
|
269
|
+
if self._Wplz is None:
|
|
270
|
+
# Check if the polygon is defined
|
|
271
|
+
self._check_polygon_defined()
|
|
272
|
+
# For computing section modulus get bounds
|
|
273
|
+
bounds = self._polygon.bounds
|
|
274
|
+
ymax = max(abs(bounds[1]), bounds[3])
|
|
275
|
+
# Compute plastic section modulus
|
|
276
|
+
# # find plastic neutral axis parallel to z
|
|
277
|
+
self._polygon = rotate(geom=self._polygon, angle=90, origin=(0, 0))
|
|
278
|
+
self._Wplz = 0
|
|
279
|
+
y_pna = self._find_plastic_neutral_axis_y()
|
|
280
|
+
poly = translate(self._polygon, xoff=0, yoff=-y_pna)
|
|
281
|
+
result = split(
|
|
282
|
+
poly,
|
|
283
|
+
LineString([[-ymax * 1.05, 0], [ymax * 1.05, 0]]),
|
|
284
|
+
)
|
|
285
|
+
for poly in result.geoms:
|
|
286
|
+
xy = poly.exterior.coords.xy
|
|
287
|
+
self._Wplz += abs(marin_integration(xy[0], xy[1], 0, 1))
|
|
288
|
+
self._polygon = rotate(
|
|
289
|
+
geom=self._polygon, angle=-90, origin=(0, 0)
|
|
290
|
+
)
|
|
291
|
+
return self._Wplz
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def iy(self) -> float:
|
|
295
|
+
"""Returns radius of inertia of profile."""
|
|
296
|
+
# Compute radius of inertia
|
|
297
|
+
self._iy = self._iy or (self.Iy / self.A) ** 0.5
|
|
298
|
+
return self._iy
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def iz(self) -> float:
|
|
302
|
+
"""Returns radius of inertia of profile."""
|
|
303
|
+
# Compute radius of inertia
|
|
304
|
+
self._iz = self._iz or (self.Iz / self.A) ** 0.5
|
|
305
|
+
return self._iz
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Common functions for creating profiles."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from shapely import (
|
|
7
|
+
LineString,
|
|
8
|
+
Point,
|
|
9
|
+
Polygon,
|
|
10
|
+
get_geometry,
|
|
11
|
+
polygonize,
|
|
12
|
+
set_precision,
|
|
13
|
+
)
|
|
14
|
+
from shapely.affinity import scale, translate
|
|
15
|
+
from shapely.geometry.polygon import orient
|
|
16
|
+
from shapely.ops import linemerge, unary_union
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _create_I_section(h: float, b: float, tw: float, tf: float, r: float):
|
|
20
|
+
"""Returns a polygon for a I section."""
|
|
21
|
+
# top flange
|
|
22
|
+
top_flange = Polygon(
|
|
23
|
+
[
|
|
24
|
+
(-b / 2, -h / 2),
|
|
25
|
+
(b / 2, -h / 2),
|
|
26
|
+
(b / 2, -h / 2 + tf),
|
|
27
|
+
(-b / 2, -h / 2 + tf),
|
|
28
|
+
]
|
|
29
|
+
)
|
|
30
|
+
# bottom flange
|
|
31
|
+
bottom_flange = translate(top_flange, xoff=0, yoff=h - tf)
|
|
32
|
+
web = Polygon(
|
|
33
|
+
[
|
|
34
|
+
(-tw / 2, -h / 2 + tf),
|
|
35
|
+
(tw / 2, -h / 2 + tf),
|
|
36
|
+
(tw / 2, h / 2 - tf),
|
|
37
|
+
(-tw / 2, h / 2 - tf),
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
# fillets
|
|
41
|
+
p = Point([tw / 2 + r, -h / 2 + tf + r]).buffer(r)
|
|
42
|
+
s = Polygon(
|
|
43
|
+
[
|
|
44
|
+
(tw / 2, -h / 2 + tf),
|
|
45
|
+
(tw / 2 + r, -h / 2 + tf),
|
|
46
|
+
(tw / 2 + r, -h / 2 + tf + r),
|
|
47
|
+
(tw / 2, -h / 2 + tf + r),
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
fillet = s.difference(p)
|
|
51
|
+
p = Point([-tw / 2 - r, -h / 2 + tf + r]).buffer(r)
|
|
52
|
+
s = Polygon(
|
|
53
|
+
[
|
|
54
|
+
(-tw / 2 - r, -h / 2 + tf),
|
|
55
|
+
(-tw / 2, -h / 2 + tf),
|
|
56
|
+
(-tw / 2, -h / 2 + tf + r),
|
|
57
|
+
(-tw / 2 - r, -h / 2 + tf + r),
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
fillet = s.difference(p).union(fillet)
|
|
61
|
+
fillet = translate(
|
|
62
|
+
scale(fillet, 1, -1), xoff=0, yoff=h - 2 * tf - r
|
|
63
|
+
).union(fillet)
|
|
64
|
+
|
|
65
|
+
# Estimate grid_size value
|
|
66
|
+
# Tentative geometry (due to approximations can be a MultiPolygon)
|
|
67
|
+
geom_trial = unary_union([fillet, top_flange, bottom_flange, web])
|
|
68
|
+
# minx, miny, maxx, maxy
|
|
69
|
+
bounds = geom_trial.bounds
|
|
70
|
+
min_size = min(bounds[2] - bounds[0], bounds[3] - bounds[1])
|
|
71
|
+
grid_size = 10 ** int(math.floor(math.log10(abs(min_size)))) * 1e-12
|
|
72
|
+
# Create the geometry
|
|
73
|
+
geometries = [
|
|
74
|
+
set_precision(geometry, grid_size=grid_size)
|
|
75
|
+
for geometry in [fillet, top_flange, bottom_flange, web]
|
|
76
|
+
]
|
|
77
|
+
return orient(unary_union(geometries), 1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _create_taper_I_section(
|
|
81
|
+
h: float,
|
|
82
|
+
b: float,
|
|
83
|
+
tw: float,
|
|
84
|
+
tf: float,
|
|
85
|
+
r1: float,
|
|
86
|
+
r2: float,
|
|
87
|
+
slope: float,
|
|
88
|
+
) -> Polygon:
|
|
89
|
+
"""Returns a shapely polygon representing a Taper Flange I Section."""
|
|
90
|
+
# Create first part of line
|
|
91
|
+
ls = [
|
|
92
|
+
set_precision(
|
|
93
|
+
LineString([[0, -h / 2], [b / 2, -h / 2]]), grid_size=1e-13
|
|
94
|
+
)
|
|
95
|
+
]
|
|
96
|
+
# Create first fillet
|
|
97
|
+
xy = np.array(
|
|
98
|
+
[
|
|
99
|
+
[b / 2, -h / 2],
|
|
100
|
+
[b / 2, -h / 2 + tf - (b / 4 * slope)],
|
|
101
|
+
[b / 4, -h / 2 + tf],
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
ls.append(
|
|
105
|
+
set_precision(
|
|
106
|
+
LineString(xy).offset_curve(r2).offset_curve(-r2), grid_size=1e-13
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
# Create second fillet
|
|
110
|
+
xy = np.array(
|
|
111
|
+
[
|
|
112
|
+
[b / 4, -h / 2 + tf],
|
|
113
|
+
[tw / 2, -h / 2 + tf + (b / 4 - tw / 2) * slope],
|
|
114
|
+
[tw / 2, 0],
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
ls.append(
|
|
118
|
+
set_precision(
|
|
119
|
+
LineString(xy).offset_curve(-r1).offset_curve(r1), grid_size=1e-13
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Merge filleted
|
|
124
|
+
merged_ls = linemerge(ls)
|
|
125
|
+
|
|
126
|
+
# mirror the parts
|
|
127
|
+
merged_ls = linemerge(
|
|
128
|
+
[
|
|
129
|
+
merged_ls,
|
|
130
|
+
translate(scale(merged_ls, -1, 1), -b / 2, 0),
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
merged_ls = linemerge(
|
|
134
|
+
[
|
|
135
|
+
merged_ls,
|
|
136
|
+
translate(scale(merged_ls, 1, -1), 0, h / 2),
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Create a polygon
|
|
141
|
+
poly = polygonize([merged_ls])
|
|
142
|
+
|
|
143
|
+
# Return the first and only polygon of this collection
|
|
144
|
+
return orient(get_geometry(poly, 0), 1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _create_taper_U_section(
|
|
148
|
+
h: float,
|
|
149
|
+
b: float,
|
|
150
|
+
tw: float,
|
|
151
|
+
tf: float,
|
|
152
|
+
r1: float,
|
|
153
|
+
r2: float,
|
|
154
|
+
slope: float,
|
|
155
|
+
u: float,
|
|
156
|
+
) -> Polygon:
|
|
157
|
+
"""Returns a shapely polygon representing a Taper Flange U Section."""
|
|
158
|
+
# Create first part of line
|
|
159
|
+
ls = [
|
|
160
|
+
set_precision(
|
|
161
|
+
LineString([[0, h / 2], [0, 0], [b, 0]]), grid_size=1e-13
|
|
162
|
+
)
|
|
163
|
+
]
|
|
164
|
+
# Create first fillet
|
|
165
|
+
xy = np.array([[b, 0], [b, tf - slope * u], [b - u, tf]])
|
|
166
|
+
ls.append(
|
|
167
|
+
set_precision(
|
|
168
|
+
LineString(xy).offset_curve(r2).offset_curve(-r2), grid_size=1e-13
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
# Create second fillet
|
|
172
|
+
xy = np.array(
|
|
173
|
+
[
|
|
174
|
+
[b - u, tf],
|
|
175
|
+
[tw, tf + slope * (b - u - tw)],
|
|
176
|
+
[tw, h / 2],
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
ls.append(
|
|
180
|
+
set_precision(
|
|
181
|
+
LineString(xy).offset_curve(-r1).offset_curve(r1), grid_size=1e-13
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Merge filleted
|
|
186
|
+
merged_ls = linemerge(ls)
|
|
187
|
+
|
|
188
|
+
# mirror the parts
|
|
189
|
+
merged_ls = linemerge(
|
|
190
|
+
[
|
|
191
|
+
merged_ls,
|
|
192
|
+
translate(scale(merged_ls, 1, -1), 0, h / 2),
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Create a polygon
|
|
197
|
+
poly = polygonize([merged_ls])
|
|
198
|
+
|
|
199
|
+
# Get the first and only polygon of this collection
|
|
200
|
+
poly = get_geometry(poly, 0)
|
|
201
|
+
# Translate it to the centroid when returning
|
|
202
|
+
return orient(
|
|
203
|
+
translate(poly, xoff=-poly.centroid.x, yoff=-poly.centroid.y), 1
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _create_parallel_U_section(
|
|
208
|
+
h: float,
|
|
209
|
+
b: float,
|
|
210
|
+
tw: float,
|
|
211
|
+
tf: float,
|
|
212
|
+
r: float,
|
|
213
|
+
) -> Polygon:
|
|
214
|
+
"""Returns a shapely polygon representing a Parallel Flange U Section."""
|
|
215
|
+
# top flange
|
|
216
|
+
top_flange = Polygon(
|
|
217
|
+
[
|
|
218
|
+
(0, h / 2 - tf),
|
|
219
|
+
(b, h / 2 - tf),
|
|
220
|
+
(b, h / 2),
|
|
221
|
+
(0, h / 2),
|
|
222
|
+
]
|
|
223
|
+
)
|
|
224
|
+
# bottom flange
|
|
225
|
+
bottom_flange = translate(top_flange, xoff=0, yoff=-h + tf)
|
|
226
|
+
web = Polygon(
|
|
227
|
+
[
|
|
228
|
+
(0, -h / 2 + tf),
|
|
229
|
+
(tw, -h / 2 + tf),
|
|
230
|
+
(tw, h / 2 - tf),
|
|
231
|
+
(0, h / 2 - tf),
|
|
232
|
+
]
|
|
233
|
+
)
|
|
234
|
+
# fillets
|
|
235
|
+
p = Point([tw + r, -h / 2 + tf + r]).buffer(r)
|
|
236
|
+
s = Polygon(
|
|
237
|
+
[
|
|
238
|
+
(tw, -h / 2 + tf),
|
|
239
|
+
(tw + r, -h / 2 + tf),
|
|
240
|
+
(tw + r, -h / 2 + tf + r),
|
|
241
|
+
(tw, -h / 2 + tf + r),
|
|
242
|
+
]
|
|
243
|
+
)
|
|
244
|
+
fillet = s.difference(p)
|
|
245
|
+
fillet = translate(
|
|
246
|
+
scale(fillet, 1, -1), xoff=0, yoff=h - 2 * tf - r
|
|
247
|
+
).union(fillet)
|
|
248
|
+
# Estimate grid_size value
|
|
249
|
+
# Tentative geometry (due to approximations can be a MultiPolygon)
|
|
250
|
+
geom_trial = unary_union([fillet, top_flange, bottom_flange, web])
|
|
251
|
+
# minx, miny, maxx, maxy
|
|
252
|
+
bounds = geom_trial.bounds
|
|
253
|
+
min_size = min(bounds[2] - bounds[0], bounds[3] - bounds[1])
|
|
254
|
+
grid_size = 10 ** int(math.floor(math.log10(abs(min_size)))) * 1e-12
|
|
255
|
+
# Create the geometry
|
|
256
|
+
geometries = [
|
|
257
|
+
set_precision(geometry, grid_size=grid_size)
|
|
258
|
+
for geometry in [fillet, top_flange, bottom_flange, web]
|
|
259
|
+
]
|
|
260
|
+
geometry = orient(unary_union(geometries), 1)
|
|
261
|
+
# Return the geometry centered at the origin
|
|
262
|
+
return translate(
|
|
263
|
+
geometry, xoff=-geometry.centroid.x, yoff=-geometry.centroid.y
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _create_L_section(
|
|
268
|
+
h: float, b: float, t1: float, t2: float, r1: float, r2: float
|
|
269
|
+
) -> Polygon:
|
|
270
|
+
"""Returns a shapely polygon representing a L Section."""
|
|
271
|
+
# Create first part of line
|
|
272
|
+
ls = [set_precision(LineString([[0, h], [0, 0], [b, 0]]), grid_size=1e-13)]
|
|
273
|
+
# Create fillet
|
|
274
|
+
xy = np.array([[b, 0], [b, t1], [b / 2, t1]])
|
|
275
|
+
ls.append(
|
|
276
|
+
set_precision(
|
|
277
|
+
LineString(xy).offset_curve(r2).offset_curve(-r2), grid_size=1e-13
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
# Create second fillet
|
|
281
|
+
xy = np.array([[b / 2, t1], [t2, t1], [t2, h / 2]])
|
|
282
|
+
ls.append(
|
|
283
|
+
set_precision(
|
|
284
|
+
LineString(xy).offset_curve(-r1).offset_curve(r1), grid_size=1e-13
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
# Last fillet
|
|
288
|
+
xy = np.array([[t2, h / 2], [t2, h], [0, h]])
|
|
289
|
+
ls.append(
|
|
290
|
+
set_precision(
|
|
291
|
+
LineString(xy).offset_curve(r2).offset_curve(-r2), grid_size=1e-13
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
# Merge filleted
|
|
295
|
+
merged_ls = linemerge(ls)
|
|
296
|
+
|
|
297
|
+
merged_ls
|
|
298
|
+
|
|
299
|
+
# Create a polygon
|
|
300
|
+
poly = polygonize([merged_ls])
|
|
301
|
+
|
|
302
|
+
# Return the first and only polygon of this collection
|
|
303
|
+
poly = get_geometry(poly, 0)
|
|
304
|
+
# Translate it to the centroid when returning
|
|
305
|
+
return orient(
|
|
306
|
+
translate(poly, xoff=-poly.centroid.x, yoff=-poly.centroid.y), 1
|
|
307
|
+
)
|