structuralcodes 0.1.0__py3-none-any.whl → 0.2.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 (24) 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/ec2_2004/shear.py +5 -1
  5. structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +3 -1
  6. structuralcodes/core/base.py +16 -4
  7. structuralcodes/geometry/_geometry.py +4 -2
  8. structuralcodes/geometry/_reinforcement.py +14 -2
  9. structuralcodes/materials/constitutive_laws/__init__.py +84 -0
  10. structuralcodes/materials/constitutive_laws/_bilinearcompression.py +138 -0
  11. structuralcodes/materials/constitutive_laws/_elastic.py +114 -0
  12. structuralcodes/materials/constitutive_laws/_elasticplastic.py +172 -0
  13. structuralcodes/materials/constitutive_laws/_parabolarectangle.py +198 -0
  14. structuralcodes/materials/constitutive_laws/_popovics.py +133 -0
  15. structuralcodes/materials/constitutive_laws/_sargin.py +115 -0
  16. structuralcodes/materials/constitutive_laws/_userdefined.py +218 -0
  17. structuralcodes/sections/_generic.py +39 -9
  18. structuralcodes/sections/section_integrators/_fiber_integrator.py +1 -3
  19. structuralcodes/sections/section_integrators/_marin_integrator.py +1 -1
  20. {structuralcodes-0.1.0.dist-info → structuralcodes-0.2.0.dist-info}/METADATA +1 -1
  21. {structuralcodes-0.1.0.dist-info → structuralcodes-0.2.0.dist-info}/RECORD +22 -15
  22. structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py +0 -257
  23. structuralcodes/materials/constitutive_laws.py +0 -981
  24. {structuralcodes-0.1.0.dist-info → structuralcodes-0.2.0.dist-info}/WHEEL +0 -0
@@ -272,7 +272,7 @@ class PointGeometry(Geometry):
272
272
  else:
273
273
  # new_material not provided, assume elastic material with same
274
274
  # elastic modulus
275
- new_material = Elastic(E=geo.material.get_tangent(eps=0)[0])
275
+ new_material = Elastic(E=geo.material.get_tangent(eps=0))
276
276
 
277
277
  return PointGeometry(
278
278
  point=geo._point,
@@ -557,6 +557,7 @@ class SurfaceGeometry:
557
557
  poly=affinity.translate(self.polygon, dx, dy),
558
558
  material=self.material,
559
559
  density=self._density,
560
+ concrete=self.concrete
560
561
  )
561
562
 
562
563
  def rotate(
@@ -584,6 +585,7 @@ class SurfaceGeometry:
584
585
  ),
585
586
  material=self.material,
586
587
  density=self._density,
588
+ concrete=self.concrete,
587
589
  )
588
590
 
589
591
  @staticmethod
@@ -622,7 +624,7 @@ class SurfaceGeometry:
622
624
  else:
623
625
  # new_material not provided, assume elastic material with same
624
626
  # elastic modulus
625
- new_material = Elastic(E=geo.material.get_tangent(eps=0)[0])
627
+ new_material = Elastic(E=geo.material.get_tangent(eps=0))
626
628
 
627
629
  return SurfaceGeometry(
628
630
  poly=geo.polygon, material=new_material, density=geo._density
@@ -18,6 +18,7 @@ def add_reinforcement(
18
18
  coords: t.Tuple[float, float],
19
19
  diameter: float,
20
20
  material: t.Union[Material, ConstitutiveLaw],
21
+ group_label: t.Optional[str] = None,
21
22
  ) -> CompoundGeometry:
22
23
  """Add a single bar given coordinate.
23
24
 
@@ -28,12 +29,16 @@ def add_reinforcement(
28
29
  diameter (float): The diameter of the reinforcement.
29
30
  material (Union(Material, ConstitutiveLaw)): A material or a
30
31
  constitutive law for the behavior of the reinforcement.
32
+ group_label (Optional(str)): A label for grouping several objects
33
+ (default is None).
31
34
 
32
35
  Returns:
33
36
  CompoundGeometry: A compound geometry with the original geometry and
34
37
  the reinforcement.
35
38
  """
36
- bar = PointGeometry(Point(coords), diameter, material)
39
+ bar = PointGeometry(
40
+ Point(coords), diameter, material, group_label=group_label
41
+ )
37
42
  return geo + bar
38
43
 
39
44
 
@@ -47,6 +52,7 @@ def add_reinforcement_line(
47
52
  s: float = 0.0,
48
53
  first: bool = True,
49
54
  last: bool = True,
55
+ group_label: t.Optional[str] = None,
50
56
  ) -> CompoundGeometry:
51
57
  """Adds a set of bars distributed in a line.
52
58
 
@@ -66,6 +72,8 @@ def add_reinforcement_line(
66
72
  True).
67
73
  last (bool): Boolean indicating if placing the last bar (default =
68
74
  True).
75
+ group_label (Optional(str)): A label for grouping several objects
76
+ (default is None).
69
77
 
70
78
  Note:
71
79
  At least n or s should be greater than zero.
@@ -110,6 +118,10 @@ def add_reinforcement_line(
110
118
  continue
111
119
  coords = p1 + v * s * i
112
120
  geo = add_reinforcement(
113
- geo, (coords[0], coords[1]), diameter, material
121
+ geo,
122
+ (coords[0], coords[1]),
123
+ diameter,
124
+ material,
125
+ group_label=group_label,
114
126
  )
115
127
  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,138 @@
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_0:
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 get_ultimate_strain(
133
+ self, yielding: bool = False
134
+ ) -> t.Tuple[float, float]:
135
+ """Return the ultimate strain (negative and positive)."""
136
+ if yielding:
137
+ return (self._eps_c, 100)
138
+ return (self._eps_cu, 100)
@@ -0,0 +1,114 @@
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 get_ultimate_strain(self, **kwargs) -> t.Tuple[float, float]:
73
+ """Return the ultimate strain (negative and positive)."""
74
+ # There is no real strain limit, so set it to very large values
75
+ # unlesse specified by the user differently
76
+ del kwargs
77
+ return self._eps_su or (-100, 100)
78
+
79
+ def set_ultimate_strain(
80
+ self, eps_su=t.Union[float, t.Tuple[float, float]]
81
+ ) -> None:
82
+ """Set ultimate strains for Elastic Material if needed.
83
+
84
+ Arguments:
85
+ eps_su (float or (float, float)): Defining ultimate strain. If a
86
+ single value is provided the same is adopted for both negative
87
+ and positive strains. If a tuple is provided, it should be
88
+ given as (negative, positive).
89
+ """
90
+ if isinstance(eps_su, float):
91
+ self._eps_su = (-abs(eps_su), abs(eps_su))
92
+ elif isinstance(eps_su, tuple):
93
+ if len(eps_su) < 2:
94
+ raise ValueError(
95
+ 'Two values need to be provided when setting the tuple'
96
+ )
97
+ eps_su_n = eps_su[0]
98
+ eps_su_p = eps_su[1]
99
+ if eps_su_p < eps_su_n:
100
+ eps_su_p, eps_su_n = eps_su_n, eps_su_p
101
+ if eps_su_p < 0:
102
+ raise ValueError(
103
+ 'Positive ultimate strain should be non-negative'
104
+ )
105
+ if eps_su_n > 0:
106
+ raise ValueError(
107
+ 'Negative utimate strain should be non-positive'
108
+ )
109
+ self._eps_su = (eps_su_n, eps_su_p)
110
+ else:
111
+ raise ValueError(
112
+ 'set_ultimate_strain requires a single value or a tuple \
113
+ with two values'
114
+ )
@@ -0,0 +1,172 @@
1
+ """Elastic-plastic 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 ElasticPlastic(ConstitutiveLaw):
14
+ """Class for elastic-plastic Constitutive Law."""
15
+
16
+ __materials__: t.Tuple[str] = (
17
+ 'steel',
18
+ 'rebars',
19
+ )
20
+
21
+ def __init__(
22
+ self,
23
+ E: float,
24
+ fy: float,
25
+ Eh: float = 0.0,
26
+ eps_su: t.Optional[float] = None,
27
+ name: t.Optional[str] = None,
28
+ ) -> None:
29
+ """Initialize an Elastic-Plastic Material.
30
+
31
+ Arguments:
32
+ E (float): The elastic modulus.
33
+ fy (float): The yield strength.
34
+
35
+ Keyword Arguments:
36
+ Eh (float): The hardening modulus.
37
+ eps_su (float): The ultimate strain.
38
+ name (str): A descriptive name for the constitutive law.
39
+ """
40
+ name = name if name is not None else 'ElasticPlasticLaw'
41
+ super().__init__(name=name)
42
+ if E > 0:
43
+ self._E = E
44
+ else:
45
+ raise ValueError('Elastic modulus E must be greater than zero')
46
+ self._fy = fy
47
+ self._Eh = Eh
48
+ self._eps_su = eps_su
49
+ self._eps_sy = fy / E
50
+
51
+ def get_stress(
52
+ self, eps: t.Union[float, ArrayLike]
53
+ ) -> t.Union[float, ArrayLike]:
54
+ """Return the stress given strain."""
55
+ eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
56
+ # Preprocess eps array in order
57
+ eps = self.preprocess_strains_with_limits(eps=eps)
58
+ # Compute stress
59
+ sig = self._E * eps
60
+ delta_sig = self._fy * (1 - self._Eh / self._E)
61
+ if np.isscalar(sig):
62
+ if sig < -self._fy:
63
+ sig = eps * self._Eh - delta_sig
64
+ if sig > self._fy:
65
+ sig = eps * self._Eh + delta_sig
66
+ if (self._eps_su is not None) and (
67
+ eps > self._eps_su or eps < -self._eps_su
68
+ ):
69
+ sig = 0
70
+ return sig
71
+ sig[sig < -self._fy] = eps[sig < -self._fy] * self._Eh - delta_sig
72
+ sig[sig > self._fy] = eps[sig > self._fy] * self._Eh + delta_sig
73
+ if self._eps_su is not None:
74
+ sig[eps > self._eps_su] = 0
75
+ sig[eps < -self._eps_su] = 0 # pylint: disable=E1130
76
+ return sig
77
+
78
+ def get_tangent(
79
+ self, eps: t.Union[float, ArrayLike]
80
+ ) -> t.Union[float, ArrayLike]:
81
+ """Return the tangent for given strain."""
82
+ if np.isscalar(eps):
83
+ tangent = (
84
+ self._E if -self._eps_sy <= eps <= self._eps_sy else self._Eh
85
+ )
86
+ if (self._eps_su is not None) and (
87
+ eps > self._eps_su or eps < -self._eps_su
88
+ ):
89
+ tangent = 0
90
+ return tangent
91
+
92
+ eps = np.atleast_1d(eps)
93
+ tangent = np.ones_like(eps) * self._E
94
+ tangent[eps > self._eps_sy] = self._Eh
95
+ tangent[eps < -self._eps_sy] = self._Eh
96
+ if self._eps_su is not None:
97
+ tangent[eps > self._eps_su] = 0
98
+ tangent[eps < -self._eps_su] = 0 # pylint: disable=E1130
99
+
100
+ return tangent
101
+
102
+ def __marin__(
103
+ self, strain: t.Tuple[float, float]
104
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
105
+ """Returns coefficients and strain limits for Marin integration in a
106
+ simply formatted way.
107
+
108
+ Arguments:
109
+ strain (float, float): Tuple defining the strain profile: eps =
110
+ strain[0] + strain[1]*y.
111
+
112
+ Example:
113
+ [(0, -0.002), (-0.002, -0.003)]
114
+ [(a0, a1, a2), (a0)]
115
+ """
116
+ strains = []
117
+ coeff = []
118
+ eps_sy_n, eps_sy_p = self.get_ultimate_strain(yielding=True)
119
+ eps_su_n, eps_su_p = self.get_ultimate_strain()
120
+ if strain[1] == 0:
121
+ # Uniform strain equal to strain[0]
122
+ # Understand in which branch are we
123
+ strain[0] = self.preprocess_strains_with_limits(strain[0])
124
+ if strain[0] > eps_sy_p and strain[0] <= eps_su_p:
125
+ # We are in the Hardening part positive
126
+ strains = None
127
+ a0 = self._Eh * strain[0] + self._fy * (1 - self._Eh / self._E)
128
+ a1 = self._Eh * strain[1]
129
+ coeff.append((a0, a1))
130
+ elif strain[0] < eps_sy_n and strain[0] >= eps_su_n:
131
+ # We are in the Hardening part negative
132
+ strains = None
133
+ a0 = self._Eh * strain[0] - self._fy * (1 - self._Eh / self._E)
134
+ a1 = self._Eh * strain[1]
135
+ coeff.append((a0, a1))
136
+ elif abs(strain[0]) <= self._eps_sy:
137
+ # We are in the elastic part
138
+ strains = None
139
+ a0 = self._E * strain[0]
140
+ a1 = self._E * strain[1]
141
+ coeff.append((a0, a1))
142
+ else:
143
+ strains = None
144
+ coeff.append((0.0,))
145
+ else:
146
+ # Hardening part negative
147
+ strains.append((eps_su_n, eps_sy_n))
148
+ a0 = self._Eh * strain[0] - self._fy * (1 - self._Eh / self._E)
149
+ a1 = self._Eh * strain[1]
150
+ coeff.append((a0, a1))
151
+ # Elastic part
152
+ strains.append((eps_sy_n, eps_sy_p))
153
+ a0 = self._E * strain[0]
154
+ a1 = self._E * strain[1]
155
+ coeff.append((a0, a1))
156
+ # Hardening part positive
157
+ strains.append((eps_sy_p, eps_su_p))
158
+ a0 = self._Eh * strain[0] + self._fy * (1 - self._Eh / self._E)
159
+ a1 = self._Eh * strain[1]
160
+ coeff.append((a0, a1))
161
+ return strains, coeff
162
+
163
+ def get_ultimate_strain(
164
+ self, yielding: bool = False
165
+ ) -> t.Tuple[float, float]:
166
+ """Return the ultimate strain (negative and positive)."""
167
+ if yielding:
168
+ return (-self._eps_sy, self._eps_sy)
169
+ # If not specified eps
170
+ if self._eps_su is None:
171
+ return (-self._eps_sy * 2, self._eps_sy * 2)
172
+ return (-self._eps_su, self._eps_su)