structuralcodes 0.1.1__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.

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