structuralcodes 0.0.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 +17 -0
- structuralcodes/codes/__init__.py +79 -0
- structuralcodes/codes/ec2_2004/__init__.py +133 -0
- structuralcodes/codes/ec2_2004/_concrete_material_properties.py +239 -0
- structuralcodes/codes/ec2_2004/_reinforcement_material_properties.py +104 -0
- structuralcodes/codes/ec2_2004/_section_7_3_crack_control.py +941 -0
- structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py +257 -0
- structuralcodes/codes/ec2_2004/shear.py +506 -0
- structuralcodes/codes/ec2_2023/__init__.py +104 -0
- structuralcodes/codes/ec2_2023/_annexB_time_dependent.py +17 -0
- structuralcodes/codes/ec2_2023/_section5_materials.py +1160 -0
- structuralcodes/codes/ec2_2023/_section9_sls.py +325 -0
- structuralcodes/codes/mc2010/__init__.py +169 -0
- structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +704 -0
- structuralcodes/codes/mc2010/_concrete_interface_different_casting_times.py +104 -0
- structuralcodes/codes/mc2010/_concrete_material_properties.py +463 -0
- structuralcodes/codes/mc2010/_concrete_punching.py +543 -0
- structuralcodes/codes/mc2010/_concrete_shear.py +749 -0
- structuralcodes/codes/mc2010/_concrete_torsion.py +164 -0
- structuralcodes/codes/mc2010/_reinforcement_material_properties.py +105 -0
- structuralcodes/core/__init__.py +1 -0
- structuralcodes/core/_section_results.py +211 -0
- structuralcodes/core/base.py +260 -0
- structuralcodes/geometry/__init__.py +25 -0
- structuralcodes/geometry/_geometry.py +875 -0
- structuralcodes/geometry/_steel_sections.py +2155 -0
- structuralcodes/materials/__init__.py +9 -0
- structuralcodes/materials/concrete/__init__.py +82 -0
- structuralcodes/materials/concrete/_concrete.py +114 -0
- structuralcodes/materials/concrete/_concreteEC2_2004.py +477 -0
- structuralcodes/materials/concrete/_concreteEC2_2023.py +435 -0
- structuralcodes/materials/concrete/_concreteMC2010.py +494 -0
- structuralcodes/materials/constitutive_laws.py +979 -0
- structuralcodes/materials/reinforcement/__init__.py +84 -0
- structuralcodes/materials/reinforcement/_reinforcement.py +172 -0
- structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +103 -0
- structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +93 -0
- structuralcodes/materials/reinforcement/_reinforcementMC2010.py +98 -0
- structuralcodes/sections/__init__.py +23 -0
- structuralcodes/sections/_generic.py +1249 -0
- structuralcodes/sections/_reinforcement.py +115 -0
- structuralcodes/sections/section_integrators/__init__.py +14 -0
- structuralcodes/sections/section_integrators/_factory.py +41 -0
- structuralcodes/sections/section_integrators/_fiber_integrator.py +238 -0
- structuralcodes/sections/section_integrators/_marin_integration.py +47 -0
- structuralcodes/sections/section_integrators/_marin_integrator.py +222 -0
- structuralcodes/sections/section_integrators/_section_integrator.py +49 -0
- structuralcodes-0.0.1.dist-info/METADATA +40 -0
- structuralcodes-0.0.1.dist-info/RECORD +50 -0
- structuralcodes-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
"""Generic class section implemenetation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations # To have clean hints of ArrayLike in docs
|
|
4
|
+
|
|
5
|
+
import typing as t
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import ArrayLike
|
|
10
|
+
from shapely import affinity
|
|
11
|
+
from shapely.geometry import (
|
|
12
|
+
LinearRing,
|
|
13
|
+
LineString,
|
|
14
|
+
MultiLineString,
|
|
15
|
+
MultiPolygon,
|
|
16
|
+
Point,
|
|
17
|
+
Polygon,
|
|
18
|
+
)
|
|
19
|
+
from shapely.ops import split
|
|
20
|
+
|
|
21
|
+
from structuralcodes.core.base import ConstitutiveLaw, Material
|
|
22
|
+
from structuralcodes.materials.concrete import Concrete
|
|
23
|
+
from structuralcodes.materials.constitutive_laws import Elastic
|
|
24
|
+
|
|
25
|
+
# Useful classes and functions: where to put?????? (core?
|
|
26
|
+
# utility folder in sections? here in this file?)
|
|
27
|
+
# Polygons, LineStrings, Points, MultiLyneStrings, MultiPolygons etc.
|
|
28
|
+
|
|
29
|
+
# to think: dataclass or class?
|
|
30
|
+
# Note that some things are already computed (like area) by shapely
|
|
31
|
+
# like: polygon.area, polygon.centroid, etc.
|
|
32
|
+
|
|
33
|
+
# For now dataclass, if we need convert to regular class,
|
|
34
|
+
# init commented for now
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Geometry:
|
|
38
|
+
"""Base class for a geometry object."""
|
|
39
|
+
|
|
40
|
+
section_counter: t.ClassVar[int] = 0
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self, name: t.Optional[str] = None, group_label: t.Optional[str] = None
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Initializes a geometry object.
|
|
46
|
+
|
|
47
|
+
The name and grouplabel serve for filtering in a compound object. By
|
|
48
|
+
default it creates a new name each time.
|
|
49
|
+
|
|
50
|
+
Arguments:
|
|
51
|
+
name (Optional(str)): The name to be given to the object.
|
|
52
|
+
group_label (Optional(str)): A label for grouping several objects.
|
|
53
|
+
"""
|
|
54
|
+
if name is not None:
|
|
55
|
+
self._name = name
|
|
56
|
+
else:
|
|
57
|
+
counter = Geometry.return_global_counter_and_increase()
|
|
58
|
+
self._name = f'Geometry_{counter}'
|
|
59
|
+
self._group_label = group_label
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def name(self):
|
|
63
|
+
"""Returns the name of the Geometry."""
|
|
64
|
+
return self._name
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def group_label(self):
|
|
68
|
+
"""Returns the group_label fo the Geometry."""
|
|
69
|
+
return self._group_label
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def _increase_global_counter(cls):
|
|
73
|
+
"""Increases the global counter by one."""
|
|
74
|
+
cls.section_counter += 1
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def return_global_counter_and_increase(cls):
|
|
78
|
+
"""Returns the current counter and increases it by one."""
|
|
79
|
+
counter = cls.section_counter
|
|
80
|
+
cls._increase_global_counter()
|
|
81
|
+
return counter
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def from_geometry(
|
|
85
|
+
geo: Geometry,
|
|
86
|
+
new_material: t.Optional[t.Union[Material, ConstitutiveLaw]] = None,
|
|
87
|
+
) -> Geometry:
|
|
88
|
+
"""Create a new geometry with a different material."""
|
|
89
|
+
raise NotImplementedError(
|
|
90
|
+
'This method should be implemented by subclasses'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PointGeometry(Geometry):
|
|
95
|
+
"""Class for a point geometry with material.
|
|
96
|
+
|
|
97
|
+
Basically it is a wrapper for shapely Point including the material (and
|
|
98
|
+
other parameters that may be needed).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
point: t.Union[Point, ArrayLike],
|
|
104
|
+
diameter: float,
|
|
105
|
+
material: t.Union[Material, ConstitutiveLaw],
|
|
106
|
+
density: t.Optional[float] = None,
|
|
107
|
+
name: t.Optional[str] = None,
|
|
108
|
+
group_label: t.Optional[str] = None,
|
|
109
|
+
):
|
|
110
|
+
"""Initializes a PointGeometry object.
|
|
111
|
+
|
|
112
|
+
The name and group_label serve for filtering in a compound object. By
|
|
113
|
+
default it creates a new name each time.
|
|
114
|
+
|
|
115
|
+
Arguments:
|
|
116
|
+
point (Union(Point, ArrayLike)): A couple of coordinates or a
|
|
117
|
+
shapely Point object.
|
|
118
|
+
diameter (float): The diameter of the point.
|
|
119
|
+
material (Union(Material, ConstitutiveLaw)): The material for the
|
|
120
|
+
point (this can be a Material or a ConstitutiveLaw).
|
|
121
|
+
density (Optional(float)): When a ConstitutiveLaw is passed as
|
|
122
|
+
material, the density can be providen by this argument. When
|
|
123
|
+
the material is a Material object the density is taken from the
|
|
124
|
+
material.
|
|
125
|
+
name (Optional(str)): The name to be given to the object.
|
|
126
|
+
group_label (Optional(str)): A label for grouping several objects
|
|
127
|
+
(default is None).
|
|
128
|
+
"""
|
|
129
|
+
super().__init__(name, group_label)
|
|
130
|
+
# I check if point is a shapely Point or an ArrayLike object
|
|
131
|
+
if not isinstance(point, Point):
|
|
132
|
+
# It is an ArrayLike object -> create the Point given the
|
|
133
|
+
# coordinates x and y (coordinates can be a List, Tuple, np.array,
|
|
134
|
+
# ...)
|
|
135
|
+
coords = np.atleast_1d(point)
|
|
136
|
+
num = len(coords)
|
|
137
|
+
if num < 2:
|
|
138
|
+
raise ValueError('Two coordinates are needed')
|
|
139
|
+
if num > 2:
|
|
140
|
+
warn_str = f'Two coordinates are needed. {num}'
|
|
141
|
+
warn_str += ' coords provided. The extra entries will be'
|
|
142
|
+
warn_str += ' discarded'
|
|
143
|
+
warnings.warn(warn_str)
|
|
144
|
+
point = Point(coords)
|
|
145
|
+
if not isinstance(material, Material) and not isinstance(
|
|
146
|
+
material, ConstitutiveLaw
|
|
147
|
+
):
|
|
148
|
+
raise TypeError(
|
|
149
|
+
f'mat should be a valid structuralcodes.base.Material \
|
|
150
|
+
or structuralcodes.base.ConstitutiveLaw object. \
|
|
151
|
+
{repr(material)}'
|
|
152
|
+
)
|
|
153
|
+
# Pass a constitutive law to the PointGeometry
|
|
154
|
+
self._density = density
|
|
155
|
+
if isinstance(material, Material):
|
|
156
|
+
self._density = material.density
|
|
157
|
+
material = material.constitutive_law
|
|
158
|
+
|
|
159
|
+
self._point = point
|
|
160
|
+
self._diameter = diameter
|
|
161
|
+
self._material = material
|
|
162
|
+
self._area = np.pi * diameter**2 / 4.0
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def diameter(self) -> float:
|
|
166
|
+
"""Returns the point diameter."""
|
|
167
|
+
return self._diameter
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def area(self) -> float:
|
|
171
|
+
"""Returns the point area."""
|
|
172
|
+
return self._area
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def material(self) -> Material:
|
|
176
|
+
"""Returns the point material."""
|
|
177
|
+
return self._material
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def density(self) -> float:
|
|
181
|
+
"""Returns the density."""
|
|
182
|
+
return self._density
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def x(self) -> float:
|
|
186
|
+
"""Returns the x coordinate of the point."""
|
|
187
|
+
return self._point.x
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def y(self) -> float:
|
|
191
|
+
"""Returns the y coordinate of the point."""
|
|
192
|
+
return self._point.y
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def point(self) -> Point:
|
|
196
|
+
"""Returns the shapely Point object."""
|
|
197
|
+
return self._point
|
|
198
|
+
|
|
199
|
+
def _repr_svg_(self) -> str:
|
|
200
|
+
"""Returns the svg representation."""
|
|
201
|
+
return str(self._point._repr_svg_())
|
|
202
|
+
|
|
203
|
+
def translate(self, dx: float = 0.0, dy: float = 0.0) -> PointGeometry:
|
|
204
|
+
"""Returns a new PointGeometry that is translated by dx, dy.
|
|
205
|
+
|
|
206
|
+
Arguments:
|
|
207
|
+
dx (float): Translation in x direction.
|
|
208
|
+
dy (float): Translation in y direction.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
PointGeometry: A new, translated point.
|
|
212
|
+
"""
|
|
213
|
+
return PointGeometry(
|
|
214
|
+
point=affinity.translate(self._point, dx, dy),
|
|
215
|
+
diameter=self._diameter,
|
|
216
|
+
material=self._material,
|
|
217
|
+
density=self._density,
|
|
218
|
+
name=self._name,
|
|
219
|
+
group_label=self._group_label,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def rotate(
|
|
223
|
+
self,
|
|
224
|
+
angle: float = 0.0,
|
|
225
|
+
point: t.Union[t.Tuple[float, float], Point] = (0.0, 0.0),
|
|
226
|
+
use_radians: bool = True,
|
|
227
|
+
) -> PointGeometry:
|
|
228
|
+
"""Returns a new PointGeometry that is rotated by angle.
|
|
229
|
+
|
|
230
|
+
Arguments:
|
|
231
|
+
angle (float): Rotation angle in radians (if use_radians = True),
|
|
232
|
+
or degress (if use_radians = False).
|
|
233
|
+
point (Union(Point, Tuple(float, float))): The origin of the
|
|
234
|
+
rotation.
|
|
235
|
+
use_radians (bool): True if angle is in radians, and False if angle
|
|
236
|
+
is in degrees.
|
|
237
|
+
"""
|
|
238
|
+
return PointGeometry(
|
|
239
|
+
point=affinity.rotate(
|
|
240
|
+
self._point, angle, origin=point, use_radians=use_radians
|
|
241
|
+
),
|
|
242
|
+
diameter=self._diameter,
|
|
243
|
+
material=self._material,
|
|
244
|
+
density=self._density,
|
|
245
|
+
name=self._name,
|
|
246
|
+
group_label=self._group_label,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def from_geometry(
|
|
251
|
+
geo: PointGeometry,
|
|
252
|
+
new_material: t.Optional[t.Union[Material, ConstitutiveLaw]] = None,
|
|
253
|
+
) -> PointGeometry:
|
|
254
|
+
"""Create a new PointGeometry with a different material.
|
|
255
|
+
|
|
256
|
+
Arguments:
|
|
257
|
+
geo (PointGeometry): The geometry.
|
|
258
|
+
new_material (Optional(Union(Material, ConstitutiveLaw))): A new
|
|
259
|
+
material to be applied to the geometry. If new_material is
|
|
260
|
+
None an Elastic material with same stiffness as the original
|
|
261
|
+
material is created.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
PointGeometry: The new PointGeometry.
|
|
265
|
+
|
|
266
|
+
Note:
|
|
267
|
+
The polygon is not copied, but just referenced in the returned
|
|
268
|
+
PointGeometry object.
|
|
269
|
+
"""
|
|
270
|
+
if not isinstance(geo, PointGeometry):
|
|
271
|
+
raise TypeError('geo should be a PointGeometry')
|
|
272
|
+
if new_material is not None:
|
|
273
|
+
# provided a new_material
|
|
274
|
+
if not isinstance(new_material, Material) and not isinstance(
|
|
275
|
+
new_material, ConstitutiveLaw
|
|
276
|
+
):
|
|
277
|
+
raise TypeError(
|
|
278
|
+
f'new_material should be a valid structuralcodes.base.\
|
|
279
|
+
Material or structuralcodes.base.ConstitutiveLaw object. \
|
|
280
|
+
{repr(new_material)}'
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
# new_material not provided, assume elastic material with same
|
|
284
|
+
# elastic modulus
|
|
285
|
+
new_material = Elastic(E=geo.material.get_tangent(eps=0)[0])
|
|
286
|
+
|
|
287
|
+
return PointGeometry(
|
|
288
|
+
point=geo._point,
|
|
289
|
+
diameter=geo._diameter,
|
|
290
|
+
material=new_material,
|
|
291
|
+
density=geo._density,
|
|
292
|
+
name=geo._name,
|
|
293
|
+
group_label=geo._group_label,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def create_line_point_angle(
|
|
298
|
+
point: t.Union[Point, t.Tuple[float, float]],
|
|
299
|
+
theta: float,
|
|
300
|
+
bbox: t.Tuple[float, float, float, float],
|
|
301
|
+
) -> LineString:
|
|
302
|
+
"""Creates a line from point and angle within the bounding box.
|
|
303
|
+
|
|
304
|
+
Arguments:
|
|
305
|
+
point (Union(Point, Tuple(float, float))): A Point or a coordinate the
|
|
306
|
+
line should pass through.
|
|
307
|
+
theta (float): The angle of the line in radians.
|
|
308
|
+
bbox (Tuple(float, float, float, float)): Bounds for the created line.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
LineString: The created line.
|
|
312
|
+
"""
|
|
313
|
+
# create a unit vector defining the line
|
|
314
|
+
v = (np.cos(theta), np.sin(theta))
|
|
315
|
+
|
|
316
|
+
# check if the line is vertical to avoid div by zero
|
|
317
|
+
if abs(v[0]) > 1e-8:
|
|
318
|
+
# it is a non vertical line
|
|
319
|
+
tg = v[1] / v[0]
|
|
320
|
+
x1 = bbox[0] - 1e-3
|
|
321
|
+
x2 = bbox[2] + 1e-3
|
|
322
|
+
y1 = point[1] + (x1 - point[0]) * tg
|
|
323
|
+
y2 = point[1] + (x2 - point[0]) * tg
|
|
324
|
+
else:
|
|
325
|
+
# it is a near-vertical line
|
|
326
|
+
# tg is almost zero
|
|
327
|
+
ctg = v[0] / v[1]
|
|
328
|
+
y1 = bbox[1] - 1e-3
|
|
329
|
+
y2 = bbox[2] + 1e-3
|
|
330
|
+
x1 = point[0] + (y1 - point[1]) * ctg
|
|
331
|
+
x2 = point[0] + (y2 - point[1]) * ctg
|
|
332
|
+
# create the line
|
|
333
|
+
return LineString([(x1, y1), (x2, y2)])
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class SurfaceGeometry:
|
|
337
|
+
"""Class for a surface geometry with material.
|
|
338
|
+
|
|
339
|
+
Basically it is a wrapper for shapely polygon including the material (and
|
|
340
|
+
other parameters needed). As a shapely polygon it can contain one or more
|
|
341
|
+
holes.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
def __init__(
|
|
345
|
+
self,
|
|
346
|
+
poly: Polygon,
|
|
347
|
+
mat: t.Union[Material, ConstitutiveLaw],
|
|
348
|
+
density: t.Optional[float] = None,
|
|
349
|
+
concrete: bool = False,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Initializes a SurfaceGeometry object.
|
|
352
|
+
|
|
353
|
+
Arguments:
|
|
354
|
+
poly (shapely.Polygon): A Shapely polygon.
|
|
355
|
+
mat (Union(Material, ConstitutiveLaw)): A Material or
|
|
356
|
+
ConsitutiveLaw class applied to the geometry.
|
|
357
|
+
density (Optional(float)): When a ConstitutiveLaw is passed as mat,
|
|
358
|
+
the density can be provided by this argument. When mat is a
|
|
359
|
+
Material object the density is taken from the material.
|
|
360
|
+
concrete (bool): Flag to indicate if the geometry is concrete.
|
|
361
|
+
"""
|
|
362
|
+
# Check if inputs are of the correct type, otherwise return error
|
|
363
|
+
if not isinstance(poly, Polygon):
|
|
364
|
+
raise TypeError(
|
|
365
|
+
f'poly need to be a valid shapely.geometry.Polygon object. \
|
|
366
|
+
{repr(poly)}'
|
|
367
|
+
)
|
|
368
|
+
if not isinstance(mat, Material) and not isinstance(
|
|
369
|
+
mat, ConstitutiveLaw
|
|
370
|
+
):
|
|
371
|
+
raise TypeError(
|
|
372
|
+
f'mat should be a valid structuralcodes.base.Material \
|
|
373
|
+
or structuralcodes.base.ConstitutiveLaw object. \
|
|
374
|
+
{repr(mat)}'
|
|
375
|
+
)
|
|
376
|
+
self.polygon = poly
|
|
377
|
+
# Pass a constitutive law to the SurfaceGeometry
|
|
378
|
+
self._density = density
|
|
379
|
+
if isinstance(mat, Material):
|
|
380
|
+
self._density = mat.density
|
|
381
|
+
if isinstance(mat, Concrete):
|
|
382
|
+
concrete = True
|
|
383
|
+
mat = mat.constitutive_law
|
|
384
|
+
|
|
385
|
+
self.material = mat
|
|
386
|
+
self.concrete = concrete
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def area(self) -> float:
|
|
390
|
+
"""Returns the area of the geometry.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
float: The area of the geometry.
|
|
394
|
+
"""
|
|
395
|
+
return self.polygon.area
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def centroid(self) -> t.Tuple[float, float]:
|
|
399
|
+
"""Returns the centroid of the geometry.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Tuple(float, float): x and y coordinates of the centroid.
|
|
403
|
+
"""
|
|
404
|
+
return self.polygon.centroid.coords[0]
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def density(self) -> float:
|
|
408
|
+
"""Returns the density."""
|
|
409
|
+
return self._density
|
|
410
|
+
|
|
411
|
+
def calculate_extents(self) -> t.Tuple[float, float, float, float]:
|
|
412
|
+
"""Calculate extents of SurfaceGeometry.
|
|
413
|
+
|
|
414
|
+
Calculates the minimum and maximum x and y values.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Tuple(float, float, float, float): Minimum and maximum x and y
|
|
418
|
+
values (``x_min``, ``x_max``, ``y_min``, ``y_max``).
|
|
419
|
+
"""
|
|
420
|
+
min_x, min_y, max_x, max_y = self.polygon.bounds
|
|
421
|
+
return min_x, max_x, min_y, max_y
|
|
422
|
+
|
|
423
|
+
def split(
|
|
424
|
+
self, line: t.Union[LineString, t.Tuple[t.Tuple[float, float], float]]
|
|
425
|
+
) -> t.Tuple[t.List[SurfaceGeometry], t.List[SurfaceGeometry]]:
|
|
426
|
+
"""Splits the geometry using a line.
|
|
427
|
+
|
|
428
|
+
Arguments:
|
|
429
|
+
line (Union(LineString, Tuple(Tuple(float, float), float))): A line
|
|
430
|
+
either represented by a LineString shapely object, or a tuple
|
|
431
|
+
(point, theta) where point is a coordinate pair and theta is
|
|
432
|
+
the angle respect the horizontal axis in radians.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Tuple(List(SurfaceGeometry), List(SurfaceGeometry)): The
|
|
436
|
+
SurfaceGeometries above and below the line.
|
|
437
|
+
"""
|
|
438
|
+
if not isinstance(line, LineString):
|
|
439
|
+
point = line[0]
|
|
440
|
+
theta = line[1]
|
|
441
|
+
|
|
442
|
+
# get boundingbox of polygon
|
|
443
|
+
bbox = self.polygon.bounds
|
|
444
|
+
|
|
445
|
+
line = create_line_point_angle(point, theta, bbox)
|
|
446
|
+
|
|
447
|
+
# split the geometry about the line
|
|
448
|
+
above_polygons = []
|
|
449
|
+
below_polygons = []
|
|
450
|
+
if line.intersects(self.polygon):
|
|
451
|
+
result = split(self.polygon, line)
|
|
452
|
+
# divide polygons "above" and "below" line
|
|
453
|
+
for geom in result.geoms:
|
|
454
|
+
if LinearRing(
|
|
455
|
+
(line.coords[0], line.coords[1], geom.centroid.coords[0])
|
|
456
|
+
).is_ccw:
|
|
457
|
+
above_polygons.append(geom)
|
|
458
|
+
else:
|
|
459
|
+
below_polygons.append(geom)
|
|
460
|
+
else:
|
|
461
|
+
# not intersecting, all the polygon is above or below the line
|
|
462
|
+
geom = self.polygon
|
|
463
|
+
if LinearRing(
|
|
464
|
+
(line.coords[0], line.coords[1], geom.centroid.coords[0])
|
|
465
|
+
).is_ccw:
|
|
466
|
+
above_polygons.append(geom)
|
|
467
|
+
else:
|
|
468
|
+
below_polygons.append(geom)
|
|
469
|
+
|
|
470
|
+
return above_polygons, below_polygons
|
|
471
|
+
|
|
472
|
+
def split_two_lines(
|
|
473
|
+
self, lines: t.Union[t.Tuple[LineString, LineString], MultiLineString]
|
|
474
|
+
) -> t.Union[Polygon, MultiPolygon]:
|
|
475
|
+
"""Splits the geometry using two lines.
|
|
476
|
+
|
|
477
|
+
Arguments:
|
|
478
|
+
lines (Union(Tuple(LineString, Linestring)), MultiLineString): Two
|
|
479
|
+
lines either represented by a tuple of two LineString shapely
|
|
480
|
+
objects, or a MultiLineString shapely object.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Union(Polygon, Multipolygon): The polygon or multipolygon obtained
|
|
484
|
+
by splitting the SurfaceGeometry with the two lines.
|
|
485
|
+
"""
|
|
486
|
+
if isinstance(lines, MultiLineString):
|
|
487
|
+
multi_line = lines
|
|
488
|
+
elif isinstance(lines, tuple):
|
|
489
|
+
if len(lines) != 2:
|
|
490
|
+
raise RuntimeError('Two lines must be input')
|
|
491
|
+
multi_line = MultiLineString(lines)
|
|
492
|
+
lines_polygon = multi_line.convex_hull
|
|
493
|
+
|
|
494
|
+
# get the intersection
|
|
495
|
+
return self.polygon.intersection(lines_polygon)
|
|
496
|
+
|
|
497
|
+
def __add__(self, other: Geometry) -> CompoundGeometry:
|
|
498
|
+
"""Add operator "+" for geometries.
|
|
499
|
+
|
|
500
|
+
Arguments:
|
|
501
|
+
other (Geometry): The other geometry to add.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
CompoundGeometry: A new CompoundGeometry.
|
|
505
|
+
"""
|
|
506
|
+
return CompoundGeometry([self, other])
|
|
507
|
+
|
|
508
|
+
def __sub__(self, other: Geometry) -> SurfaceGeometry:
|
|
509
|
+
"""Add operator "-" for geometries.
|
|
510
|
+
|
|
511
|
+
Arguments:
|
|
512
|
+
other (Geometry): The other geometry to be subtracted.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
SurfaceGeometry: The resulting SurfaceGeometry.
|
|
516
|
+
"""
|
|
517
|
+
material = self.material
|
|
518
|
+
density = self._density
|
|
519
|
+
|
|
520
|
+
# if we subtract a point from a surface we obtain the same surface
|
|
521
|
+
sub_polygon = self.polygon
|
|
522
|
+
if isinstance(other, SurfaceGeometry):
|
|
523
|
+
# We are subtracting a surface from another surface
|
|
524
|
+
sub_polygon = self.polygon - other.polygon
|
|
525
|
+
if isinstance(other, CompoundGeometry):
|
|
526
|
+
# We are subtracting a compound from a surface
|
|
527
|
+
sub_polygon = self.polygon
|
|
528
|
+
for g in other.geometries:
|
|
529
|
+
sub_polygon = sub_polygon - g.polygon
|
|
530
|
+
|
|
531
|
+
return SurfaceGeometry(poly=sub_polygon, mat=material, density=density)
|
|
532
|
+
|
|
533
|
+
def _repr_svg_(self) -> str:
|
|
534
|
+
"""Returns the svg representation."""
|
|
535
|
+
return str(self.polygon._repr_svg_())
|
|
536
|
+
|
|
537
|
+
def translate(self, dx: float = 0.0, dy: float = 0.0) -> SurfaceGeometry:
|
|
538
|
+
"""Returns a new SurfaceGeometry that is translated by dx, dy.
|
|
539
|
+
|
|
540
|
+
Arguments:
|
|
541
|
+
dx (float): Translation in x direction.
|
|
542
|
+
dy (float): Translation in y direction.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
SurfaceGeometry: The translated SurfaceGeometry.
|
|
546
|
+
"""
|
|
547
|
+
return SurfaceGeometry(
|
|
548
|
+
poly=affinity.translate(self.polygon, dx, dy),
|
|
549
|
+
mat=self.material,
|
|
550
|
+
density=self._density,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
def rotate(
|
|
554
|
+
self,
|
|
555
|
+
angle: float = 0.0,
|
|
556
|
+
point: t.Tuple[float, float] = (0.0, 0.0),
|
|
557
|
+
use_radians: bool = True,
|
|
558
|
+
) -> SurfaceGeometry:
|
|
559
|
+
"""Returns a new SurfaceGeometry that is rotated by angle.
|
|
560
|
+
|
|
561
|
+
Arguments:
|
|
562
|
+
angle (float): Rotation angle in radians (if use_radians = True),
|
|
563
|
+
or degress (if use_radians = False).
|
|
564
|
+
point (Union(Point, Tuple(float, float))): The origin of the
|
|
565
|
+
rotation.
|
|
566
|
+
use_radians (bool): True if angle is in radians, and False if angle
|
|
567
|
+
is in degrees.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
SurfaceGeometry: The rotated SurfaceGeometry.
|
|
571
|
+
"""
|
|
572
|
+
return SurfaceGeometry(
|
|
573
|
+
poly=affinity.rotate(
|
|
574
|
+
self.polygon, angle, origin=point, use_radians=use_radians
|
|
575
|
+
),
|
|
576
|
+
mat=self.material,
|
|
577
|
+
density=self._density,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
@staticmethod
|
|
581
|
+
def from_geometry(
|
|
582
|
+
geo: SurfaceGeometry,
|
|
583
|
+
new_material: t.Optional[t.Union[Material, ConstitutiveLaw]] = None,
|
|
584
|
+
) -> SurfaceGeometry:
|
|
585
|
+
"""Create a new SurfaceGeometry with a different material.
|
|
586
|
+
|
|
587
|
+
Arguments:
|
|
588
|
+
geo (SurfaceGeometry): The geometry.
|
|
589
|
+
new_material: (Optional(Union(Material, ConstitutiveLaw))): A new
|
|
590
|
+
material to be applied to the geometry. If new_material is None
|
|
591
|
+
an Elastic material with same stiffness of the original
|
|
592
|
+
material is created.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
SurfaceGeometry: The new SurfaceGeometry.
|
|
596
|
+
|
|
597
|
+
Note:
|
|
598
|
+
The polygon is not copied, but just referenced in the returned
|
|
599
|
+
SurfaceGeometry object.
|
|
600
|
+
"""
|
|
601
|
+
if not isinstance(geo, SurfaceGeometry):
|
|
602
|
+
raise TypeError('geo should be a SurfaceGeometry')
|
|
603
|
+
if new_material is not None:
|
|
604
|
+
# provided a new_material
|
|
605
|
+
if not isinstance(new_material, Material) and not isinstance(
|
|
606
|
+
new_material, ConstitutiveLaw
|
|
607
|
+
):
|
|
608
|
+
raise TypeError(
|
|
609
|
+
f'new_material should be a valid structuralcodes.base.\
|
|
610
|
+
Material or structuralcodes.base.ConstitutiveLaw object. \
|
|
611
|
+
{repr(new_material)}'
|
|
612
|
+
)
|
|
613
|
+
else:
|
|
614
|
+
# new_material not provided, assume elastic material with same
|
|
615
|
+
# elastic modulus
|
|
616
|
+
new_material = Elastic(E=geo.material.get_tangent(eps=0)[0])
|
|
617
|
+
|
|
618
|
+
return SurfaceGeometry(
|
|
619
|
+
poly=geo.polygon, mat=new_material, density=geo._density
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# here we can also add static methods like:
|
|
623
|
+
# from_points
|
|
624
|
+
# from_points_and_facets
|
|
625
|
+
# from_surface_geometry
|
|
626
|
+
# from_dxf
|
|
627
|
+
# from_ascii
|
|
628
|
+
# ...
|
|
629
|
+
# we could also add methods wrapping shapely function, like:
|
|
630
|
+
# mirror, translation, rotation, etc.
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _process_geometries_multipolygon(
|
|
634
|
+
geometries: MultiPolygon,
|
|
635
|
+
materials: t.Optional[
|
|
636
|
+
t.Union[t.List[Material], Material, ConstitutiveLaw]
|
|
637
|
+
],
|
|
638
|
+
) -> list:
|
|
639
|
+
"""Process geometries for initialization."""
|
|
640
|
+
checked_geometries = []
|
|
641
|
+
# a MultiPolygon is provided
|
|
642
|
+
if isinstance(materials, (ConstitutiveLaw, Material)):
|
|
643
|
+
for g in geometries.geoms:
|
|
644
|
+
checked_geometries.append(SurfaceGeometry(poly=g, mat=materials))
|
|
645
|
+
elif isinstance(materials, list):
|
|
646
|
+
# the list of materials is provided, one for each polygon
|
|
647
|
+
if len(geometries.geoms) != len(materials):
|
|
648
|
+
raise ValueError(
|
|
649
|
+
'geometries and materials should have the same length'
|
|
650
|
+
)
|
|
651
|
+
for g, m in zip(geometries.geoms, materials):
|
|
652
|
+
checked_geometries.append(SurfaceGeometry(poly=g, mat=m))
|
|
653
|
+
return checked_geometries
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _process_geometries_list(
|
|
657
|
+
geometries: t.List[Geometry],
|
|
658
|
+
) -> t.Tuple[list, list]:
|
|
659
|
+
"""Process geometries for initialization."""
|
|
660
|
+
# a list of SurfaceGeometry is provided
|
|
661
|
+
checked_geometries = []
|
|
662
|
+
checked_point_geometries = []
|
|
663
|
+
for geo in geometries:
|
|
664
|
+
if isinstance(geo, SurfaceGeometry):
|
|
665
|
+
checked_geometries.append(geo)
|
|
666
|
+
elif isinstance(geo, CompoundGeometry):
|
|
667
|
+
for g in geo.geometries:
|
|
668
|
+
checked_geometries.append(g)
|
|
669
|
+
for pg in geo.point_geometries:
|
|
670
|
+
checked_point_geometries.append(pg)
|
|
671
|
+
elif isinstance(geo, PointGeometry):
|
|
672
|
+
checked_point_geometries.append(geo)
|
|
673
|
+
return (checked_geometries, checked_point_geometries)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class CompoundGeometry(Geometry):
|
|
677
|
+
"""Class for a compound geometry.
|
|
678
|
+
|
|
679
|
+
It is basicaly a set of geometries, each one with its own materials and
|
|
680
|
+
properties.
|
|
681
|
+
"""
|
|
682
|
+
|
|
683
|
+
def __init__(
|
|
684
|
+
self,
|
|
685
|
+
geometries: t.Union[t.List[Geometry], MultiPolygon],
|
|
686
|
+
materials: t.Optional[t.Union[t.List[Material], Material]] = None,
|
|
687
|
+
) -> None:
|
|
688
|
+
"""Creates a compound geometry.
|
|
689
|
+
|
|
690
|
+
Arguments:
|
|
691
|
+
geometries (Union(List(Geometry), MultiPolygon)): A list of
|
|
692
|
+
SurfaceGeometry objects or a shapely MultiPolygon object
|
|
693
|
+
(in this case also a list of materials should be given).
|
|
694
|
+
materials (Optional(List(Material), Material)): A material (applied
|
|
695
|
+
to all polygons) or a list of materials. In this case the
|
|
696
|
+
number of polygons should match the number of materials.
|
|
697
|
+
"""
|
|
698
|
+
if isinstance(geometries, MultiPolygon):
|
|
699
|
+
# a MultiPolygon is provided
|
|
700
|
+
self.geometries = _process_geometries_multipolygon(
|
|
701
|
+
geometries, materials
|
|
702
|
+
)
|
|
703
|
+
self.point_geometries = []
|
|
704
|
+
# useful for representation in svg
|
|
705
|
+
geoms_representation = [g.polygon for g in self.geometries]
|
|
706
|
+
self.geom = MultiPolygon(geoms_representation)
|
|
707
|
+
return
|
|
708
|
+
if isinstance(geometries, list):
|
|
709
|
+
self.geometries, self.point_geometries = _process_geometries_list(
|
|
710
|
+
geometries
|
|
711
|
+
)
|
|
712
|
+
# useful for representation in svg
|
|
713
|
+
geoms_representation = [g.polygon for g in self.geometries]
|
|
714
|
+
geoms_representation += [
|
|
715
|
+
pg._point.buffer(pg._diameter / 2)
|
|
716
|
+
for pg in self.point_geometries
|
|
717
|
+
]
|
|
718
|
+
self.geom = MultiPolygon(geoms_representation)
|
|
719
|
+
self._reinforced_concrete = None
|
|
720
|
+
|
|
721
|
+
# we can add here static methods like
|
|
722
|
+
# from_dxf
|
|
723
|
+
# from_ascii
|
|
724
|
+
# ...
|
|
725
|
+
|
|
726
|
+
def _repr_svg_(self) -> str:
|
|
727
|
+
"""Returns the svg representation."""
|
|
728
|
+
return str(self.geom._repr_svg_())
|
|
729
|
+
|
|
730
|
+
@property
|
|
731
|
+
def reinforced_concrete(self) -> bool:
|
|
732
|
+
"""Returns True if it is a Reinforced Concrete section."""
|
|
733
|
+
if self._reinforced_concrete is None:
|
|
734
|
+
self._reinforced_concrete = False
|
|
735
|
+
for geo in self.geometries:
|
|
736
|
+
if geo.concrete:
|
|
737
|
+
self._reinforced_concrete = True
|
|
738
|
+
break
|
|
739
|
+
return self._reinforced_concrete
|
|
740
|
+
|
|
741
|
+
@property
|
|
742
|
+
def area(self) -> float:
|
|
743
|
+
"""Return the area of the compound geometry."""
|
|
744
|
+
area = 0
|
|
745
|
+
for geo in self.geometries:
|
|
746
|
+
area += geo.area
|
|
747
|
+
return area
|
|
748
|
+
|
|
749
|
+
def calculate_extents(self) -> t.Tuple[float, float, float, float]:
|
|
750
|
+
"""Calculate extents of CompundGeometry.
|
|
751
|
+
|
|
752
|
+
Calculates the minimum and maximum x and y-values.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Tuple(float, float, float, float): Minimum and maximum x and y
|
|
756
|
+
values (``x_min``, ``x_max``, ``y_min``, ``y_max``).
|
|
757
|
+
|
|
758
|
+
Note:
|
|
759
|
+
Considers only SurfaceGeometries and not PointGeometries!
|
|
760
|
+
"""
|
|
761
|
+
min_x = 1e16
|
|
762
|
+
max_x = -1e16
|
|
763
|
+
min_y = 1e16
|
|
764
|
+
max_y = -1e16
|
|
765
|
+
for geo in self.geometries:
|
|
766
|
+
xmin, xmax, ymin, ymax = geo.calculate_extents()
|
|
767
|
+
min_x = min(min_x, xmin)
|
|
768
|
+
min_y = min(min_y, ymin)
|
|
769
|
+
max_x = max(max_x, xmax)
|
|
770
|
+
max_y = max(max_y, ymax)
|
|
771
|
+
return min_x, max_x, min_y, max_y
|
|
772
|
+
|
|
773
|
+
def translate(self, dx: float = 0.0, dy: float = 0.0) -> CompoundGeometry:
|
|
774
|
+
"""Returns a new CompountGeometry that is translated by dx, dy.
|
|
775
|
+
|
|
776
|
+
Arguments:
|
|
777
|
+
dx (float): Translation in x direction.
|
|
778
|
+
dy (float): Translation in y direction.
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
CompoundGeometry: The translated CompoundGeometry.
|
|
782
|
+
"""
|
|
783
|
+
processed_geoms = []
|
|
784
|
+
for g in self.geometries:
|
|
785
|
+
processed_geoms.append(g.translate(dx, dy))
|
|
786
|
+
for pg in self.point_geometries:
|
|
787
|
+
processed_geoms.append(pg.translate(dx, dy))
|
|
788
|
+
return CompoundGeometry(geometries=processed_geoms)
|
|
789
|
+
|
|
790
|
+
def rotate(
|
|
791
|
+
self,
|
|
792
|
+
angle: float = 0.0,
|
|
793
|
+
point: t.Tuple[float, float] = (0.0, 0.0),
|
|
794
|
+
use_radians: bool = True,
|
|
795
|
+
) -> CompoundGeometry:
|
|
796
|
+
"""Returns a new CompoundGeometry that is rotated by angle.
|
|
797
|
+
|
|
798
|
+
Arguments:
|
|
799
|
+
angle (float): Rotation angle in radians (if use_radians = True),
|
|
800
|
+
or degress (if use_radians = False).
|
|
801
|
+
point (Union(Point, Tuple(float, float))): The origin of the
|
|
802
|
+
rotation.
|
|
803
|
+
use_radians (bool): True if angle is in radians, and False if angle
|
|
804
|
+
is in degrees.
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
CompoundGeometry: The rotated CompoundGeometry.
|
|
808
|
+
"""
|
|
809
|
+
processed_geoms = []
|
|
810
|
+
for g in self.geometries:
|
|
811
|
+
processed_geoms.append(g.rotate(angle, point, use_radians))
|
|
812
|
+
for pg in self.point_geometries:
|
|
813
|
+
processed_geoms.append(pg.rotate(angle, point, use_radians))
|
|
814
|
+
return CompoundGeometry(geometries=processed_geoms)
|
|
815
|
+
|
|
816
|
+
def __add__(self, other: Geometry) -> CompoundGeometry:
|
|
817
|
+
"""Add operator "+" for geometries.
|
|
818
|
+
|
|
819
|
+
Arguments:
|
|
820
|
+
other (Geometry): The other geometry to add.
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
CompoundGeometry: A new CompoundGeometry.
|
|
824
|
+
"""
|
|
825
|
+
return CompoundGeometry([self, other])
|
|
826
|
+
|
|
827
|
+
def __sub__(self, other: Geometry) -> CompoundGeometry:
|
|
828
|
+
"""Add operator "-" for geometries.
|
|
829
|
+
|
|
830
|
+
Arguments:
|
|
831
|
+
other (Geometry): The other geometry to be subtracted.
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
CompoundGeometry: A new CompoundGeometry.
|
|
835
|
+
"""
|
|
836
|
+
# if we subtract a point from a surface we obtain the same surface
|
|
837
|
+
if isinstance(other, PointGeometry):
|
|
838
|
+
return self
|
|
839
|
+
# Otherwise perform subtraction
|
|
840
|
+
processed_geoms = []
|
|
841
|
+
for g in self.geometries:
|
|
842
|
+
processed_geoms.append(g - other)
|
|
843
|
+
for pg in self.point_geometries:
|
|
844
|
+
processed_geoms.append(pg)
|
|
845
|
+
return CompoundGeometry(geometries=processed_geoms)
|
|
846
|
+
|
|
847
|
+
@staticmethod
|
|
848
|
+
def from_geometry(
|
|
849
|
+
geo: CompoundGeometry,
|
|
850
|
+
new_material: t.Optional[t.Union[Material, ConstitutiveLaw]] = None,
|
|
851
|
+
) -> CompoundGeometry:
|
|
852
|
+
"""Create a new CompoundGeometry with a different material.
|
|
853
|
+
|
|
854
|
+
Arguments:
|
|
855
|
+
geo (CompoundGeometry): The geometry.
|
|
856
|
+
new_material (Optional(Union(Material, ConstitutiveLaw))): A new
|
|
857
|
+
material to be applied to the geometry. If new_material is None
|
|
858
|
+
an Elastic material with same stiffness of the original
|
|
859
|
+
material is created.
|
|
860
|
+
|
|
861
|
+
Returns:
|
|
862
|
+
CompoundGeometry: The new CompoundGeometry.
|
|
863
|
+
"""
|
|
864
|
+
if not isinstance(geo, CompoundGeometry):
|
|
865
|
+
raise TypeError('geo should be a CompoundGeometry')
|
|
866
|
+
processed_geoms = []
|
|
867
|
+
for g in geo.geometries:
|
|
868
|
+
processed_geoms.append(
|
|
869
|
+
SurfaceGeometry.from_geometry(geo=g, new_material=new_material)
|
|
870
|
+
)
|
|
871
|
+
for pg in geo.point_geometries:
|
|
872
|
+
processed_geoms.append(
|
|
873
|
+
PointGeometry.from_geometry(geo=pg, new_material=new_material)
|
|
874
|
+
)
|
|
875
|
+
return CompoundGeometry(geometries=processed_geoms)
|