structuralcodes 0.5.0__tar.gz → 0.6.0__tar.gz

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 (79) hide show
  1. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/PKG-INFO +2 -2
  2. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/pyproject.toml +1 -1
  3. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/__init__.py +1 -1
  4. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +2 -2
  5. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/core/base.py +115 -4
  6. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/geometry/__init__.py +2 -8
  7. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/geometry/_geometry.py +11 -22
  8. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/geometry/_reinforcement.py +1 -3
  9. structuralcodes-0.6.0/structuralcodes/geometry/profiles/__init__.py +19 -0
  10. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_base_profile.py +305 -0
  11. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_common_functions.py +194 -0
  12. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_he.py +192 -0
  13. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_ipe.py +130 -0
  14. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_ipn.py +329 -0
  15. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_ub.py +264 -0
  16. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_ubp.py +227 -0
  17. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_uc.py +276 -0
  18. structuralcodes-0.6.0/structuralcodes/geometry/profiles/_upn.py +315 -0
  19. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/basic/_elastic.py +18 -1
  20. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/basic/_elasticplastic.py +18 -1
  21. structuralcodes-0.6.0/structuralcodes/materials/basic/_generic.py +43 -0
  22. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/concrete/__init__.py +3 -0
  23. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/concrete/_concrete.py +10 -1
  24. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/concrete/_concreteEC2_2004.py +14 -0
  25. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/concrete/_concreteEC2_2023.py +14 -0
  26. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/concrete/_concreteMC2010.py +19 -0
  27. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/constitutive_laws/__init__.py +3 -0
  28. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/constitutive_laws/_elasticplastic.py +2 -2
  29. structuralcodes-0.6.0/structuralcodes/materials/constitutive_laws/_initial_strain.py +130 -0
  30. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/reinforcement/__init__.py +6 -0
  31. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/reinforcement/_reinforcement.py +10 -1
  32. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +14 -0
  33. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +14 -0
  34. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/reinforcement/_reinforcementMC2010.py +14 -0
  35. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/sections/section_integrators/__init__.py +3 -1
  36. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/sections/section_integrators/_marin_integrator.py +1 -1
  37. structuralcodes-0.5.0/structuralcodes/geometry/_steel_sections.py +0 -2155
  38. structuralcodes-0.5.0/structuralcodes/materials/basic/_generic.py +0 -26
  39. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/LICENSE +0 -0
  40. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/README.md +0 -0
  41. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/__init__.py +0 -0
  42. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2004/__init__.py +0 -0
  43. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2004/_concrete_creep_and_shrinkage.py +0 -0
  44. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2004/_concrete_material_properties.py +0 -0
  45. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2004/_reinforcement_material_properties.py +0 -0
  46. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2004/_section_7_3_crack_control.py +0 -0
  47. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2004/shear.py +0 -0
  48. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2023/__init__.py +0 -0
  49. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2023/_annexB_time_dependent.py +0 -0
  50. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2023/_section5_materials.py +0 -0
  51. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/ec2_2023/_section9_sls.py +0 -0
  52. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/__init__.py +0 -0
  53. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/_concrete_interface_different_casting_times.py +0 -0
  54. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/_concrete_material_properties.py +0 -0
  55. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/_concrete_punching.py +0 -0
  56. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/_concrete_shear.py +0 -0
  57. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/_concrete_torsion.py +0 -0
  58. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/_interface_concrete_steel_rebar.py +0 -0
  59. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2010/_reinforcement_material_properties.py +0 -0
  60. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/codes/mc2020/__init__.py +0 -0
  61. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/core/__init__.py +0 -0
  62. {structuralcodes-0.5.0/structuralcodes/sections/section_integrators → structuralcodes-0.6.0/structuralcodes/core}/_marin_integration.py +0 -0
  63. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/core/_section_results.py +0 -0
  64. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/geometry/_circular.py +0 -0
  65. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/geometry/_rectangular.py +0 -0
  66. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/__init__.py +0 -0
  67. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/basic/__init__.py +0 -0
  68. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/constitutive_laws/_bilinearcompression.py +0 -0
  69. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/constitutive_laws/_elastic.py +0 -0
  70. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/constitutive_laws/_parabolarectangle.py +0 -0
  71. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/constitutive_laws/_popovics.py +0 -0
  72. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/constitutive_laws/_sargin.py +0 -0
  73. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/materials/constitutive_laws/_userdefined.py +0 -0
  74. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/sections/__init__.py +0 -0
  75. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/sections/_generic.py +0 -0
  76. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/sections/_rc_utils.py +0 -0
  77. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/sections/section_integrators/_factory.py +0 -0
  78. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/sections/section_integrators/_fiber_integrator.py +0 -0
  79. {structuralcodes-0.5.0 → structuralcodes-0.6.0}/structuralcodes/sections/section_integrators/_section_integrator.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structuralcodes
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: A Python package that contains models from structural design codes.
5
5
  Author-email: fib - International Federation for Structural Concrete <info@fib-international.org>
6
- Requires-Python: >=3.8
6
+ Requires-Python: >=3.9
7
7
  Description-Content-Type: text/markdown
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Operating System :: OS Independent
@@ -10,7 +10,7 @@ authors = [
10
10
  {name = "fib - International Federation for Structural Concrete", email = "info@fib-international.org"}
11
11
  ]
12
12
  readme = "README.md"
13
- requires-python = ">=3.8"
13
+ requires-python = ">=3.9"
14
14
  classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
16
  "Operating System :: OS Independent"
@@ -3,7 +3,7 @@
3
3
  from . import codes, core, geometry, materials, sections
4
4
  from .codes import get_design_codes, set_design_code, set_national_annex
5
5
 
6
- __version__ = '0.5.0'
6
+ __version__ = '0.6.0'
7
7
 
8
8
  __all__ = [
9
9
  'set_design_code',
@@ -95,13 +95,13 @@ def _check_initial_stress(sigma: float, fcm: float) -> None:
95
95
  raise ValueError(
96
96
  'The stress level exceeds the range of application.'
97
97
  'Maximum allowable stress is 0.6*fcm. Current stress level '
98
- f'is {round(abs(sigma)/fcm, 3)}*fcm.'
98
+ f'is {round(abs(sigma) / fcm, 3)}*fcm.'
99
99
  )
100
100
  if abs(sigma) > 0.4 * fcm:
101
101
  warnings.warn(
102
102
  'Initial stress is too high to consider the '
103
103
  'concrete as an aging linear visco-elastic material: '
104
- f'sigma = {round(abs(sigma)/fcm,3)}*fcm > 0.4*fcm. Nonlinear'
104
+ f'sigma = {round(abs(sigma) / fcm, 3)}*fcm > 0.4*fcm. Nonlinear'
105
105
  ' creep calculations are performed according to subclause '
106
106
  '5.1.9.4.3 (d) of the fib Model Code 2010 to account for '
107
107
  'large compressive stresses.'
@@ -15,17 +15,44 @@ class Material(abc.ABC):
15
15
  """Abstract base class for materials."""
16
16
 
17
17
  _constitutive_law = None
18
-
19
- def __init__(self, density: float, name: t.Optional[str] = None) -> None:
18
+ _initial_strain: t.Optional[float] = None
19
+ _initial_stress: t.Optional[float] = None
20
+ _strain_compatibility: t.Optional[bool] = None
21
+
22
+ def __init__(
23
+ self,
24
+ density: float,
25
+ initial_strain: t.Optional[float] = None,
26
+ initial_stress: t.Optional[float] = None,
27
+ strain_compatibility: t.Optional[bool] = None,
28
+ name: t.Optional[str] = None,
29
+ ) -> None:
20
30
  """Initializes an instance of a new material.
21
31
 
22
32
  Args:
23
- density (float): density of the material in kg/m3
33
+ density (float): Density of the material in kg/m3.
24
34
 
25
35
  Keyword Args:
36
+ initial_strain (Optional[float]): Initial strain of the material.
37
+ initial_stress (Optional[float]): Initial stress of the material.
38
+ strain_compatibility (Optional[bool]): Only relevant if
39
+ initial_strain or initial_stress are different from zero. If
40
+ True, the material deforms with the geometry. If False, the
41
+ stress in the material upon loading is kept constant
42
+ corresponding to the initial strain.
26
43
  name (Optional[str]): descriptive name of the material
44
+
45
+ Raise:
46
+ ValueError: if both initial_strain and initial_stress are provided
27
47
  """
28
48
  self._density = abs(density)
49
+ if initial_strain is not None and initial_stress is not None:
50
+ raise ValueError(
51
+ 'Both initial_strain and initial_stress cannot be provided.'
52
+ )
53
+ self._initial_strain = initial_strain
54
+ self._initial_stress = initial_stress
55
+ self._strain_compatibility = strain_compatibility
29
56
  self._name = name if name is not None else 'Material'
30
57
 
31
58
  @property
@@ -43,6 +70,88 @@ class Material(abc.ABC):
43
70
  """Returns the density of the material in kg/m3."""
44
71
  return self._density
45
72
 
73
+ @property
74
+ def initial_strain(self):
75
+ """Returns the initial strain of the material."""
76
+ return self._initial_strain
77
+
78
+ @property
79
+ def initial_stress(self):
80
+ """Returns the initial stress of the material."""
81
+ return self._initial_stress
82
+
83
+ @property
84
+ def strain_compatibility(self):
85
+ """Returns the strain compatibility of the material.
86
+
87
+ If true (default), the strain compatibility is enforced
88
+ haveing the same strain as in all other materials of the
89
+ section at the same point. If false, the strain compatibility
90
+ is not enforced and the initial strain is applied to the section
91
+ independently.
92
+ """
93
+ return self._strain_compatibility
94
+
95
+ def _apply_initial_strain(self):
96
+ """Wraps the current constitutive law to apply initial strain."""
97
+ strain_compatibility = (
98
+ self._strain_compatibility
99
+ if self._strain_compatibility is not None
100
+ else True
101
+ )
102
+ if self._initial_stress is not None:
103
+ # Specified a stress, compute the strain from it
104
+ self._initial_strain_from_stress()
105
+ if self._initial_strain is not None:
106
+ # Lazy import to avoid circular dependency
107
+ from structuralcodes.materials.constitutive_laws import ( # noqa: PLC0415
108
+ InitialStrain,
109
+ )
110
+
111
+ if self._initial_stress is None:
112
+ # Compute the stress from the strain
113
+ self._initial_stress = self._constitutive_law.get_stress(
114
+ self._initial_strain
115
+ )
116
+
117
+ self._constitutive_law = InitialStrain(
118
+ self._constitutive_law,
119
+ self._initial_strain,
120
+ strain_compatibility,
121
+ )
122
+
123
+ def _initial_strain_from_stress(self):
124
+ """Computes the initial strain from the initial stress.
125
+
126
+ This function is called internally so it assumes that the
127
+ initial stress is not None
128
+ """
129
+ # Iteratively compute the initial strain that gives the desired
130
+ # initial stress. Note that the wrapped law can be nonlinear
131
+ tol = 1e-12
132
+ max_iter = 100
133
+ target_stress = self._initial_stress
134
+ strain = 0.0
135
+ stress = self._constitutive_law.get_stress(strain)
136
+ d_stress = target_stress - stress
137
+ num_iter = 0
138
+ while abs(d_stress) > tol and num_iter < max_iter:
139
+ tangent = self._constitutive_law.get_tangent(strain)
140
+ if tangent == 0:
141
+ raise ValueError(
142
+ 'Tangent modulus = 0 during initial strain computation.'
143
+ )
144
+ d_strain = d_stress / tangent
145
+ strain += d_strain
146
+ stress = self._constitutive_law.get_stress(strain)
147
+ d_stress = target_stress - stress
148
+ num_iter += 1
149
+
150
+ if abs(d_stress) > tol:
151
+ raise RuntimeError('Failed to converge for given initial stress.')
152
+
153
+ self._initial_strain = strain
154
+
46
155
 
47
156
  class ConstitutiveLaw(abc.ABC):
48
157
  """Abstract base class for constitutive laws."""
@@ -150,7 +259,9 @@ class ConstitutiveLaw(abc.ABC):
150
259
 
151
260
  eps = np.concatenate((eps_neg, eps_pos))
152
261
  sig = self.get_stress(eps)
153
- from structuralcodes.materials.constitutive_laws import UserDefined
262
+ from structuralcodes.materials.constitutive_laws import ( # noqa: PLC0415
263
+ UserDefined,
264
+ )
154
265
 
155
266
  return UserDefined(eps, sig)
156
267
 
@@ -1,5 +1,6 @@
1
1
  """Main entry point for geometry."""
2
2
 
3
+ from . import profiles
3
4
  from ._circular import CircularGeometry
4
5
  from ._geometry import (
5
6
  CompoundGeometry,
@@ -14,7 +15,6 @@ from ._reinforcement import (
14
15
  add_reinforcement_circle,
15
16
  add_reinforcement_line,
16
17
  )
17
- from ._steel_sections import HE, IPE, IPN, UB, UBP, UC, UPN
18
18
 
19
19
  __all__ = [
20
20
  'Geometry',
@@ -22,13 +22,7 @@ __all__ = [
22
22
  'SurfaceGeometry',
23
23
  'CompoundGeometry',
24
24
  'create_line_point_angle',
25
- 'IPE',
26
- 'HE',
27
- 'UB',
28
- 'UC',
29
- 'UBP',
30
- 'IPN',
31
- 'UPN',
25
+ 'profiles',
32
26
  'add_reinforcement',
33
27
  'add_reinforcement_line',
34
28
  'CircularGeometry',
@@ -79,6 +79,17 @@ class Geometry:
79
79
  'This method should be implemented by subclasses'
80
80
  )
81
81
 
82
+ def __add__(self, other: Geometry) -> CompoundGeometry:
83
+ """Add operator "+" for geometries.
84
+
85
+ Arguments:
86
+ other (Geometry): The other geometry to add.
87
+
88
+ Returns:
89
+ CompoundGeometry: A new CompoundGeometry.
90
+ """
91
+ return CompoundGeometry([self, other])
92
+
82
93
 
83
94
  class PointGeometry(Geometry):
84
95
  """Class for a point geometry with material.
@@ -470,17 +481,6 @@ class SurfaceGeometry(Geometry):
470
481
  # get the intersection
471
482
  return self.polygon.intersection(lines_polygon)
472
483
 
473
- def __add__(self, other: Geometry) -> CompoundGeometry:
474
- """Add operator "+" for geometries.
475
-
476
- Arguments:
477
- other (Geometry): The other geometry to add.
478
-
479
- Returns:
480
- CompoundGeometry: A new CompoundGeometry.
481
- """
482
- return CompoundGeometry([self, other])
483
-
484
484
  def __sub__(self, other: Geometry) -> SurfaceGeometry:
485
485
  """Add operator "-" for geometries.
486
486
 
@@ -789,17 +789,6 @@ class CompoundGeometry(Geometry):
789
789
  processed_geoms.append(pg.rotate(angle, point, use_radians))
790
790
  return CompoundGeometry(geometries=processed_geoms)
791
791
 
792
- def __add__(self, other: Geometry) -> CompoundGeometry:
793
- """Add operator "+" for geometries.
794
-
795
- Arguments:
796
- other (Geometry): The other geometry to add.
797
-
798
- Returns:
799
- CompoundGeometry: A new CompoundGeometry.
800
- """
801
- return CompoundGeometry([self, other])
802
-
803
792
  def __sub__(self, other: Geometry) -> CompoundGeometry:
804
793
  """Add operator "-" for geometries.
805
794
 
@@ -78,8 +78,6 @@ def add_reinforcement_line(
78
78
  CompoundGeometry: A compound geometry with the original geometry and
79
79
  the reinforcement.
80
80
  """
81
- from math import floor
82
-
83
81
  p1 = np.array(coords_i)
84
82
  p2 = np.array(coords_j)
85
83
  distance = np.linalg.norm(p2 - p1)
@@ -100,7 +98,7 @@ def add_reinforcement_line(
100
98
  elif s > 0:
101
99
  # Provided the spacing
102
100
  # 1. Compute the number of bars
103
- n = floor(distance / s) + 1
101
+ n = math.floor(distance / s) + 1
104
102
  # 2. Distribute the bars centered in the segment
105
103
  d = (n - 1) * s
106
104
  p1 = p1 + v * (distance - d) / 2.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