structuralcodes 0.1.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of structuralcodes might be problematic. Click here for more details.

Files changed (30) hide show
  1. structuralcodes/__init__.py +1 -1
  2. structuralcodes/codes/ec2_2004/__init__.py +43 -11
  3. structuralcodes/codes/ec2_2004/_concrete_creep_and_shrinkage.py +529 -0
  4. structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +105 -73
  5. structuralcodes/core/_section_results.py +5 -19
  6. structuralcodes/core/base.py +42 -15
  7. structuralcodes/geometry/__init__.py +10 -1
  8. structuralcodes/geometry/_circular.py +81 -0
  9. structuralcodes/geometry/_geometry.py +4 -2
  10. structuralcodes/geometry/_rectangular.py +83 -0
  11. structuralcodes/geometry/_reinforcement.py +132 -5
  12. structuralcodes/materials/constitutive_laws/__init__.py +84 -0
  13. structuralcodes/materials/constitutive_laws/_bilinearcompression.py +183 -0
  14. structuralcodes/materials/constitutive_laws/_elastic.py +133 -0
  15. structuralcodes/materials/constitutive_laws/_elasticplastic.py +227 -0
  16. structuralcodes/materials/constitutive_laws/_parabolarectangle.py +255 -0
  17. structuralcodes/materials/constitutive_laws/_popovics.py +133 -0
  18. structuralcodes/materials/constitutive_laws/_sargin.py +115 -0
  19. structuralcodes/materials/constitutive_laws/_userdefined.py +262 -0
  20. structuralcodes/sections/__init__.py +2 -0
  21. structuralcodes/sections/_generic.py +174 -27
  22. structuralcodes/sections/_rc_utils.py +114 -0
  23. structuralcodes/sections/section_integrators/_fiber_integrator.py +204 -110
  24. structuralcodes/sections/section_integrators/_marin_integrator.py +273 -102
  25. structuralcodes/sections/section_integrators/_section_integrator.py +28 -4
  26. {structuralcodes-0.1.1.dist-info → structuralcodes-0.3.0.dist-info}/METADATA +2 -2
  27. {structuralcodes-0.1.1.dist-info → structuralcodes-0.3.0.dist-info}/RECORD +28 -18
  28. {structuralcodes-0.1.1.dist-info → structuralcodes-0.3.0.dist-info}/WHEEL +1 -1
  29. structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py +0 -257
  30. structuralcodes/materials/constitutive_laws.py +0 -981
@@ -0,0 +1,83 @@
1
+ """Classes for rectangular geometries.
2
+
3
+ The class `RectangularGeometry` represents a rectangular SurfaceGeometry with
4
+ homogenous material.
5
+ This class is simply a wrapper of `SurfaceGeometry` class and permits an easy
6
+ input by the user.
7
+ """
8
+
9
+ import typing as t
10
+
11
+ from shapely import Polygon
12
+
13
+ from structuralcodes.core.base import ConstitutiveLaw, Material
14
+
15
+ from ._geometry import SurfaceGeometry
16
+
17
+
18
+ class RectangularGeometry(SurfaceGeometry):
19
+ """This is a wrapper class for defining a `SurfaceGeometry` of rectangular
20
+ shape with a homogeneous material.
21
+ """
22
+
23
+ _width: float
24
+ _height: float
25
+
26
+ def __init__(
27
+ self,
28
+ width: float,
29
+ height: float,
30
+ material: t.Union[Material, ConstitutiveLaw],
31
+ density: t.Optional[float] = None,
32
+ concrete: bool = False,
33
+ ) -> None:
34
+ """Initialize a RectangularGeometry.
35
+
36
+ Arguments:
37
+ width (float): The width of the geometry.
38
+ height (float): The height of the geometry.
39
+ material (Union(Material, ConstitutiveLaw)): A Material or
40
+ ConsitutiveLaw class applied to the geometry.
41
+ density (Optional(float)): When a ConstitutiveLaw is passed as
42
+ material, the density can be provided by this argument. When
43
+ material is a Material object the density is taken from the
44
+ material.
45
+ concrete (bool): Flag to indicate if the geometry is concrete. When
46
+ passing a Material as material, this is automatically inferred.
47
+
48
+ Note:
49
+ The RectangularGeometry is simply a wrapper for a SurfaceGeometry
50
+ object.
51
+ """
52
+ # Check that size is strictly positive
53
+ if width <= 0:
54
+ raise ValueError('Width must be a positive number.')
55
+ if height <= 0:
56
+ raise ValueError('Height must be a positive number.')
57
+
58
+ self._width = width
59
+ self._height = height
60
+
61
+ # Create the shapely polygon
62
+ polygon = Polygon(
63
+ (
64
+ (-width / 2, -height / 2),
65
+ (width / 2, -height / 2),
66
+ (width / 2, height / 2),
67
+ (-width / 2, height / 2),
68
+ )
69
+ )
70
+ # Pass everything to the base class
71
+ super().__init__(
72
+ poly=polygon, material=material, density=density, concrete=concrete
73
+ )
74
+
75
+ @property
76
+ def width(self):
77
+ """Returns the width of the rectangle."""
78
+ return self._width
79
+
80
+ @property
81
+ def height(self):
82
+ """Return the height of the rectangle."""
83
+ return self._height
@@ -1,16 +1,14 @@
1
1
  """Functions related to reinforcement definition."""
2
2
 
3
+ import math
3
4
  import typing as t
4
5
 
5
6
  import numpy as np
6
7
  from shapely import Point
7
8
 
8
9
  from structuralcodes.core.base import ConstitutiveLaw, Material
9
- from structuralcodes.geometry import (
10
- CompoundGeometry,
11
- PointGeometry,
12
- SurfaceGeometry,
13
- )
10
+
11
+ from ._geometry import CompoundGeometry, PointGeometry, SurfaceGeometry
14
12
 
15
13
 
16
14
  def add_reinforcement(
@@ -125,3 +123,132 @@ def add_reinforcement_line(
125
123
  group_label=group_label,
126
124
  )
127
125
  return geo
126
+
127
+
128
+ def add_reinforcement_circle(
129
+ geo: t.Union[SurfaceGeometry, CompoundGeometry],
130
+ center: t.Tuple[float, float],
131
+ radius: float,
132
+ diameter: float,
133
+ material: t.Union[Material, ConstitutiveLaw],
134
+ n: int = 0,
135
+ s: float = 0.0,
136
+ first: bool = True,
137
+ last: bool = True,
138
+ start_angle: float = 0.0,
139
+ stop_angle: float = 2 * np.pi,
140
+ group_label: t.Optional[str] = None,
141
+ ) -> CompoundGeometry:
142
+ """Adds a set of bars distributed in a circular arch line.
143
+ By default the whole circle is considered. If one wants to specify a
144
+ circular arch, the `start_angle` and `stop_angle` attributes need to be
145
+ specified.
146
+
147
+ Arguments:
148
+ geo (Union(SurfaceGeometry, CompoundGeometry)): The geometry used as
149
+ input.
150
+ center (Tuple(float, float)): Coordinates of the center point of
151
+ the circle line where reinforcement will be added.
152
+ radius (float): Radius of the circle line where reinforcement will be
153
+ added.
154
+ diameter (float): The diameter of the bars.
155
+ material (Union(Material, ConstitutiveLaw)): A valid material or
156
+ constitutive law.
157
+ n (int): The number of bars to be distributed inside the line (default
158
+ = 0).
159
+ s (float): The distance between the bars (default = 0).
160
+ first (bool): Boolean indicating if placing the first bar (default =
161
+ True).
162
+ last (bool): Boolean indicating if placing the last bar (default =
163
+ True).
164
+ start_angle (float): Start angle (respect to X axis) for defining the
165
+ arch where to add bars in radians (default = 0)
166
+ stop_angle (float): Stop angle (respect to X axis) for defining the
167
+ arch where to add bars in radians (default = 2pi)
168
+ group_label (Optional(str)): A label for grouping several objects
169
+ (default is None).
170
+
171
+ Note:
172
+ At least n or s should be greater than zero.
173
+ Attribues start_angle and stop_angle by default are 0 and 2pi
174
+ respectively, so that bars will be distributed along the whole circle.
175
+ If only a portion of the circle must be used (i.e. an arch of
176
+ circumference), then set start and stop angles correspondingly.
177
+ stop_angle must always be larger than start_angle.
178
+
179
+ Returns:
180
+ CompoundGeometry: A compound geometry with the original geometry and
181
+ the reinforcement.
182
+ """
183
+ # Check that difference between stop and start angle is
184
+ # positive and less than 2pi
185
+ if stop_angle - start_angle <= 0 or stop_angle - start_angle > 2 * np.pi:
186
+ raise ValueError(
187
+ 'Stop angle should be larger than start angle and difference \
188
+ them should be at most 2pi.'
189
+ )
190
+ # Calculate length from start_angle to stop_angle
191
+ length = radius * (stop_angle - start_angle)
192
+
193
+ # If the whole circle, than deal with the case that would add an extra bar
194
+ whole = math.isclose(length - 2 * np.pi * radius, 0, abs_tol=1e-4)
195
+ add_n = 0 if not whole else 1
196
+
197
+ # delta_angle is used if we need to center the set of bars
198
+ # in the curve.
199
+ delta_angle = 0
200
+ if n > 0 and s > 0:
201
+ # Provided both the number of bars and spacing
202
+ # Check there is enough space for fitting the bars
203
+ n += add_n
204
+ needed_length = (n - 1) * s
205
+ if needed_length > length:
206
+ raise ValueError(
207
+ f'There is not room to fit {n} bars with a spacing of {s} \
208
+ in {length}'
209
+ )
210
+ # Compute delta_angle to make bars centered in the curvilinear segment
211
+ delta_angle = (length - needed_length) / 2.0 / radius
212
+ elif n > 0:
213
+ # Provided the number of bars
214
+ s = length / (n - 1)
215
+ # If we are distributing bars i the whole circle add a fictitious extra
216
+ # bar (than later will be removed).
217
+ n += add_n
218
+ elif s > 0:
219
+ # Provided the spacing
220
+ # 1. Compute the number of bars
221
+ n = math.floor(length / s) + 1
222
+ # 2. Distribute the bars centered in the curvilinear segment
223
+ needed_length = (n - 1) * s
224
+ delta_angle = (length - needed_length) / 2.0 / radius
225
+ # set whole to False bacause in this case we don't need to deal with
226
+ # the special case
227
+ whole = False
228
+ else:
229
+ raise ValueError('At least n or s should be provided')
230
+
231
+ phi_rebars = np.linspace(
232
+ start_angle + delta_angle, stop_angle - delta_angle, n
233
+ )
234
+ if whole:
235
+ n -= 1
236
+ phi_rebars = phi_rebars[:-1]
237
+
238
+ x = center[0] + radius * np.cos(phi_rebars)
239
+ y = center[1] + radius * np.sin(phi_rebars)
240
+
241
+ # add the bars
242
+ for i in range(n):
243
+ if i == 0 and not first:
244
+ continue
245
+ if i == n - 1 and not last:
246
+ continue
247
+ geo = add_reinforcement(
248
+ geo,
249
+ (x[i], y[i]),
250
+ diameter,
251
+ material,
252
+ group_label=group_label,
253
+ )
254
+ return geo
@@ -0,0 +1,84 @@
1
+ """Constitutive laws for materials."""
2
+
3
+ import typing as t
4
+
5
+ from ...core.base import ConstitutiveLaw, Material
6
+ from ._bilinearcompression import BilinearCompression
7
+ from ._elastic import Elastic
8
+ from ._elasticplastic import ElasticPlastic
9
+ from ._parabolarectangle import ParabolaRectangle
10
+ from ._popovics import Popovics
11
+ from ._sargin import Sargin
12
+ from ._userdefined import UserDefined
13
+
14
+ __all__ = [
15
+ 'Elastic',
16
+ 'ElasticPlastic',
17
+ 'ParabolaRectangle',
18
+ 'BilinearCompression',
19
+ 'Popovics',
20
+ 'Sargin',
21
+ 'UserDefined',
22
+ 'get_constitutive_laws_list',
23
+ 'create_constitutive_law',
24
+ ]
25
+
26
+ CONSTITUTIVE_LAWS: t.Dict[str, ConstitutiveLaw] = {
27
+ 'elastic': Elastic,
28
+ 'elasticplastic': ElasticPlastic,
29
+ 'elasticperfectlyplastic': ElasticPlastic,
30
+ 'bilinearcompression': BilinearCompression,
31
+ 'parabolarectangle': ParabolaRectangle,
32
+ 'popovics': Popovics,
33
+ 'sargin': Sargin,
34
+ }
35
+
36
+
37
+ def get_constitutive_laws_list() -> t.List[str]:
38
+ """Returns a list with valid keywords for constitutive law factory."""
39
+ return list(CONSTITUTIVE_LAWS.keys())
40
+
41
+
42
+ def create_constitutive_law(
43
+ constitutive_law_name: str, material: Material
44
+ ) -> ConstitutiveLaw:
45
+ """A factory function to create the constitutive law.
46
+
47
+ Arguments:
48
+ constitutive_law_name (str): A string defining a valid constitutive law
49
+ type. The available keys can be get with the method
50
+ `get_constitutive_laws_list`.
51
+ material (Material): The material containing the properties needed for
52
+ the definition of the constitutive law.
53
+
54
+ Note:
55
+ For working with this facotry function, the material class
56
+ implementations need to provide special dunder methods (__elastic__,
57
+ __parabolarectangle__, etc.) needed for the specific material that
58
+ return the kwargs needed to create the corresponding constitutive
59
+ law object. If the special dunder method is not found an exception
60
+ will be raised.
61
+
62
+ If the consitutive law selected is not available for the specific
63
+ material, an exception will be raised.
64
+ """
65
+ law = None
66
+ const_law = CONSTITUTIVE_LAWS.get(constitutive_law_name.lower())
67
+ if const_law is not None:
68
+ method_name = f'__{constitutive_law_name}__'
69
+ # check if the material object has the special method needed
70
+ if hasattr(material, method_name):
71
+ method = getattr(material, method_name)
72
+ if callable(method):
73
+ # get the kwargs from the special dunder method
74
+ kwargs = method()
75
+ # create the constitutive law
76
+ law = const_law(**kwargs)
77
+ else:
78
+ raise ValueError(
79
+ f'Constitutive law {constitutive_law_name} not available for'
80
+ f' material {material.__class__.__name__}'
81
+ )
82
+ else:
83
+ raise ValueError(f'Unknown constitutive law: {constitutive_law_name}')
84
+ return law
@@ -0,0 +1,183 @@
1
+ """Bilinear compression constitutive law."""
2
+
3
+ from __future__ import annotations # To have clean hints of ArrayLike in docs
4
+
5
+ import typing as t
6
+
7
+ import numpy as np
8
+ from numpy.typing import ArrayLike
9
+
10
+ from ...core.base import ConstitutiveLaw
11
+
12
+
13
+ class BilinearCompression(ConstitutiveLaw):
14
+ """Class for Bilinear Elastic-PerfectlyPlastic Constitutive Law for
15
+ Concrete (only compression behavior).
16
+ """
17
+
18
+ __materials__: t.Tuple[str] = ('concrete',)
19
+
20
+ def __init__(
21
+ self,
22
+ fc: float,
23
+ eps_c: float,
24
+ eps_cu: t.Optional[float] = None,
25
+ name: t.Optional[str] = None,
26
+ ) -> None:
27
+ """Initialize a BilinearCompression Material.
28
+
29
+ Arguments:
30
+ fc (float): Compressive strength (negative number).
31
+ eps_c (float): Strain at compressive strength (pure number).
32
+
33
+ Keyword Arguments:
34
+ eps_cu (float): Ultimate strain (pure number).
35
+ name (str): A descriptive name for the constitutive law.
36
+ """
37
+ name = name if name is not None else 'BilinearCompressionLaw'
38
+ super().__init__(name=name)
39
+ self._fc = -abs(fc)
40
+ self._eps_c = -abs(eps_c)
41
+ self._eps_cu = -abs(eps_cu)
42
+ self._E = self._fc / self._eps_c
43
+
44
+ def get_stress(
45
+ self, eps: t.Union[float, ArrayLike]
46
+ ) -> t.Union[float, ArrayLike]:
47
+ """Return the stress given strain."""
48
+ eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
49
+ # Preprocess eps array in order
50
+ eps = self.preprocess_strains_with_limits(eps=eps)
51
+ # Compute stress
52
+ # If it is a scalar
53
+ if np.isscalar(eps):
54
+ sig = 0
55
+ if self._fc / self._E <= eps <= 0:
56
+ sig = self._E * eps
57
+ return sig
58
+ # If it is an array
59
+ sig = self._E * eps
60
+ sig[sig < self._fc] = self._fc
61
+ sig[eps > 0] = 0
62
+ sig[eps < self._eps_cu] = 0
63
+ return sig
64
+
65
+ def get_tangent(
66
+ self, eps: t.Union[float, ArrayLike]
67
+ ) -> t.Union[float, ArrayLike]:
68
+ """Return the tangent for given strain."""
69
+ eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
70
+ # If it is a scalar
71
+ if np.isscalar(eps):
72
+ tangent = 0
73
+ if self._fc / self._E <= eps <= 0:
74
+ tangent = self._E
75
+ return tangent
76
+ # If it is an array
77
+ tangent = np.ones_like(eps) * self._E
78
+ tangent[eps < self._eps_c] = 0.0
79
+
80
+ return tangent
81
+
82
+ def __marin__(
83
+ self, strain: t.Tuple[float, float]
84
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
85
+ """Returns coefficients and strain limits for Marin integration in a
86
+ simply formatted way.
87
+
88
+ Arguments:
89
+ strain (float, float): Tuple defining the strain profile: eps =
90
+ strain[0] + strain[1]*y.
91
+
92
+ Example:
93
+ [(0, -0.002), (-0.002, -0.003)]
94
+ [(a0, a1, a2), (a0)]
95
+ """
96
+ strains = []
97
+ coeff = []
98
+ if strain[1] == 0:
99
+ # Uniform strain equal to strain[0]
100
+ # understand in which branch we are
101
+ strain[0] = self.preprocess_strains_with_limits(strain[0])
102
+ if strain[0] > 0:
103
+ # We are in tensile branch
104
+ strains = None
105
+ coeff.append((0.0,))
106
+ elif strain[0] > self._eps_c:
107
+ # We are in the linear branch
108
+ strains = None
109
+ a0 = self._E * strain[0]
110
+ a1 = self._E * strain[1]
111
+ coeff.append((a0, a1))
112
+ elif strain[0] >= self._eps_cu:
113
+ # We are in the constant branch
114
+ strains = None
115
+ coeff.append((self._fc,))
116
+ else:
117
+ # We are in a branch of non-resisting concrete
118
+ # Too much compression
119
+ strains = None
120
+ coeff.append((0.0,))
121
+ else:
122
+ # linear part
123
+ strains.append((self._eps_c, 0))
124
+ a0 = self._E * strain[0]
125
+ a1 = self._E * strain[1]
126
+ coeff.append((a0, a1))
127
+ # Constant part
128
+ strains.append((self._eps_cu, self._eps_c))
129
+ coeff.append((self._fc,))
130
+ return strains, coeff
131
+
132
+ def __marin_tangent__(
133
+ self, strain: t.Tuple[float, float]
134
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
135
+ """Returns coefficients and strain limits for Marin integration of
136
+ tangent in a simply formatted way.
137
+
138
+ Arguments:
139
+ strain (float, float): Tuple defining the strain profile: eps =
140
+ strain[0] + strain[1]*y.
141
+
142
+ Example:
143
+ [(0, -0.002), (-0.002, -0.003)]
144
+ [(a0, a1, a2), (a0)]
145
+ """
146
+ strains = []
147
+ coeff = []
148
+ if strain[1] == 0:
149
+ # Uniform strain equal to strain[0]
150
+ # understand in which branch we are
151
+ strain[0] = self.preprocess_strains_with_limits(strain[0])
152
+ if strain[0] > 0:
153
+ # We are in tensile branch
154
+ strains = None
155
+ coeff.append((0.0,))
156
+ elif strain[0] > self._eps_c:
157
+ # We are in the linear branch
158
+ strains = None
159
+ a0 = self._E
160
+ coeff.append((a0,))
161
+ else:
162
+ # We are in the constant branch or
163
+ # We are in a branch of non-resisting concrete
164
+ # Too much compression
165
+ strains = None
166
+ coeff.append((0.0,))
167
+ else:
168
+ # linear part
169
+ strains.append((self._eps_c, 0))
170
+ a0 = self._E
171
+ coeff.append((a0,))
172
+ # Constant part
173
+ strains.append((self._eps_cu, self._eps_c))
174
+ coeff.append((0.0,))
175
+ return strains, coeff
176
+
177
+ def get_ultimate_strain(
178
+ self, yielding: bool = False
179
+ ) -> t.Tuple[float, float]:
180
+ """Return the ultimate strain (negative and positive)."""
181
+ if yielding:
182
+ return (self._eps_c, 100)
183
+ return (self._eps_cu, 100)
@@ -0,0 +1,133 @@
1
+ """Elastic constitutive law."""
2
+
3
+ from __future__ import annotations # To have clean hints of ArrayLike in docs
4
+
5
+ import typing as t
6
+
7
+ import numpy as np
8
+ from numpy.typing import ArrayLike
9
+
10
+ from ...core.base import ConstitutiveLaw
11
+
12
+
13
+ class Elastic(ConstitutiveLaw):
14
+ """Class for elastic constitutive law."""
15
+
16
+ __materials__: t.Tuple[str] = (
17
+ 'concrete',
18
+ 'steel',
19
+ 'rebars',
20
+ )
21
+
22
+ def __init__(self, E: float, name: t.Optional[str] = None) -> None:
23
+ """Initialize an Elastic Material.
24
+
25
+ Arguments:
26
+ E (float): The elastic modulus.
27
+
28
+ Keyword Arguments:
29
+ name (str): A descriptive name for the constitutive law.
30
+ """
31
+ name = name if name is not None else 'ElasticLaw'
32
+ super().__init__(name=name)
33
+ self._E = E
34
+ self._eps_su = None
35
+
36
+ def get_stress(
37
+ self, eps: t.Union[float, ArrayLike]
38
+ ) -> t.Union[float, ArrayLike]:
39
+ """Return stress given strain."""
40
+ eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
41
+ return self._E * eps
42
+
43
+ def get_tangent(
44
+ self, eps: t.Union[float, ArrayLike]
45
+ ) -> t.Union[float, ArrayLike]:
46
+ """Return the tangent."""
47
+ if np.isscalar(eps):
48
+ return self._E
49
+ eps = np.atleast_1d(eps)
50
+ return np.ones_like(eps) * self._E
51
+
52
+ def __marin__(
53
+ self, strain: t.Tuple[float, float]
54
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
55
+ """Returns coefficients and strain limits for Marin integration in a
56
+ simply formatted way.
57
+
58
+ Arguments:
59
+ strain (float, float): Tuple defining the strain profile: eps =
60
+ strain[0] + strain[1]*y.
61
+
62
+ Example:
63
+ [(0, -0.002), (-0.002, -0.003)]
64
+ [(a0, a1, a2), (a0)]
65
+ """
66
+ strains = None
67
+ a0 = self._E * strain[0]
68
+ a1 = self._E * strain[1]
69
+ coeff = [(a0, a1)]
70
+ return strains, coeff
71
+
72
+ def __marin_tangent__(
73
+ self, strain: t.Tuple[float, float]
74
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
75
+ """Returns coefficients and strain limits for Marin integration of
76
+ tangent in a simply formatted way.
77
+
78
+ Arguments:
79
+ strain (float, float): Tuple defining the strain profile: eps =
80
+ strain[0] + strain[1]*y.
81
+
82
+ Example:
83
+ [(0, -0.002), (-0.002, -0.003)]
84
+ [(a0, a1, a2), (a0)]
85
+ """
86
+ strains = None
87
+ a0 = self._E
88
+ coeff = [(a0,)]
89
+ return strains, coeff
90
+
91
+ def get_ultimate_strain(self, **kwargs) -> t.Tuple[float, float]:
92
+ """Return the ultimate strain (negative and positive)."""
93
+ # There is no real strain limit, so set it to very large values
94
+ # unlesse specified by the user differently
95
+ del kwargs
96
+ return self._eps_su or (-100, 100)
97
+
98
+ def set_ultimate_strain(
99
+ self, eps_su=t.Union[float, t.Tuple[float, float]]
100
+ ) -> None:
101
+ """Set ultimate strains for Elastic Material if needed.
102
+
103
+ Arguments:
104
+ eps_su (float or (float, float)): Defining ultimate strain. If a
105
+ single value is provided the same is adopted for both negative
106
+ and positive strains. If a tuple is provided, it should be
107
+ given as (negative, positive).
108
+ """
109
+ if isinstance(eps_su, float):
110
+ self._eps_su = (-abs(eps_su), abs(eps_su))
111
+ elif isinstance(eps_su, tuple):
112
+ if len(eps_su) < 2:
113
+ raise ValueError(
114
+ 'Two values need to be provided when setting the tuple'
115
+ )
116
+ eps_su_n = eps_su[0]
117
+ eps_su_p = eps_su[1]
118
+ if eps_su_p < eps_su_n:
119
+ eps_su_p, eps_su_n = eps_su_n, eps_su_p
120
+ if eps_su_p < 0:
121
+ raise ValueError(
122
+ 'Positive ultimate strain should be non-negative'
123
+ )
124
+ if eps_su_n > 0:
125
+ raise ValueError(
126
+ 'Negative utimate strain should be non-positive'
127
+ )
128
+ self._eps_su = (eps_su_n, eps_su_p)
129
+ else:
130
+ raise ValueError(
131
+ 'set_ultimate_strain requires a single value or a tuple \
132
+ with two values'
133
+ )