structuralcodes 0.4.0__py3-none-any.whl → 0.6.0__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.

Files changed (52) hide show
  1. structuralcodes/__init__.py +1 -1
  2. structuralcodes/codes/ec2_2004/__init__.py +2 -0
  3. structuralcodes/codes/ec2_2004/shear.py +44 -4
  4. structuralcodes/codes/mc2010/__init__.py +18 -0
  5. structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +2 -2
  6. structuralcodes/codes/mc2010/_concrete_punching.py +300 -388
  7. structuralcodes/core/base.py +116 -5
  8. structuralcodes/geometry/__init__.py +2 -8
  9. structuralcodes/geometry/_circular.py +3 -10
  10. structuralcodes/geometry/_geometry.py +58 -114
  11. structuralcodes/geometry/_rectangular.py +3 -10
  12. structuralcodes/geometry/_reinforcement.py +9 -14
  13. structuralcodes/geometry/profiles/__init__.py +19 -0
  14. structuralcodes/geometry/profiles/_base_profile.py +305 -0
  15. structuralcodes/geometry/profiles/_common_functions.py +194 -0
  16. structuralcodes/geometry/profiles/_he.py +192 -0
  17. structuralcodes/geometry/profiles/_ipe.py +130 -0
  18. structuralcodes/geometry/profiles/_ipn.py +329 -0
  19. structuralcodes/geometry/profiles/_ub.py +264 -0
  20. structuralcodes/geometry/profiles/_ubp.py +227 -0
  21. structuralcodes/geometry/profiles/_uc.py +276 -0
  22. structuralcodes/geometry/profiles/_upn.py +315 -0
  23. structuralcodes/materials/__init__.py +2 -1
  24. structuralcodes/materials/basic/__init__.py +11 -0
  25. structuralcodes/materials/basic/_elastic.py +69 -0
  26. structuralcodes/materials/basic/_elasticplastic.py +92 -0
  27. structuralcodes/materials/basic/_generic.py +43 -0
  28. structuralcodes/materials/concrete/__init__.py +3 -0
  29. structuralcodes/materials/concrete/_concrete.py +10 -1
  30. structuralcodes/materials/concrete/_concreteEC2_2004.py +14 -0
  31. structuralcodes/materials/concrete/_concreteEC2_2023.py +14 -0
  32. structuralcodes/materials/concrete/_concreteMC2010.py +19 -0
  33. structuralcodes/materials/constitutive_laws/__init__.py +3 -0
  34. structuralcodes/materials/constitutive_laws/_elasticplastic.py +2 -2
  35. structuralcodes/materials/constitutive_laws/_initial_strain.py +130 -0
  36. structuralcodes/materials/reinforcement/__init__.py +6 -0
  37. structuralcodes/materials/reinforcement/_reinforcement.py +10 -1
  38. structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +15 -1
  39. structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +15 -1
  40. structuralcodes/materials/reinforcement/_reinforcementMC2010.py +15 -1
  41. structuralcodes/sections/_generic.py +53 -14
  42. structuralcodes/sections/_rc_utils.py +15 -5
  43. structuralcodes/sections/section_integrators/__init__.py +3 -1
  44. structuralcodes/sections/section_integrators/_fiber_integrator.py +19 -11
  45. structuralcodes/sections/section_integrators/_marin_integrator.py +25 -20
  46. {structuralcodes-0.4.0.dist-info → structuralcodes-0.6.0.dist-info}/METADATA +2 -2
  47. structuralcodes-0.6.0.dist-info/RECORD +77 -0
  48. structuralcodes/geometry/_steel_sections.py +0 -2155
  49. structuralcodes-0.4.0.dist-info/RECORD +0 -63
  50. /structuralcodes/{sections/section_integrators → core}/_marin_integration.py +0 -0
  51. {structuralcodes-0.4.0.dist-info → structuralcodes-0.6.0.dist-info}/WHEEL +0 -0
  52. {structuralcodes-0.4.0.dist-info → structuralcodes-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ import typing as t
6
6
  import numpy as np
7
7
  from shapely import Point
8
8
 
9
- from structuralcodes.core.base import ConstitutiveLaw, Material
9
+ from structuralcodes.core.base import Material
10
10
 
11
11
  from ._geometry import CompoundGeometry, PointGeometry, SurfaceGeometry
12
12
 
@@ -15,7 +15,7 @@ def add_reinforcement(
15
15
  geo: t.Union[SurfaceGeometry, CompoundGeometry],
16
16
  coords: t.Tuple[float, float],
17
17
  diameter: float,
18
- material: t.Union[Material, ConstitutiveLaw],
18
+ material: Material,
19
19
  group_label: t.Optional[str] = None,
20
20
  ) -> CompoundGeometry:
21
21
  """Add a single bar given coordinate.
@@ -25,8 +25,7 @@ def add_reinforcement(
25
25
  reinforcement.
26
26
  coords (Tuple(float, float)): A tuple with cordinates of bar.
27
27
  diameter (float): The diameter of the reinforcement.
28
- material (Union(Material, ConstitutiveLaw)): A material or a
29
- constitutive law for the behavior of the reinforcement.
28
+ material (Material): A material for the reinforcement.
30
29
  group_label (Optional(str)): A label for grouping several objects
31
30
  (default is None).
32
31
 
@@ -45,7 +44,7 @@ def add_reinforcement_line(
45
44
  coords_i: t.Tuple[float, float],
46
45
  coords_j: t.Tuple[float, float],
47
46
  diameter: float,
48
- material: t.Union[Material, ConstitutiveLaw],
47
+ material: Material,
49
48
  n: int = 0,
50
49
  s: float = 0.0,
51
50
  first: bool = True,
@@ -60,9 +59,8 @@ def add_reinforcement_line(
60
59
  coords_i (Tuple(float, float)): Coordinates of the initial point of
61
60
  line.
62
61
  coords_j (Tuple(float, float)): Coordinates of the final point of line.
63
- diamter (float): The diameter of the bars.
64
- material (Union(Material, ConstitutiveLaw)): A valid material or
65
- constitutive law.
62
+ diameter (float): The diameter of the bars.
63
+ material (Material): A material for the reinforcement.
66
64
  n (int): The number of bars to be distributed inside the line (default
67
65
  = 0).
68
66
  s (float): The distance between the bars (default = 0).
@@ -80,8 +78,6 @@ def add_reinforcement_line(
80
78
  CompoundGeometry: A compound geometry with the original geometry and
81
79
  the reinforcement.
82
80
  """
83
- from math import floor
84
-
85
81
  p1 = np.array(coords_i)
86
82
  p2 = np.array(coords_j)
87
83
  distance = np.linalg.norm(p2 - p1)
@@ -102,7 +98,7 @@ def add_reinforcement_line(
102
98
  elif s > 0:
103
99
  # Provided the spacing
104
100
  # 1. Compute the number of bars
105
- n = floor(distance / s) + 1
101
+ n = math.floor(distance / s) + 1
106
102
  # 2. Distribute the bars centered in the segment
107
103
  d = (n - 1) * s
108
104
  p1 = p1 + v * (distance - d) / 2.0
@@ -130,7 +126,7 @@ def add_reinforcement_circle(
130
126
  center: t.Tuple[float, float],
131
127
  radius: float,
132
128
  diameter: float,
133
- material: t.Union[Material, ConstitutiveLaw],
129
+ material: Material,
134
130
  n: int = 0,
135
131
  s: float = 0.0,
136
132
  first: bool = True,
@@ -152,8 +148,7 @@ def add_reinforcement_circle(
152
148
  radius (float): Radius of the circle line where reinforcement will be
153
149
  added.
154
150
  diameter (float): The diameter of the bars.
155
- material (Union(Material, ConstitutiveLaw)): A valid material or
156
- constitutive law.
151
+ material (Material): A material for the reinforcement.
157
152
  n (int): The number of bars to be distributed inside the line (default
158
153
  = 0).
159
154
  s (float): The distance between the bars (default = 0).
@@ -0,0 +1,19 @@
1
+ """Main entry point for profiles."""
2
+
3
+ from ._he import HE
4
+ from ._ipe import IPE
5
+ from ._ipn import IPN
6
+ from ._ub import UB
7
+ from ._ubp import UBP
8
+ from ._uc import UC
9
+ from ._upn import UPN
10
+
11
+ __all__ = [
12
+ 'HE',
13
+ 'IPE',
14
+ 'IPN',
15
+ 'UB',
16
+ 'UBP',
17
+ 'UC',
18
+ 'UPN',
19
+ ]
@@ -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,194 @@
1
+ """Common functions for creating profiles."""
2
+
3
+ import numpy as np
4
+ from shapely import (
5
+ LineString,
6
+ Point,
7
+ Polygon,
8
+ get_geometry,
9
+ polygonize,
10
+ set_precision,
11
+ )
12
+ from shapely.affinity import scale, translate
13
+ from shapely.geometry.polygon import orient
14
+ from shapely.ops import linemerge, unary_union
15
+
16
+
17
+ def _create_I_section(h: float, b: float, tw: float, tf: float, r: float):
18
+ """Returns a polygon for a I section."""
19
+ # top flange
20
+ top_flange = Polygon(
21
+ [
22
+ (-b / 2, -h / 2),
23
+ (b / 2, -h / 2),
24
+ (b / 2, -h / 2 + tf),
25
+ (-b / 2, -h / 2 + tf),
26
+ ]
27
+ )
28
+ # bottom flange
29
+ bottom_flange = translate(top_flange, xoff=0, yoff=h - tf)
30
+ web = Polygon(
31
+ [
32
+ (-tw / 2, -h / 2 + tf),
33
+ (tw / 2, -h / 2 + tf),
34
+ (tw / 2, h / 2 - tf),
35
+ (-tw / 2, h / 2 - tf),
36
+ ]
37
+ )
38
+ # fillets
39
+ p = Point([tw / 2 + r, -h / 2 + tf + r]).buffer(r)
40
+ s = Polygon(
41
+ [
42
+ (tw / 2, -h / 2 + tf),
43
+ (tw / 2 + r, -h / 2 + tf),
44
+ (tw / 2 + r, -h / 2 + tf + r),
45
+ (tw / 2, -h / 2 + tf + r),
46
+ ]
47
+ )
48
+ fillet = s.difference(p)
49
+ p = Point([-tw / 2 - r, -h / 2 + tf + r]).buffer(r)
50
+ s = Polygon(
51
+ [
52
+ (-tw / 2 - r, -h / 2 + tf),
53
+ (-tw / 2, -h / 2 + tf),
54
+ (-tw / 2, -h / 2 + tf + r),
55
+ (-tw / 2 - r, -h / 2 + tf + r),
56
+ ]
57
+ )
58
+ fillet = s.difference(p).union(fillet)
59
+ fillet = translate(
60
+ scale(fillet, 1, -1), xoff=0, yoff=h - 2 * tf - r
61
+ ).union(fillet)
62
+ # Create the geometry
63
+ geometries = [
64
+ set_precision(geometry, grid_size=1e-13)
65
+ for geometry in [fillet, top_flange, bottom_flange, web]
66
+ ]
67
+ return orient(unary_union(geometries), 1)
68
+
69
+
70
+ def _create_taper_I_section(
71
+ h: float,
72
+ b: float,
73
+ tw: float,
74
+ tf: float,
75
+ r1: float,
76
+ r2: float,
77
+ slope: float,
78
+ ) -> Polygon:
79
+ """Returns a shapely polygon representing a Taper Flange I Section."""
80
+ # Create first part of line
81
+ ls = [
82
+ set_precision(
83
+ LineString([[0, -h / 2], [b / 2, -h / 2]]), grid_size=1e-13
84
+ )
85
+ ]
86
+ # Create first fillet
87
+ xy = np.array(
88
+ [
89
+ [b / 2, -h / 2],
90
+ [b / 2, -h / 2 + tf - (b / 4 * slope)],
91
+ [b / 4, -h / 2 + tf],
92
+ ]
93
+ )
94
+ ls.append(
95
+ set_precision(
96
+ LineString(xy).offset_curve(r2).offset_curve(-r2), grid_size=1e-13
97
+ )
98
+ )
99
+ # Create second fillet
100
+ xy = np.array(
101
+ [
102
+ [b / 4, -h / 2 + tf],
103
+ [tw / 2, -h / 2 + tf + (b / 4 - tw / 2) * slope],
104
+ [tw / 2, 0],
105
+ ]
106
+ )
107
+ ls.append(
108
+ set_precision(
109
+ LineString(xy).offset_curve(-r1).offset_curve(r1), grid_size=1e-13
110
+ )
111
+ )
112
+
113
+ # Merge filleted
114
+ merged_ls = linemerge(ls)
115
+
116
+ # mirror the parts
117
+ merged_ls = linemerge(
118
+ [
119
+ merged_ls,
120
+ translate(scale(merged_ls, -1, 1), -b / 2, 0),
121
+ ]
122
+ )
123
+ merged_ls = linemerge(
124
+ [
125
+ merged_ls,
126
+ translate(scale(merged_ls, 1, -1), 0, h / 2),
127
+ ]
128
+ )
129
+
130
+ # Create a polygon
131
+ poly = polygonize([merged_ls])
132
+
133
+ # Return the first and only polygon of this collection
134
+ return orient(get_geometry(poly, 0), 1)
135
+
136
+
137
+ def _create_taper_U_section(
138
+ h: float,
139
+ b: float,
140
+ tw: float,
141
+ tf: float,
142
+ r1: float,
143
+ r2: float,
144
+ slope: float,
145
+ u: float,
146
+ ) -> Polygon:
147
+ """Returns a shapely polygon representing a Taper Flange U Section."""
148
+ # Create first part of line
149
+ ls = [
150
+ set_precision(
151
+ LineString([[0, h / 2], [0, 0], [b, 0]]), grid_size=1e-13
152
+ )
153
+ ]
154
+ # Create first fillet
155
+ xy = np.array([[b, 0], [b, tf - slope * u], [b - u, tf]])
156
+ ls.append(
157
+ set_precision(
158
+ LineString(xy).offset_curve(r2).offset_curve(-r2), grid_size=1e-13
159
+ )
160
+ )
161
+ # Create second fillet
162
+ xy = np.array(
163
+ [
164
+ [b - u, tf],
165
+ [tw, tf + slope * (b - u - tw)],
166
+ [tw, h / 2],
167
+ ]
168
+ )
169
+ ls.append(
170
+ set_precision(
171
+ LineString(xy).offset_curve(-r1).offset_curve(r1), grid_size=1e-13
172
+ )
173
+ )
174
+
175
+ # Merge filleted
176
+ merged_ls = linemerge(ls)
177
+
178
+ # mirror the parts
179
+ merged_ls = linemerge(
180
+ [
181
+ merged_ls,
182
+ translate(scale(merged_ls, 1, -1), 0, h / 2),
183
+ ]
184
+ )
185
+
186
+ # Create a polygon
187
+ poly = polygonize([merged_ls])
188
+
189
+ # Get the first and only polygon of this collection
190
+ poly = get_geometry(poly, 0)
191
+ # Translate it to the centroid when returning
192
+ return orient(
193
+ translate(poly, xoff=-poly.centroid.x, yoff=-poly.centroid.y), 1
194
+ )