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.

Files changed (50) hide show
  1. structuralcodes/__init__.py +17 -0
  2. structuralcodes/codes/__init__.py +79 -0
  3. structuralcodes/codes/ec2_2004/__init__.py +133 -0
  4. structuralcodes/codes/ec2_2004/_concrete_material_properties.py +239 -0
  5. structuralcodes/codes/ec2_2004/_reinforcement_material_properties.py +104 -0
  6. structuralcodes/codes/ec2_2004/_section_7_3_crack_control.py +941 -0
  7. structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py +257 -0
  8. structuralcodes/codes/ec2_2004/shear.py +506 -0
  9. structuralcodes/codes/ec2_2023/__init__.py +104 -0
  10. structuralcodes/codes/ec2_2023/_annexB_time_dependent.py +17 -0
  11. structuralcodes/codes/ec2_2023/_section5_materials.py +1160 -0
  12. structuralcodes/codes/ec2_2023/_section9_sls.py +325 -0
  13. structuralcodes/codes/mc2010/__init__.py +169 -0
  14. structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +704 -0
  15. structuralcodes/codes/mc2010/_concrete_interface_different_casting_times.py +104 -0
  16. structuralcodes/codes/mc2010/_concrete_material_properties.py +463 -0
  17. structuralcodes/codes/mc2010/_concrete_punching.py +543 -0
  18. structuralcodes/codes/mc2010/_concrete_shear.py +749 -0
  19. structuralcodes/codes/mc2010/_concrete_torsion.py +164 -0
  20. structuralcodes/codes/mc2010/_reinforcement_material_properties.py +105 -0
  21. structuralcodes/core/__init__.py +1 -0
  22. structuralcodes/core/_section_results.py +211 -0
  23. structuralcodes/core/base.py +260 -0
  24. structuralcodes/geometry/__init__.py +25 -0
  25. structuralcodes/geometry/_geometry.py +875 -0
  26. structuralcodes/geometry/_steel_sections.py +2155 -0
  27. structuralcodes/materials/__init__.py +9 -0
  28. structuralcodes/materials/concrete/__init__.py +82 -0
  29. structuralcodes/materials/concrete/_concrete.py +114 -0
  30. structuralcodes/materials/concrete/_concreteEC2_2004.py +477 -0
  31. structuralcodes/materials/concrete/_concreteEC2_2023.py +435 -0
  32. structuralcodes/materials/concrete/_concreteMC2010.py +494 -0
  33. structuralcodes/materials/constitutive_laws.py +979 -0
  34. structuralcodes/materials/reinforcement/__init__.py +84 -0
  35. structuralcodes/materials/reinforcement/_reinforcement.py +172 -0
  36. structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +103 -0
  37. structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +93 -0
  38. structuralcodes/materials/reinforcement/_reinforcementMC2010.py +98 -0
  39. structuralcodes/sections/__init__.py +23 -0
  40. structuralcodes/sections/_generic.py +1249 -0
  41. structuralcodes/sections/_reinforcement.py +115 -0
  42. structuralcodes/sections/section_integrators/__init__.py +14 -0
  43. structuralcodes/sections/section_integrators/_factory.py +41 -0
  44. structuralcodes/sections/section_integrators/_fiber_integrator.py +238 -0
  45. structuralcodes/sections/section_integrators/_marin_integration.py +47 -0
  46. structuralcodes/sections/section_integrators/_marin_integrator.py +222 -0
  47. structuralcodes/sections/section_integrators/_section_integrator.py +49 -0
  48. structuralcodes-0.0.1.dist-info/METADATA +40 -0
  49. structuralcodes-0.0.1.dist-info/RECORD +50 -0
  50. 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)