structuralcodes 0.0.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (50) hide show
  1. structuralcodes/__init__.py +17 -0
  2. structuralcodes/codes/__init__.py +79 -0
  3. structuralcodes/codes/ec2_2004/__init__.py +133 -0
  4. structuralcodes/codes/ec2_2004/_concrete_material_properties.py +239 -0
  5. structuralcodes/codes/ec2_2004/_reinforcement_material_properties.py +104 -0
  6. structuralcodes/codes/ec2_2004/_section_7_3_crack_control.py +941 -0
  7. structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py +257 -0
  8. structuralcodes/codes/ec2_2004/shear.py +506 -0
  9. structuralcodes/codes/ec2_2023/__init__.py +104 -0
  10. structuralcodes/codes/ec2_2023/_annexB_time_dependent.py +17 -0
  11. structuralcodes/codes/ec2_2023/_section5_materials.py +1160 -0
  12. structuralcodes/codes/ec2_2023/_section9_sls.py +325 -0
  13. structuralcodes/codes/mc2010/__init__.py +169 -0
  14. structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +704 -0
  15. structuralcodes/codes/mc2010/_concrete_interface_different_casting_times.py +104 -0
  16. structuralcodes/codes/mc2010/_concrete_material_properties.py +463 -0
  17. structuralcodes/codes/mc2010/_concrete_punching.py +543 -0
  18. structuralcodes/codes/mc2010/_concrete_shear.py +749 -0
  19. structuralcodes/codes/mc2010/_concrete_torsion.py +164 -0
  20. structuralcodes/codes/mc2010/_reinforcement_material_properties.py +105 -0
  21. structuralcodes/core/__init__.py +1 -0
  22. structuralcodes/core/_section_results.py +211 -0
  23. structuralcodes/core/base.py +260 -0
  24. structuralcodes/geometry/__init__.py +25 -0
  25. structuralcodes/geometry/_geometry.py +875 -0
  26. structuralcodes/geometry/_steel_sections.py +2155 -0
  27. structuralcodes/materials/__init__.py +9 -0
  28. structuralcodes/materials/concrete/__init__.py +82 -0
  29. structuralcodes/materials/concrete/_concrete.py +114 -0
  30. structuralcodes/materials/concrete/_concreteEC2_2004.py +477 -0
  31. structuralcodes/materials/concrete/_concreteEC2_2023.py +435 -0
  32. structuralcodes/materials/concrete/_concreteMC2010.py +494 -0
  33. structuralcodes/materials/constitutive_laws.py +979 -0
  34. structuralcodes/materials/reinforcement/__init__.py +84 -0
  35. structuralcodes/materials/reinforcement/_reinforcement.py +172 -0
  36. structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +103 -0
  37. structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +93 -0
  38. structuralcodes/materials/reinforcement/_reinforcementMC2010.py +98 -0
  39. structuralcodes/sections/__init__.py +23 -0
  40. structuralcodes/sections/_generic.py +1249 -0
  41. structuralcodes/sections/_reinforcement.py +115 -0
  42. structuralcodes/sections/section_integrators/__init__.py +14 -0
  43. structuralcodes/sections/section_integrators/_factory.py +41 -0
  44. structuralcodes/sections/section_integrators/_fiber_integrator.py +238 -0
  45. structuralcodes/sections/section_integrators/_marin_integration.py +47 -0
  46. structuralcodes/sections/section_integrators/_marin_integrator.py +222 -0
  47. structuralcodes/sections/section_integrators/_section_integrator.py +49 -0
  48. structuralcodes-0.0.1.dist-info/METADATA +40 -0
  49. structuralcodes-0.0.1.dist-info/RECORD +50 -0
  50. structuralcodes-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,979 @@
1
+ """Collection of some standard constitutive laws."""
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, Material
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(self, eps: ArrayLike) -> ArrayLike:
37
+ """Return stress given strain."""
38
+ eps = np.atleast_1d(np.asarray(eps))
39
+ return self._E * eps
40
+
41
+ def get_tangent(self, eps: ArrayLike) -> ArrayLike:
42
+ """Return the tangent."""
43
+ eps = np.atleast_1d(np.asarray(eps))
44
+ return np.ones_like(eps) * self._E
45
+
46
+ def __marin__(
47
+ self, strain: t.Tuple[float, float]
48
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
49
+ """Returns coefficients and strain limits for Marin integration in a
50
+ simply formatted way.
51
+
52
+ Arguments:
53
+ strain (float, float): Tuple defining the strain profile: eps =
54
+ strain[0] + strain[1]*y.
55
+
56
+ Example:
57
+ [(0, -0.002), (-0.002, -0.003)]
58
+ [(a0, a1, a2), (a0)]
59
+ """
60
+ strains = None
61
+ a0 = self._E * strain[0]
62
+ a1 = self._E * strain[1]
63
+ coeff = [(a0, a1)]
64
+ return strains, coeff
65
+
66
+ def get_ultimate_strain(self, **kwargs) -> t.Tuple[float, float]:
67
+ """Return the ultimate strain (positive and negative)."""
68
+ # There is no real strain limit, so set it to very large values
69
+ # unlesse specified by the user differently
70
+ del kwargs
71
+ return self._eps_su or (100, -100)
72
+
73
+ def set_ultimate_strain(
74
+ self, eps_su=t.Union[float, t.Tuple[float, float]]
75
+ ) -> None:
76
+ """Set ultimate strains for Elastic Material if needed.
77
+
78
+ Arguments:
79
+ eps_su (float or (float, float)): Defining ultimate strain if a
80
+ single value is provided the same is adopted for both positive
81
+ and negative strains.
82
+ """
83
+ if isinstance(eps_su, float):
84
+ self._eps_su = (abs(eps_su), -abs(eps_su))
85
+ elif isinstance(eps_su, tuple):
86
+ if len(eps_su) < 2:
87
+ raise ValueError(
88
+ 'Two values need to be provided when setting the tuple'
89
+ )
90
+ eps_su_p = eps_su[0]
91
+ eps_su_n = eps_su[1]
92
+ if eps_su_p < eps_su_n:
93
+ eps_su_p, eps_su_n = eps_su_n, eps_su_p
94
+ if eps_su_p < 0:
95
+ raise ValueError(
96
+ 'Positive ultimate strain should be non-negative'
97
+ )
98
+ if eps_su_n > 0:
99
+ raise ValueError(
100
+ 'Negative utimate strain should be non-positive'
101
+ )
102
+ self._eps_su = (eps_su_p, eps_su_n)
103
+ else:
104
+ raise ValueError(
105
+ 'set_ultimate_strain requires a single value or a tuple \
106
+ with two values'
107
+ )
108
+
109
+
110
+ class ElasticPlastic(ConstitutiveLaw):
111
+ """Class for elastic-plastic Constitutive Law."""
112
+
113
+ __materials__: t.Tuple[str] = (
114
+ 'steel',
115
+ 'rebars',
116
+ )
117
+
118
+ def __init__(
119
+ self,
120
+ E: float,
121
+ fy: float,
122
+ Eh: float = 0.0,
123
+ eps_su: t.Optional[float] = None,
124
+ name: t.Optional[str] = None,
125
+ ) -> None:
126
+ """Initialize an Elastic-Plastic Material.
127
+
128
+ Arguments:
129
+ E (float): The elastic modulus.
130
+ fy (float): The yield strength.
131
+
132
+ Keyword Arguments:
133
+ Eh (float): The hardening modulus.
134
+ eps_su (float): The ultimate strain.
135
+ name (str): A descriptive name for the constitutive law.
136
+ """
137
+ name = name if name is not None else 'ElasticPlasticLaw'
138
+ super().__init__(name=name)
139
+ if E > 0:
140
+ self._E = E
141
+ else:
142
+ raise ValueError('Elastic modulus E must be greater than zero')
143
+ self._fy = fy
144
+ self._Eh = Eh
145
+ self._eps_su = eps_su
146
+ self._eps_sy = fy / E
147
+
148
+ def get_stress(self, eps: ArrayLike) -> ArrayLike:
149
+ """Return the stress given strain."""
150
+ eps = np.atleast_1d(np.asarray(eps))
151
+ # Preprocess eps array in order
152
+ eps = self.preprocess_strains_with_limits(eps=eps)
153
+ # Compute stress
154
+ sig = self._E * eps
155
+ delta_sig = self._fy * (1 - self._Eh / self._E)
156
+ sig[sig < -self._fy] = eps[sig < -self._fy] * self._Eh - delta_sig
157
+ sig[sig > self._fy] = eps[sig > self._fy] * self._Eh + delta_sig
158
+ if self._eps_su is not None:
159
+ sig[eps > self._eps_su] = 0
160
+ sig[eps < -self._eps_su] = 0 # pylint: disable=E1130
161
+ return sig
162
+
163
+ def get_tangent(self, eps: ArrayLike) -> ArrayLike:
164
+ """Return the tangent for given strain."""
165
+ eps = np.atleast_1d(np.asarray(eps))
166
+ tangent = np.ones_like(eps) * self._E
167
+ tangent[eps > self._eps_sy] = self._Eh
168
+ tangent[eps < -self._eps_sy] = self._Eh
169
+
170
+ return tangent
171
+
172
+ def __marin__(
173
+ self, strain: t.Tuple[float, float]
174
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
175
+ """Returns coefficients and strain limits for Marin integration in a
176
+ simply formatted way.
177
+
178
+ Arguments:
179
+ strain (float, float): Tuple defining the strain profile: eps =
180
+ strain[0] + strain[1]*y.
181
+
182
+ Example:
183
+ [(0, -0.002), (-0.002, -0.003)]
184
+ [(a0, a1, a2), (a0)]
185
+ """
186
+ strains = []
187
+ coeff = []
188
+ eps_sy_p, eps_sy_n = self.get_ultimate_strain(yielding=True)
189
+ eps_su_p, eps_su_n = self.get_ultimate_strain()
190
+ if strain[1] == 0:
191
+ # Uniform strain equal to strain[0]
192
+ # Understand in which branch are we
193
+ strain[0] = self.preprocess_strains_with_limits(strain[0])[0]
194
+ if strain[0] > eps_sy_p and strain[0] <= eps_su_p:
195
+ # We are in the Hardening part positive
196
+ strains = None
197
+ a0 = self._Eh * strain[0] + self._fy * (1 - self._Eh / self._E)
198
+ a1 = self._Eh * strain[1]
199
+ coeff.append((a0, a1))
200
+ elif strain[0] < eps_sy_n and strain[0] >= eps_su_n:
201
+ # We are in the Hardening part negative
202
+ strains = None
203
+ a0 = self._Eh * strain[0] - self._fy * (1 - self._Eh / self._E)
204
+ a1 = self._Eh * strain[1]
205
+ coeff.append((a0, a1))
206
+ elif abs(strain[0]) <= self._eps_sy:
207
+ # We are in the elastic part
208
+ strains = None
209
+ a0 = self._E * strain[0]
210
+ a1 = self._E * strain[1]
211
+ coeff.append((a0, a1))
212
+ else:
213
+ strains = None
214
+ coeff.append((0.0,))
215
+ else:
216
+ # Hardening part negative
217
+ strains.append((eps_su_n, eps_sy_n))
218
+ a0 = self._Eh * strain[0] - self._fy * (1 - self._Eh / self._E)
219
+ a1 = self._Eh * strain[1]
220
+ coeff.append((a0, a1))
221
+ # Elastic part
222
+ strains.append((eps_sy_n, eps_sy_p))
223
+ a0 = self._E * strain[0]
224
+ a1 = self._E * strain[1]
225
+ coeff.append((a0, a1))
226
+ # Hardening part positive
227
+ strains.append((eps_sy_p, eps_su_p))
228
+ a0 = self._Eh * strain[0] + self._fy * (1 - self._Eh / self._E)
229
+ a1 = self._Eh * strain[1]
230
+ coeff.append((a0, a1))
231
+ return strains, coeff
232
+
233
+ def get_ultimate_strain(
234
+ self, yielding: bool = False
235
+ ) -> t.Tuple[float, float]:
236
+ """Return the ultimate strain (positive and negative)."""
237
+ if yielding:
238
+ return (self._eps_sy, -self._eps_sy)
239
+ # If not specified eps
240
+ if self._eps_su is None:
241
+ return (self._eps_sy * 2, -self._eps_sy * 2)
242
+ return (self._eps_su, -self._eps_su)
243
+
244
+
245
+ class ParabolaRectangle(ConstitutiveLaw):
246
+ """Class for parabola rectangle constitutive law.
247
+
248
+ The stresses and strains are assumed negative in compression and positive
249
+ in tension.
250
+ """
251
+
252
+ __materials__: t.Tuple[str] = ('concrete',)
253
+
254
+ def __init__(
255
+ self,
256
+ fc: float,
257
+ eps_0: float = -0.002,
258
+ eps_u: float = -0.0035,
259
+ n: float = 2.0,
260
+ name: t.Optional[str] = None,
261
+ ) -> None:
262
+ """Initialize a Parabola-Rectangle Material.
263
+
264
+ Arguments:
265
+ fc (float): The strength of concrete in compression.
266
+
267
+ Keyword Arguments:
268
+ eps_0 (float): Peak strain of concrete in compression. Default
269
+ value = -0.002.
270
+ eps_u (float): Ultimate strain of concrete in compression. Default
271
+ value = -0.0035.
272
+ n (float): Exponent for the pre-peak branch. Default value = 2.
273
+ name (str): A name for the constitutive law.
274
+ """
275
+ name = name if name is not None else 'ParabolaRectangleLaw'
276
+ super().__init__(name=name)
277
+ self._fc = -abs(fc)
278
+ self._eps_0 = -abs(eps_0)
279
+ self._eps_u = -abs(eps_u)
280
+ self._n = n
281
+
282
+ def get_stress(self, eps: ArrayLike) -> ArrayLike:
283
+ """Return the stress given strain."""
284
+ eps = np.atleast_1d(np.asarray(eps))
285
+ # Preprocess eps array in order
286
+ eps = self.preprocess_strains_with_limits(eps=eps)
287
+ # Compute stress
288
+ sig = np.zeros_like(eps)
289
+ # Parabolic branch
290
+ sig[(eps <= 0) & (eps >= self._eps_0)] = self._fc * (
291
+ 1
292
+ - (1 - (eps[(eps <= 0) & (eps >= self._eps_0)] / self._eps_0))
293
+ ** self._n
294
+ )
295
+ # Rectangle branch
296
+ sig[eps < self._eps_0] = self._fc
297
+ # Zero elsewhere
298
+ sig[eps < self._eps_u] = 0
299
+ sig[eps > 0] = 0
300
+ return sig
301
+
302
+ def get_tangent(self, eps: ArrayLike) -> ArrayLike:
303
+ """Return the tangent given strain."""
304
+ eps = np.atleast_1d(np.asarray(eps))
305
+ # parabolic branch
306
+ tangent = np.zeros_like(eps)
307
+ tangent[(eps <= 0) & (eps >= self._eps_0)] = (
308
+ self._n
309
+ * self._fc
310
+ / self._eps_0
311
+ * (1 - (eps[(eps <= 0) & (eps >= self._eps_0)] / self._eps_0))
312
+ ** (self._n - 1)
313
+ )
314
+ # Elsewhere tangent is zero
315
+ tangent[eps < self._eps_0] = 0.0
316
+ tangent[eps > 0] = 0.0
317
+ return tangent
318
+
319
+ def __marin__(
320
+ self, strain: t.Tuple[float, float]
321
+ ) -> t.Tuple[t.List[float], t.List[float]]:
322
+ """Returns coefficients and strain limits for Marin integration in a
323
+ simply formatted way.
324
+
325
+ Arguments:
326
+ strain (float, float): Tuple defining the strain profile: eps =
327
+ strain[0] + strain[1]*y.
328
+
329
+ Example:
330
+ [(0, -0.002), (-0.002, -0.003)]
331
+ [(a0, a1, a2), (a0)]
332
+ """
333
+ if self._n != 2:
334
+ # The constitutive law is not writtable as a polynomial,
335
+ # Call the generic distretizing method
336
+ return super().__marin__(strain=strain)
337
+
338
+ strains = []
339
+ coeff = []
340
+ if strain[1] == 0:
341
+ # Uniform strain equal to strain[0]
342
+ # understand in which branch are we
343
+ strain[0] = self.preprocess_strains_with_limits(strain[0])[0]
344
+ if strain[0] > 0:
345
+ # We are in tensile branch
346
+ strains = None
347
+ coeff.append((0.0,))
348
+ elif strain[0] > self._eps_0:
349
+ # We are in the parabolic branch
350
+ strains = None
351
+ a0 = (
352
+ 2
353
+ * self._fc
354
+ * strain[0]
355
+ / self._eps_0
356
+ * (1 - 0.5 * (strain[0] / self._eps_0))
357
+ )
358
+ a1 = (
359
+ 2
360
+ * self._fc
361
+ / self._eps_0
362
+ * strain[1]
363
+ * (1 - strain[0] / self._eps_0)
364
+ )
365
+ coeff.append((a0, a1, 0.0))
366
+ elif strain[0] >= self._eps_u:
367
+ # We are in the constant branch
368
+ strains = None
369
+ coeff.append((self._fc,))
370
+ else:
371
+ # We are in a branch of non-resisting concrete
372
+ # Too much compression
373
+ strains = None
374
+ coeff.append((0.0,))
375
+ else:
376
+ # Parabolic part
377
+ strains.append((self._eps_0, 0))
378
+ a0 = (
379
+ 2
380
+ * self._fc
381
+ * strain[0]
382
+ / self._eps_0
383
+ * (1 - 0.5 * (strain[0] / self._eps_0))
384
+ )
385
+ a1 = (
386
+ 2
387
+ * self._fc
388
+ / self._eps_0
389
+ * strain[1]
390
+ * (1 - strain[0] / self._eps_0)
391
+ )
392
+ a2 = -self._fc * strain[1] ** 2 / self._eps_0**2
393
+ coeff.append((a0, a1, a2))
394
+ # Constant part
395
+ strains.append((self._eps_u, self._eps_0))
396
+ coeff.append((self._fc,))
397
+ return strains, coeff
398
+
399
+ def get_ultimate_strain(
400
+ self, yielding: bool = False
401
+ ) -> t.Tuple[float, float]:
402
+ """Return the ultimate strain (positive and negative)."""
403
+ if yielding:
404
+ return (100, self._eps_0)
405
+ return (100, self._eps_u)
406
+
407
+
408
+ class BilinearCompression(ConstitutiveLaw):
409
+ """Class for Bilinear Elastic-PerfectlyPlastic Constitutive Law for
410
+ Concrete (only compression behavior).
411
+ """
412
+
413
+ __materials__: t.Tuple[str] = ('concrete',)
414
+
415
+ def __init__(
416
+ self,
417
+ fc: float,
418
+ eps_c: float,
419
+ eps_cu: t.Optional[float] = None,
420
+ name: t.Optional[str] = None,
421
+ ) -> None:
422
+ """Initialize a BilinearCompression Material.
423
+
424
+ Arguments:
425
+ fc (float): Compressive strength (negative number).
426
+ eps_c (float): Strain at compressive strength (pure number).
427
+
428
+ Keyword Arguments:
429
+ eps_cu (float): Ultimate strain (pure number).
430
+ name (str): A descriptive name for the constitutive law.
431
+ """
432
+ name = name if name is not None else 'BilinearCompressionLaw'
433
+ super().__init__(name=name)
434
+ self._fc = -abs(fc)
435
+ self._eps_c = -abs(eps_c)
436
+ self._eps_cu = -abs(eps_cu)
437
+ self._E = self._fc / self._eps_c
438
+
439
+ def get_stress(self, eps: ArrayLike) -> ArrayLike:
440
+ """Return the stress given strain."""
441
+ eps = np.atleast_1d(np.asarray(eps))
442
+ # Preprocess eps array in order
443
+ eps = self.preprocess_strains_with_limits(eps=eps)
444
+ # Compute stress
445
+ sig = self._E * eps
446
+ sig[sig < self._fc] = self._fc
447
+ sig[eps > 0] = 0
448
+ sig[eps < self._eps_cu] = 0
449
+ return sig
450
+
451
+ def get_tangent(self, eps: ArrayLike) -> ArrayLike:
452
+ """Return the tangent for given strain."""
453
+ eps = np.atleast_1d(np.asarray(eps))
454
+ tangent = np.ones_like(eps) * self._E
455
+ tangent[eps < self._eps_c] = 0.0
456
+
457
+ return tangent
458
+
459
+ def __marin__(
460
+ self, strain: t.Tuple[float, float]
461
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
462
+ """Returns coefficients and strain limits for Marin integration in a
463
+ simply formatted way.
464
+
465
+ Arguments:
466
+ strain (float, float): Tuple defining the strain profile: eps =
467
+ strain[0] + strain[1]*y.
468
+
469
+ Example:
470
+ [(0, -0.002), (-0.002, -0.003)]
471
+ [(a0, a1, a2), (a0)]
472
+ """
473
+ strains = []
474
+ coeff = []
475
+ if strain[1] == 0:
476
+ # Uniform strain equal to strain[0]
477
+ # understand in which branch we are
478
+ strain[0] = self.preprocess_strains_with_limits(strain[0])[0]
479
+ if strain[0] > 0:
480
+ # We are in tensile branch
481
+ strains = None
482
+ coeff.append((0.0,))
483
+ elif strain[0] > self._eps_0:
484
+ # We are in the linear branch
485
+ strains = None
486
+ a0 = self._E * strain[0]
487
+ a1 = self._E * strain[1]
488
+ coeff.append((a0, a1))
489
+ elif strain[0] >= self._eps_cu:
490
+ # We are in the constant branch
491
+ strains = None
492
+ coeff.append((self._fc,))
493
+ else:
494
+ # We are in a branch of non-resisting concrete
495
+ # Too much compression
496
+ strains = None
497
+ coeff.append((0.0,))
498
+ else:
499
+ # linear part
500
+ strains.append((self._eps_c, 0))
501
+ a0 = self._E * strain[0]
502
+ a1 = self._E * strain[1]
503
+ coeff.append((a0, a1))
504
+ # Constant part
505
+ strains.append((self._eps_cu, self._eps_c))
506
+ coeff.append((self._fc,))
507
+ return strains, coeff
508
+
509
+ def get_ultimate_strain(
510
+ self, yielding: bool = False
511
+ ) -> t.Tuple[float, float]:
512
+ """Return the ultimate strain (positive and negative)."""
513
+ if yielding:
514
+ return (100, self._eps_c)
515
+ return (100, self._eps_cu)
516
+
517
+
518
+ class Sargin(ConstitutiveLaw):
519
+ """Class for Sargin constitutive law.
520
+
521
+ The stresses and strains are assumed negative in compression and positive
522
+ in tension.
523
+
524
+ References:
525
+ Sargin, M. (1971), "Stress-strain relationship for concrete and the
526
+ analysis of structural concrete section, Study No. 4,
527
+ Solid Mechanics Division, University of Waterloo, Ontario, Canada
528
+ """
529
+
530
+ __materials__: t.Tuple[str] = ('concrete',)
531
+
532
+ def __init__(
533
+ self,
534
+ fc: float,
535
+ eps_c1: float = -0.0023,
536
+ eps_cu1: float = -0.0035,
537
+ k: float = 2.04,
538
+ name: t.Optional[str] = None,
539
+ ) -> None:
540
+ """Initialize a Sargin Material.
541
+
542
+ Arguments:
543
+ fc (float): The strength of concrete in compression
544
+
545
+ Keyword Arguments:
546
+ eps_c1 (float): Peak strain of concrete in compression. Default
547
+ value = -0.0023.
548
+ eps_u (float): Ultimate strain of concrete in compression. Default
549
+ value = -0.0035.
550
+ k (float): Plasticity number. Default value = 2.04.
551
+ name (str): A name for the constitutive law.
552
+
553
+ Raises:
554
+ ValueError: If k is less or equal to 0.
555
+
556
+ Note:
557
+ If positive values are input for fc, eps_c1 and eps_cu1 are input,
558
+ they will be assumed negative.
559
+ """
560
+ name = name if name is not None else 'SarginLaw'
561
+ super().__init__(name=name)
562
+ self._fc = -abs(fc)
563
+ self._eps_c1 = -abs(eps_c1)
564
+ self._eps_cu1 = -abs(eps_cu1)
565
+ self._k = k
566
+
567
+ def get_stress(self, eps: ArrayLike) -> ArrayLike:
568
+ """Return the stress given the strain."""
569
+ eps = np.atleast_1d(np.asarray(eps))
570
+ # Preprocess eps array in order
571
+ eps = self.preprocess_strains_with_limits(eps=eps)
572
+ # Compute stress
573
+ # Polynomial branch
574
+ eta = eps / self._eps_c1
575
+
576
+ sig = self._fc * (self._k * eta - eta**2) / (1 + (self._k - 2) * eta)
577
+
578
+ # Elsewhere stress is 0.0
579
+ sig[eps < self._eps_cu1] = 0.0
580
+ sig[eps > 0] = 0.0
581
+
582
+ return sig
583
+
584
+ def get_tangent(self, eps: ArrayLike) -> ArrayLike:
585
+ """Return the tangent given strain."""
586
+ eps = np.atleast_1d(np.asarray(eps))
587
+ # polynomial branch
588
+ eta = eps / self._eps_c1
589
+
590
+ tangent = (
591
+ self._fc
592
+ / self._eps_c1
593
+ * ((2 - self._k) * eta**2 - 2 * eta + self._k)
594
+ / (1 + (self._k - 2) * eta) ** 2
595
+ )
596
+ # Elsewhere tangent is zero
597
+ tangent[eps < self._eps_cu1] = 0.0
598
+ tangent[eps > 0] = 0.0
599
+
600
+ return tangent
601
+
602
+ def get_ultimate_strain(
603
+ self, yielding: bool = False
604
+ ) -> t.Tuple[float, float]:
605
+ """Return the ultimate strain (positive and negative)."""
606
+ if yielding:
607
+ return (100, self._eps_c1)
608
+ return (100, self._eps_cu1)
609
+
610
+
611
+ class Popovics(ConstitutiveLaw):
612
+ """Class for Popovics-Mander constitutive law.
613
+
614
+ The stresses and strains are assumed negative in compression and positive
615
+ in tension.
616
+
617
+ If the relation Ec = 5000 * sqrt(fc) is used for elastic modulus, the
618
+ constitutive law is identical to the one proposed by Mander et al. (1988).
619
+
620
+ References:
621
+ Popovics, S., 1973, “A Numerical Approach to the Complete Stress-Strain
622
+ Curve of Concrete”, Cement and Concrete Research, 3(4), 583-599.
623
+
624
+ Mander, J.B., Priestley, M.J.N., Park, R., 1988, "Theoretical Stress-Strain
625
+ Model for Confined Concrete", Journal of Structural Engineering, 114(8),
626
+ 1804-1826.
627
+ """
628
+
629
+ __materials__: t.Tuple[str] = ('concrete',)
630
+
631
+ def __init__(
632
+ self,
633
+ fc: float,
634
+ eps_c: float = -0.002,
635
+ eps_cu: float = -0.0035,
636
+ Ec: t.Optional[float] = None,
637
+ name: t.Optional[str] = None,
638
+ ) -> None:
639
+ """Initialize a Popovics Material.
640
+
641
+ Arguments:
642
+ fc (float): the strength of concrete in compression
643
+
644
+ Keyword Arguments:
645
+ eps_c (float): Peak strain of concrete in compression. Default
646
+ value = -0.002.
647
+ eps_cu (float): Ultimate strain of concrete in compression. Default
648
+ value = -0.0035.
649
+ E (optional float): Elastic modulus of concrete. If None, the
650
+ equation Ec = 5000 * fc**0.5 proposed by Mander et al. (1988)
651
+ is adopted (fc in MPa). Default value = None.
652
+ name (str): A name for the constitutive law.
653
+
654
+ Raises:
655
+ ValueError: If E is less or equal to 0.
656
+
657
+ Note:
658
+ If positive values are input for fc, eps_c and eps_cu are input,
659
+ they will be assumed negative.
660
+ """
661
+ name = name if name is not None else 'PopovicsLaw'
662
+ super().__init__(name=name)
663
+ self._fc = -abs(fc)
664
+ self._eps_c = -abs(eps_c)
665
+ self._eps_cu = -abs(eps_cu)
666
+ if Ec is None:
667
+ # fc in MPa, relation of Mander et al. (1988)
668
+ Ec = 5000 * abs(fc) ** 0.5
669
+ if Ec <= 0:
670
+ raise ValueError('Elastic modulus must be a positive number.')
671
+ E_sec = self._fc / self._eps_c
672
+ self._n = Ec / (Ec - E_sec)
673
+
674
+ def get_stress(self, eps: ArrayLike) -> ArrayLike:
675
+ """Return the stress given the strain."""
676
+ eps = np.atleast_1d(np.asarray(eps))
677
+ # Preprocess eps array in order
678
+ eps = self.preprocess_strains_with_limits(eps=eps)
679
+ # Compute stress
680
+ # Compression branch
681
+ eta = eps / self._eps_c
682
+
683
+ sig = self._fc * eta * self._n / (self._n - 1 + eta**self._n)
684
+
685
+ # Elsewhere stress is 0.0
686
+ sig[eps < self._eps_cu] = 0.0
687
+ sig[eps > 0] = 0.0
688
+
689
+ return sig
690
+
691
+ def get_tangent(self, eps: ArrayLike) -> ArrayLike:
692
+ """Return the tangent given strain."""
693
+ eps = np.atleast_1d(np.asarray(eps))
694
+ # Preprocess eps array in order
695
+ eps = self.preprocess_strains_with_limits(eps=eps)
696
+ # Compression branch
697
+ eta = eps / self._eps_c
698
+
699
+ tangent = (
700
+ (1 - eta**self._n)
701
+ / (self._n - 1 + eta**self._n) ** 2
702
+ * self._n
703
+ * (self._n - 1)
704
+ * self._fc
705
+ / self._eps_c
706
+ )
707
+ # Elsewhere tangent is zero
708
+ tangent[eps < self._eps_cu] = 0.0
709
+ tangent[eps > 0] = 0.0
710
+
711
+ return tangent
712
+
713
+ def get_ultimate_strain(
714
+ self, yielding: bool = False
715
+ ) -> t.Tuple[float, float]:
716
+ """Return the ultimate strain (positive and negative)."""
717
+ if yielding:
718
+ return (100, self._eps_c)
719
+ return (100, self._eps_cu)
720
+
721
+
722
+ class UserDefined(ConstitutiveLaw):
723
+ """Class for a user defined constitutive law.
724
+
725
+ The curve is defined with positive and optionally negative values. After
726
+ the last value, the stress can go to zero to simulate failure (default), or
727
+ be maintained constante, or the last tanget or secant values may be
728
+ maintained indefinetely. The flag parameter controls this behavior.
729
+ """
730
+
731
+ __materials__: t.Tuple[str] = ('concrete', 'steel', 'rebars')
732
+
733
+ def __init__(
734
+ self,
735
+ x: ArrayLike,
736
+ y: ArrayLike,
737
+ name: t.Optional[str] = None,
738
+ flag: int = 0,
739
+ ) -> None:
740
+ """Initialize a UserDefined constitutive law.
741
+
742
+ Arguments:
743
+ x (ArrayLike): Data for strains.
744
+ y (ArrayLike): Data for stresses. Must be of same length as x.
745
+
746
+ Keyword Arguments:
747
+ name (Optional, str): A name for the constitutive law.
748
+ flag (Optional): A flag specifying the behavior after the last
749
+ point. Admissible values: 0 (default): stress drops to zero
750
+ after ultimate strain, 1: stress is mantained constant, 2:
751
+ last tangent is used, 3: last secant is used.
752
+ """
753
+ name = name if name is not None else 'UserDefinedLaw'
754
+ super().__init__(name=name)
755
+ x = np.atleast_1d(np.asarray(x))
756
+ y = np.atleast_1d(np.asarray(y))
757
+ if len(x) != len(y):
758
+ raise ValueError('The two arrays should have the same length')
759
+ if not np.any(x < 0):
760
+ # User provided only positive part, reflect in negative
761
+ self._x = np.concatenate((-np.flip(x)[:-1], x))
762
+ self._y = np.concatenate((-np.flip(y)[:-1], y))
763
+ else:
764
+ # User gave both positive and negative parts
765
+ self._x = x
766
+ self._y = y
767
+ # Define what happens after last strain
768
+ if flag not in (0, 1, 2, 3):
769
+ raise ValueError('Flag can assume values 0, 1, 2 or 3.')
770
+ self._ultimate_strain_p = self._x[-1]
771
+ self._ultimate_strain_n = self._x[0]
772
+ if flag in (1, 2, 3):
773
+ x = np.insert(self._x, 0, self._x[0] * 100)
774
+ x = np.append(x, self._x[-1] * 100)
775
+ if flag == 1:
776
+ y = np.insert(self._y, 0, self._y[0])
777
+ y = np.append(y, self._y[-1])
778
+ elif flag == 2:
779
+ tangent_p = (self._y[-1] - self._y[-2]) / (
780
+ self._x[-1] - self._x[-2]
781
+ )
782
+ tangent_n = (self._y[1] - self._y[0]) / (
783
+ self._x[1] - self._x[0]
784
+ )
785
+ y = np.insert(
786
+ self._y, 0, (x[0] - x[1]) * tangent_n + self._y[0]
787
+ )
788
+ y = np.append(y, (x[-1] - x[-2]) * tangent_p + self._y[-1])
789
+ elif flag == 3:
790
+ secant_p = self._y[-1] / self._x[-1]
791
+ secant_n = self._y[0] / self._x[0]
792
+ y = np.insert(
793
+ self._y, 0, (x[0] - x[1]) * secant_n + self._y[0]
794
+ )
795
+ y = np.append(y, (x[-1] - x[-2]) * secant_p + self._y[-1])
796
+ self._x = x
797
+ self._y = y
798
+
799
+ # Compute slope of each segment
800
+ self._slopes = np.diff(self._y) / np.diff(self._x)
801
+
802
+ def get_stress(self, eps: ArrayLike) -> ArrayLike:
803
+ """Return the stress given strain."""
804
+ eps = np.atleast_1d(np.asarray(eps))
805
+ # Preprocess eps array in order
806
+ eps = self.preprocess_strains_with_limits(eps=eps)
807
+ # Compute stress
808
+ return np.interp(eps, self._x, self._y, left=0, right=0)
809
+
810
+ def get_tangent(self, eps: ArrayLike) -> ArrayLike:
811
+ """Return the tangent given strain."""
812
+ eps = np.atleast_1d(np.array(eps))
813
+
814
+ # Find the segment index for each x value
815
+ indices = np.searchsorted(self._x, eps) - 1
816
+
817
+ # Check that indices are within vlaid range
818
+ indices = np.clip(indices, 0, len(self._slopes) - 1)
819
+
820
+ # Get the corresponding slopes
821
+ tangent = self._slopes[indices]
822
+
823
+ # Elsewhere tangent is zero
824
+ tangent[eps < self._x[0]] = 0.0
825
+ tangent[eps > self._x[-1]] = 0.0
826
+
827
+ return tangent
828
+
829
+ def __marin__(
830
+ self, strain: t.Tuple[float, float]
831
+ ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]:
832
+ """Returns coefficients and strain limits for Marin integration in a
833
+ simply formatted way.
834
+
835
+ Arguments:
836
+ strain (float, float): Tuple defining the strain profile: eps =
837
+ strain[0] + strain[1]*y.
838
+
839
+ Example:
840
+ [(0, -0.002), (-0.002, -0.003)]
841
+ [(a0, a1, a2), (a0)]
842
+ """
843
+ strains = []
844
+ coeff = []
845
+ if strain[1] == 0:
846
+ # Uniform strain equal to strain[0]
847
+ # understand in which branch are we
848
+ strain[0] = self.preprocess_strains_with_limits(strain[0])[0]
849
+ found = False
850
+ for i in range(len(self._x) - 1):
851
+ if self._x[i] <= strain[0] and self._x[i + 1] >= strain[0]:
852
+ strains = None
853
+ stiffness = (self._y[i + 1] - self._y[i]) / (
854
+ self._x[i + 1] - self._x[i]
855
+ )
856
+ a0 = stiffness * (strain[0] - self._x[i]) + self._y[i]
857
+ a1 = stiffness * strain[1]
858
+ coeff.append((a0, a1))
859
+ found = True
860
+ break
861
+ if not found:
862
+ strains = None
863
+ coeff.append((0.0,))
864
+ else:
865
+ for i in range(len(self._x) - 1):
866
+ # For each branch of the linear piecewise function
867
+ stiffness = (self._y[i + 1] - self._y[i]) / (
868
+ self._x[i + 1] - self._x[i]
869
+ )
870
+ strains.append((self._x[i], self._x[i + 1]))
871
+ a0 = stiffness * (strain[0] - self._x[i]) + self._y[i]
872
+ a1 = stiffness * strain[1]
873
+ coeff.append((a0, a1))
874
+
875
+ return strains, coeff
876
+
877
+ def get_ultimate_strain(self, **kwargs) -> t.Tuple[float, float]:
878
+ """Return the ultimate strain (positive and negative)."""
879
+ del kwargs
880
+ return (self._ultimate_strain_p, self._ultimate_strain_n)
881
+
882
+ def set_ultimate_strain(
883
+ self, eps_su=t.Union[float, t.Tuple[float, float]]
884
+ ) -> None:
885
+ """Set ultimate strains for Elastic Material if needed.
886
+
887
+ Arguments:
888
+ eps_su (float or (float, float)): Defining ultimate strain if a
889
+ single value is provided the same is adopted for both positive
890
+ and negative strains.
891
+ """
892
+ if isinstance(eps_su, float):
893
+ self._ultimate_strain_p = abs(eps_su)
894
+ self._ultimate_strain_n = -abs(eps_su)
895
+ elif isinstance(eps_su, tuple):
896
+ if len(eps_su) < 2:
897
+ raise ValueError(
898
+ 'Two values need to be provided when setting the tuple'
899
+ )
900
+ eps_su_p = eps_su[0]
901
+ eps_su_n = eps_su[1]
902
+ if eps_su_p < eps_su_n:
903
+ eps_su_p, eps_su_n = eps_su_n, eps_su_p
904
+ if eps_su_p < 0:
905
+ raise ValueError(
906
+ 'Positive ultimate strain should be non-negative'
907
+ )
908
+ if eps_su_n > 0:
909
+ raise ValueError(
910
+ 'Negative utimate strain should be non-positive'
911
+ )
912
+ self._ultimate_strain_p = eps_su_p
913
+ self._ultimate_strain_n = eps_su_n
914
+ else:
915
+ raise ValueError(
916
+ 'set_ultimate_strain requires a single value or a tuple \
917
+ with two values'
918
+ )
919
+
920
+
921
+ CONSTITUTIVE_LAWS: t.Dict[str, ConstitutiveLaw] = {
922
+ 'elastic': Elastic,
923
+ 'elasticplastic': ElasticPlastic,
924
+ 'elasticperfectlyplastic': ElasticPlastic,
925
+ 'bilinearcompression': BilinearCompression,
926
+ 'parabolarectangle': ParabolaRectangle,
927
+ 'popovics': Popovics,
928
+ 'sargin': Sargin,
929
+ }
930
+
931
+
932
+ def get_constitutive_laws_list() -> t.List[str]:
933
+ """Returns a list with valid keywords for constitutive law factory."""
934
+ return list(CONSTITUTIVE_LAWS.keys())
935
+
936
+
937
+ def create_constitutive_law(
938
+ constitutive_law_name: str, material: Material
939
+ ) -> ConstitutiveLaw:
940
+ """A factory function to create the constitutive law.
941
+
942
+ Arguments:
943
+ constitutive_law_name (str): A string defining a valid constitutive law
944
+ type. The available keys can be get with the method
945
+ `get_constitutive_laws_list`.
946
+ material (Material): The material containing the properties needed for
947
+ the definition of the constitutive law.
948
+
949
+ Note:
950
+ For working with this facotry function, the material class
951
+ implementations need to provide special dunder methods (__elastic__,
952
+ __parabolarectangle__, etc.) needed for the specific material that
953
+ return the kwargs needed to create the corresponding constitutive
954
+ law object. If the special dunder method is not found an exception
955
+ will be raised.
956
+
957
+ If the consitutive law selected is not available for the specific
958
+ material, an exception will be raised.
959
+ """
960
+ law = None
961
+ const_law = CONSTITUTIVE_LAWS.get(constitutive_law_name.lower())
962
+ if const_law is not None:
963
+ method_name = f'__{constitutive_law_name}__'
964
+ # check if the material object has the special method needed
965
+ if hasattr(material, method_name):
966
+ method = getattr(material, method_name)
967
+ if callable(method):
968
+ # get the kwargs from the special dunder method
969
+ kwargs = method()
970
+ # create the constitutive law
971
+ law = const_law(**kwargs)
972
+ else:
973
+ raise ValueError(
974
+ f'Constitutive law {constitutive_law_name} not available for'
975
+ f' material {material.__class__.__name__}'
976
+ )
977
+ else:
978
+ raise ValueError(f'Unknown constitutive law: {constitutive_law_name}')
979
+ return law