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,227 @@
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 __marin_tangent__(
164
+ self, strain: t.Tuple[float, float]
165
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
166
+ """Returns coefficients and strain limits for Marin integration of
167
+ tangent in a simply formatted way.
168
+
169
+ Arguments:
170
+ strain (float, float): Tuple defining the strain profile: eps =
171
+ strain[0] + strain[1]*y.
172
+
173
+ Example:
174
+ [(0, -0.002), (-0.002, -0.003)]
175
+ [(a0, a1, a2), (a0)]
176
+ """
177
+ strains = []
178
+ coeff = []
179
+ eps_sy_n, eps_sy_p = self.get_ultimate_strain(yielding=True)
180
+ eps_su_n, eps_su_p = self.get_ultimate_strain()
181
+ if strain[1] == 0:
182
+ # Uniform strain equal to strain[0]
183
+ # Understand in which branch are we
184
+ strain[0] = self.preprocess_strains_with_limits(strain[0])
185
+ if strain[0] > eps_sy_p and strain[0] <= eps_su_p:
186
+ # We are in the Hardening part positive
187
+ strains = None
188
+ a0 = self._Eh
189
+ coeff.append((a0,))
190
+ elif strain[0] < eps_sy_n and strain[0] >= eps_su_n:
191
+ # We are in the Hardening part negative
192
+ strains = None
193
+ a0 = self._Eh
194
+ coeff.append((a0,))
195
+ elif abs(strain[0]) <= self._eps_sy:
196
+ # We are in the elastic part
197
+ strains = None
198
+ a0 = self._E
199
+ coeff.append((a0,))
200
+ else:
201
+ strains = None
202
+ coeff.append((0.0,))
203
+ else:
204
+ # Hardening part negative
205
+ strains.append((eps_su_n, eps_sy_n))
206
+ a0 = self._Eh
207
+ coeff.append((a0,))
208
+ # Elastic part
209
+ strains.append((eps_sy_n, eps_sy_p))
210
+ a0 = self._E
211
+ coeff.append((a0,))
212
+ # Hardening part positive
213
+ strains.append((eps_sy_p, eps_su_p))
214
+ a0 = self._Eh
215
+ coeff.append((a0,))
216
+ return strains, coeff
217
+
218
+ def get_ultimate_strain(
219
+ self, yielding: bool = False
220
+ ) -> t.Tuple[float, float]:
221
+ """Return the ultimate strain (negative and positive)."""
222
+ if yielding:
223
+ return (-self._eps_sy, self._eps_sy)
224
+ # If not specified eps
225
+ if self._eps_su is None:
226
+ return (-self._eps_sy * 2, self._eps_sy * 2)
227
+ return (-self._eps_su, self._eps_su)
@@ -0,0 +1,255 @@
1
+ """Parabola-Rectangle 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 ParabolaRectangle(ConstitutiveLaw):
14
+ """Class for parabola rectangle constitutive law.
15
+
16
+ The stresses and strains are assumed negative in compression and positive
17
+ in tension.
18
+ """
19
+
20
+ __materials__: t.Tuple[str] = ('concrete',)
21
+
22
+ def __init__(
23
+ self,
24
+ fc: float,
25
+ eps_0: float = -0.002,
26
+ eps_u: float = -0.0035,
27
+ n: float = 2.0,
28
+ name: t.Optional[str] = None,
29
+ ) -> None:
30
+ """Initialize a Parabola-Rectangle Material.
31
+
32
+ Arguments:
33
+ fc (float): The strength of concrete in compression.
34
+
35
+ Keyword Arguments:
36
+ eps_0 (float): Peak strain of concrete in compression. Default
37
+ value = -0.002.
38
+ eps_u (float): Ultimate strain of concrete in compression. Default
39
+ value = -0.0035.
40
+ n (float): Exponent for the pre-peak branch. Default value = 2.
41
+ name (str): A name for the constitutive law.
42
+ """
43
+ name = name if name is not None else 'ParabolaRectangleLaw'
44
+ super().__init__(name=name)
45
+ self._fc = -abs(fc)
46
+ self._eps_0 = -abs(eps_0)
47
+ self._eps_u = -abs(eps_u)
48
+ self._n = n
49
+
50
+ def get_stress(
51
+ self, eps: t.Union[float, ArrayLike]
52
+ ) -> t.Union[float, ArrayLike]:
53
+ """Return the stress given strain."""
54
+ eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
55
+ # Preprocess eps array in order
56
+ eps = self.preprocess_strains_with_limits(eps=eps)
57
+ # Compute stress
58
+ # If it is a scalar
59
+ if np.isscalar(eps):
60
+ sig = 0
61
+ if self._eps_0 <= eps <= 0:
62
+ sig = self._fc * (1 - (1 - eps / self._eps_0) ** self._n)
63
+ if self._eps_u <= eps < self._eps_0:
64
+ sig = self._fc
65
+ return sig
66
+ # If it is an array
67
+ sig = np.zeros_like(eps)
68
+ # Parabolic branch
69
+ sig[(eps <= 0) & (eps >= self._eps_0)] = self._fc * (
70
+ 1
71
+ - (1 - (eps[(eps <= 0) & (eps >= self._eps_0)] / self._eps_0))
72
+ ** self._n
73
+ )
74
+ # Rectangle branch
75
+ sig[eps < self._eps_0] = self._fc
76
+ # Zero elsewhere
77
+ sig[eps < self._eps_u] = 0
78
+ sig[eps > 0] = 0
79
+ return sig
80
+
81
+ def get_tangent(
82
+ self, eps: t.Union[float, ArrayLike]
83
+ ) -> t.Union[float, ArrayLike]:
84
+ """Return the tangent given strain."""
85
+ eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
86
+ # If it is a scalar
87
+ if np.isscalar(eps):
88
+ tangent = 0
89
+ if self._eps_0 <= eps <= 0:
90
+ tangent = (
91
+ self._n
92
+ * self._fc
93
+ / self._eps_0
94
+ * (1 - (eps / self._eps_0)) ** (self._n - 1)
95
+ )
96
+ return tangent
97
+ # If it is an array
98
+ # parabolic branch
99
+ tangent = np.zeros_like(eps)
100
+ tangent[(eps <= 0) & (eps >= self._eps_0)] = (
101
+ self._n
102
+ * self._fc
103
+ / self._eps_0
104
+ * (1 - (eps[(eps <= 0) & (eps >= self._eps_0)] / self._eps_0))
105
+ ** (self._n - 1)
106
+ )
107
+ # Elsewhere tangent is zero
108
+ tangent[eps < self._eps_0] = 0.0
109
+ tangent[eps > 0] = 0.0
110
+ return tangent
111
+
112
+ def __marin__(
113
+ self, strain: t.Tuple[float, float]
114
+ ) -> t.Tuple[t.List[float], t.List[float]]:
115
+ """Returns coefficients and strain limits for Marin integration in a
116
+ simply formatted way.
117
+
118
+ Arguments:
119
+ strain (float, float): Tuple defining the strain profile: eps =
120
+ strain[0] + strain[1]*y.
121
+
122
+ Example:
123
+ [(0, -0.002), (-0.002, -0.003)]
124
+ [(a0, a1, a2), (a0)]
125
+ """
126
+ if self._n != 2:
127
+ # The constitutive law is not writtable as a polynomial,
128
+ # Call the generic distretizing method
129
+ return super().__marin__(strain=strain)
130
+
131
+ strains = []
132
+ coeff = []
133
+ if strain[1] == 0:
134
+ # Uniform strain equal to strain[0]
135
+ # understand in which branch are we
136
+ strain[0] = self.preprocess_strains_with_limits(strain[0])
137
+ if strain[0] > 0:
138
+ # We are in tensile branch
139
+ strains = None
140
+ coeff.append((0.0,))
141
+ elif strain[0] > self._eps_0:
142
+ # We are in the parabolic branch
143
+ strains = None
144
+ a0 = (
145
+ 2
146
+ * self._fc
147
+ * strain[0]
148
+ / self._eps_0
149
+ * (1 - 0.5 * (strain[0] / self._eps_0))
150
+ )
151
+ a1 = (
152
+ 2
153
+ * self._fc
154
+ / self._eps_0
155
+ * strain[1]
156
+ * (1 - strain[0] / self._eps_0)
157
+ )
158
+ coeff.append((a0, a1, 0.0))
159
+ elif strain[0] >= self._eps_u:
160
+ # We are in the constant branch
161
+ strains = None
162
+ coeff.append((self._fc,))
163
+ else:
164
+ # We are in a branch of non-resisting concrete
165
+ # Too much compression
166
+ strains = None
167
+ coeff.append((0.0,))
168
+ else:
169
+ # Parabolic part
170
+ strains.append((self._eps_0, 0))
171
+ a0 = (
172
+ 2
173
+ * self._fc
174
+ * strain[0]
175
+ / self._eps_0
176
+ * (1 - 0.5 * (strain[0] / self._eps_0))
177
+ )
178
+ a1 = (
179
+ 2
180
+ * self._fc
181
+ / self._eps_0
182
+ * strain[1]
183
+ * (1 - strain[0] / self._eps_0)
184
+ )
185
+ a2 = -self._fc * strain[1] ** 2 / self._eps_0**2
186
+ coeff.append((a0, a1, a2))
187
+ # Constant part
188
+ strains.append((self._eps_u, self._eps_0))
189
+ coeff.append((self._fc,))
190
+ return strains, coeff
191
+
192
+ def __marin_tangent__(
193
+ self, strain: t.Tuple[float, float]
194
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
195
+ """Returns coefficients and strain limits for Marin integration of
196
+ tangent in a simply formatted way.
197
+
198
+ Arguments:
199
+ strain (float, float): Tuple defining the strain profile: eps =
200
+ strain[0] + strain[1]*y.
201
+
202
+ Example:
203
+ [(0, -0.002), (-0.002, -0.003)]
204
+ [(a0, a1, a2), (a0)]
205
+ """
206
+ if self._n != 2:
207
+ # The constitutive law is not writtable as a polynomial,
208
+ # Call the generic distretizing method
209
+ return super().__marin_tangent__(strain=strain)
210
+
211
+ strains = []
212
+ coeff = []
213
+ if strain[1] == 0:
214
+ # Uniform strain equal to strain[0]
215
+ # understand in which branch are we
216
+ strain[0] = self.preprocess_strains_with_limits(strain[0])
217
+ if strain[0] > 0:
218
+ # We are in tensile branch
219
+ strains = None
220
+ coeff.append((0.0,))
221
+ elif strain[0] > self._eps_0:
222
+ # We are in the parabolic branch
223
+ strains = None
224
+ a0 = (
225
+ 2
226
+ * self._fc
227
+ / self._eps_0
228
+ * (1 - (strain[0] / self._eps_0))
229
+ )
230
+ a1 = -2 * self._fc / self._eps_0**2 * strain[1]
231
+ coeff.append((a0, a1))
232
+ else:
233
+ # We are in the constant branch or
234
+ # We are in a branch of non-resisting concrete
235
+ # Too much compression
236
+ strains = None
237
+ coeff.append((0.0,))
238
+ else:
239
+ # Parabolic part
240
+ strains.append((self._eps_0, 0))
241
+ a0 = 2 * self._fc / self._eps_0 * (1 - (strain[0] / self._eps_0))
242
+ a1 = -2 * self._fc / self._eps_0**2 * strain[1]
243
+ coeff.append((a0, a1))
244
+ # Constant part
245
+ strains.append((self._eps_u, self._eps_0))
246
+ coeff.append((0.0,))
247
+ return strains, coeff
248
+
249
+ def get_ultimate_strain(
250
+ self, yielding: bool = False
251
+ ) -> t.Tuple[float, float]:
252
+ """Return the ultimate strain (negative and positive)."""
253
+ if yielding:
254
+ return (self._eps_0, 100)
255
+ return (self._eps_u, 100)
@@ -0,0 +1,133 @@
1
+ """Popovics 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 Popovics(ConstitutiveLaw):
14
+ """Class for Popovics-Mander constitutive law.
15
+
16
+ The stresses and strains are assumed negative in compression and positive
17
+ in tension.
18
+
19
+ If the relation Ec = 5000 * sqrt(fc) is used for elastic modulus, the
20
+ constitutive law is identical to the one proposed by Mander et al. (1988).
21
+
22
+ References:
23
+ Popovics, S., 1973, “A Numerical Approach to the Complete Stress-Strain
24
+ Curve of Concrete”, Cement and Concrete Research, 3(4), 583-599.
25
+
26
+ Mander, J.B., Priestley, M.J.N., Park, R., 1988, "Theoretical Stress-Strain
27
+ Model for Confined Concrete", Journal of Structural Engineering, 114(8),
28
+ 1804-1826.
29
+ """
30
+
31
+ __materials__: t.Tuple[str] = ('concrete',)
32
+
33
+ def __init__(
34
+ self,
35
+ fc: float,
36
+ eps_c: float = -0.002,
37
+ eps_cu: float = -0.0035,
38
+ Ec: t.Optional[float] = None,
39
+ name: t.Optional[str] = None,
40
+ ) -> None:
41
+ """Initialize a Popovics Material.
42
+
43
+ Arguments:
44
+ fc (float): the strength of concrete in compression
45
+
46
+ Keyword Arguments:
47
+ eps_c (float): Peak strain of concrete in compression. Default
48
+ value = -0.002.
49
+ eps_cu (float): Ultimate strain of concrete in compression. Default
50
+ value = -0.0035.
51
+ E (optional float): Elastic modulus of concrete. If None, the
52
+ equation Ec = 5000 * fc**0.5 proposed by Mander et al. (1988)
53
+ is adopted (fc in MPa). Default value = None.
54
+ name (str): A name for the constitutive law.
55
+
56
+ Raises:
57
+ ValueError: If E is less or equal to 0.
58
+
59
+ Note:
60
+ If positive values are input for fc, eps_c and eps_cu are input,
61
+ they will be assumed negative.
62
+ """
63
+ name = name if name is not None else 'PopovicsLaw'
64
+ super().__init__(name=name)
65
+ self._fc = -abs(fc)
66
+ self._eps_c = -abs(eps_c)
67
+ self._eps_cu = -abs(eps_cu)
68
+ if Ec is None:
69
+ # fc in MPa, relation of Mander et al. (1988)
70
+ Ec = 5000 * abs(fc) ** 0.5
71
+ if Ec <= 0:
72
+ raise ValueError('Elastic modulus must be a positive number.')
73
+ E_sec = self._fc / self._eps_c
74
+ self._n = Ec / (Ec - E_sec)
75
+
76
+ def get_stress(
77
+ self, eps: t.Union[float, ArrayLike]
78
+ ) -> t.Union[float, ArrayLike]:
79
+ """Return the stress given the strain."""
80
+ eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
81
+ # Preprocess eps array in order
82
+ eps = self.preprocess_strains_with_limits(eps=eps)
83
+ # Compute stress
84
+ # Compression branch
85
+ eta = eps / self._eps_c
86
+
87
+ sig = self._fc * eta * self._n / (self._n - 1 + eta**self._n)
88
+
89
+ # Elsewhere stress is 0.0
90
+ if np.isscalar(eps):
91
+ if eps < self._eps_cu or eps > 0:
92
+ return 0.0
93
+ else:
94
+ sig[eps < self._eps_cu] = 0.0
95
+ sig[eps > 0] = 0.0
96
+
97
+ return sig
98
+
99
+ def get_tangent(
100
+ self, eps: t.Union[float, ArrayLike]
101
+ ) -> t.Union[float, ArrayLike]:
102
+ """Return the tangent given strain."""
103
+ eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
104
+ # Preprocess eps array in order
105
+ eps = self.preprocess_strains_with_limits(eps=eps)
106
+ # Compression branch
107
+ eta = eps / self._eps_c
108
+
109
+ tangent = (
110
+ (1 - eta**self._n)
111
+ / (self._n - 1 + eta**self._n) ** 2
112
+ * self._n
113
+ * (self._n - 1)
114
+ * self._fc
115
+ / self._eps_c
116
+ )
117
+ # Elsewhere tangent is zero
118
+ if np.isscalar(eps):
119
+ if eps < self._eps_cu or eps > 0:
120
+ return 0
121
+ else:
122
+ tangent[eps < self._eps_cu] = 0.0
123
+ tangent[eps > 0] = 0.0
124
+
125
+ return tangent
126
+
127
+ def get_ultimate_strain(
128
+ self, yielding: bool = False
129
+ ) -> t.Tuple[float, float]:
130
+ """Return the ultimate strain (negative and positive)."""
131
+ if yielding:
132
+ return (self._eps_c, 100)
133
+ return (self._eps_cu, 100)