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.

Files changed (48) hide show
  1. structuralcodes/__init__.py +1 -1
  2. structuralcodes/codes/ec2_2004/shear.py +3 -2
  3. structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +2 -2
  4. structuralcodes/core/base.py +138 -12
  5. structuralcodes/geometry/__init__.py +2 -8
  6. structuralcodes/geometry/_geometry.py +103 -19
  7. structuralcodes/geometry/_reinforcement.py +1 -3
  8. structuralcodes/geometry/profiles/__init__.py +33 -0
  9. structuralcodes/geometry/profiles/_base_profile.py +305 -0
  10. structuralcodes/geometry/profiles/_common_functions.py +307 -0
  11. structuralcodes/geometry/profiles/_hd.py +374 -0
  12. structuralcodes/geometry/profiles/_he.py +192 -0
  13. structuralcodes/geometry/profiles/_hp.py +319 -0
  14. structuralcodes/geometry/profiles/_ipe.py +130 -0
  15. structuralcodes/geometry/profiles/_ipn.py +329 -0
  16. structuralcodes/geometry/profiles/_l.py +528 -0
  17. structuralcodes/geometry/profiles/_li.py +217 -0
  18. structuralcodes/geometry/profiles/_u.py +173 -0
  19. structuralcodes/geometry/profiles/_ub.py +1356 -0
  20. structuralcodes/geometry/profiles/_ubp.py +227 -0
  21. structuralcodes/geometry/profiles/_uc.py +276 -0
  22. structuralcodes/geometry/profiles/_upe.py +133 -0
  23. structuralcodes/geometry/profiles/_upn.py +315 -0
  24. structuralcodes/geometry/profiles/_w.py +2157 -0
  25. structuralcodes/materials/basic/_elastic.py +18 -1
  26. structuralcodes/materials/basic/_elasticplastic.py +18 -1
  27. structuralcodes/materials/basic/_generic.py +18 -1
  28. structuralcodes/materials/concrete/__init__.py +3 -0
  29. structuralcodes/materials/concrete/_concrete.py +10 -1
  30. structuralcodes/materials/concrete/_concreteEC2_2004.py +15 -1
  31. structuralcodes/materials/concrete/_concreteEC2_2023.py +15 -1
  32. structuralcodes/materials/concrete/_concreteMC2010.py +20 -1
  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 +14 -0
  39. structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +14 -0
  40. structuralcodes/materials/reinforcement/_reinforcementMC2010.py +14 -0
  41. structuralcodes/sections/section_integrators/__init__.py +3 -1
  42. structuralcodes/sections/section_integrators/_marin_integrator.py +1 -1
  43. {structuralcodes-0.5.0.dist-info → structuralcodes-0.6.1.dist-info}/METADATA +2 -2
  44. {structuralcodes-0.5.0.dist-info → structuralcodes-0.6.1.dist-info}/RECORD +47 -30
  45. structuralcodes/geometry/_steel_sections.py +0 -2155
  46. /structuralcodes/{sections/section_integrators → core}/_marin_integration.py +0 -0
  47. {structuralcodes-0.5.0.dist-info → structuralcodes-0.6.1.dist-info}/WHEEL +0 -0
  48. {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
+ )