elasticipy 2.9.0__py3-none-any.whl → 3.0.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.
- Elasticipy/FourthOrderTensor.py +360 -44
- Elasticipy/Plasticity.py +105 -55
- Elasticipy/SecondOrderTensor.py +243 -54
- Elasticipy/StressStrainTensors.py +9 -5
- Elasticipy/ThermalExpansion.py +88 -17
- {elasticipy-2.9.0.dist-info → elasticipy-3.0.0.dist-info}/METADATA +2 -1
- elasticipy-3.0.0.dist-info/RECORD +15 -0
- {elasticipy-2.9.0.dist-info → elasticipy-3.0.0.dist-info}/WHEEL +1 -1
- elasticipy-2.9.0.dist-info/RECORD +0 -15
- {elasticipy-2.9.0.dist-info → elasticipy-3.0.0.dist-info}/LICENSE +0 -0
- {elasticipy-2.9.0.dist-info → elasticipy-3.0.0.dist-info}/top_level.txt +0 -0
Elasticipy/Plasticity.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from Elasticipy.StressStrainTensors import StrainTensor, StressTensor
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
class IsotropicHardening:
|
|
5
6
|
"""
|
|
6
7
|
Template class for isotropic hardening plasticity models
|
|
@@ -11,15 +12,19 @@ class IsotropicHardening:
|
|
|
11
12
|
|
|
12
13
|
Parameters
|
|
13
14
|
----------
|
|
14
|
-
criterion : str
|
|
15
|
+
criterion : str or PlasticityCriterion
|
|
15
16
|
Plasticity criterion to use. Can be 'von Mises', 'Tresca' or 'J2'. J2 is the same as von Mises.
|
|
16
17
|
"""
|
|
17
|
-
criterion
|
|
18
|
-
|
|
19
|
-
criterion
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
if isinstance(criterion, str):
|
|
19
|
+
criterion = criterion.lower()
|
|
20
|
+
if criterion in ('von mises', 'mises', 'vonmises', 'j2'):
|
|
21
|
+
self.criterion = VonMisesPlasticity
|
|
22
|
+
elif criterion == 'tresca':
|
|
23
|
+
self.criterion = TrescaPlasticity
|
|
24
|
+
else:
|
|
25
|
+
raise ValueError('The criterion can be "Tresca", "von Mises" or "J2".')
|
|
26
|
+
else:
|
|
27
|
+
self.criterion = criterion
|
|
23
28
|
self.plastic_strain = 0.0
|
|
24
29
|
|
|
25
30
|
def flow_stress(self, strain, **kwargs):
|
|
@@ -60,12 +65,6 @@ class IsotropicHardening:
|
|
|
60
65
|
def reset_strain(self):
|
|
61
66
|
self.plastic_strain = 0.0
|
|
62
67
|
|
|
63
|
-
def eq_stress(self, stress):
|
|
64
|
-
if self.criterion == 'j2':
|
|
65
|
-
return stress.vonMises()
|
|
66
|
-
else:
|
|
67
|
-
return stress.Tresca()
|
|
68
|
-
|
|
69
68
|
|
|
70
69
|
class JohnsonCook(IsotropicHardening):
|
|
71
70
|
def __init__(self, A, B, n, C=None, eps_dot_ref=1.0, m=None, T0=25, Tm=None, criterion='von Mises'):
|
|
@@ -93,7 +92,7 @@ class JohnsonCook(IsotropicHardening):
|
|
|
93
92
|
Reference temperature
|
|
94
93
|
Tm : float, optional
|
|
95
94
|
Melting temperature (at which the flow stress is zero)
|
|
96
|
-
criterion : str, optional
|
|
95
|
+
criterion : str or PlasticityCriterion, optional
|
|
97
96
|
Plasticity criterion to use. It can be 'von Mises' or 'Tresca'.
|
|
98
97
|
|
|
99
98
|
Notes
|
|
@@ -194,7 +193,7 @@ class JohnsonCook(IsotropicHardening):
|
|
|
194
193
|
apply_strain : apply strain to the JC model and updates its hardening value
|
|
195
194
|
"""
|
|
196
195
|
if isinstance(stress, StressTensor):
|
|
197
|
-
eq_stress = self.eq_stress(stress)
|
|
196
|
+
eq_stress = self.criterion.eq_stress(stress)
|
|
198
197
|
else:
|
|
199
198
|
eq_stress = stress
|
|
200
199
|
if T is None:
|
|
@@ -223,7 +222,7 @@ class JohnsonCook(IsotropicHardening):
|
|
|
223
222
|
self.apply_strain(strain_increment)
|
|
224
223
|
|
|
225
224
|
if isinstance(stress, StressTensor):
|
|
226
|
-
n =
|
|
225
|
+
n = self.criterion.normal(stress)
|
|
227
226
|
return n * strain_increment
|
|
228
227
|
else:
|
|
229
228
|
return strain_increment
|
|
@@ -235,48 +234,99 @@ class JohnsonCook(IsotropicHardening):
|
|
|
235
234
|
self.plastic_strain = 0.0
|
|
236
235
|
|
|
237
236
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
237
|
+
class PlasticityCriterion:
|
|
238
|
+
@staticmethod
|
|
239
|
+
def eq_stress(stress, **kwargs):
|
|
240
|
+
"""
|
|
241
|
+
Return the equivalent stress, with respect to the plasticity criterion.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
stress : StressTensor
|
|
246
|
+
Stress to compute the equivalent stress from
|
|
247
|
+
kwargs : dict
|
|
248
|
+
keyword arguments passed to the function
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
float or numpy.ndarray
|
|
252
|
+
"""
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
def normal(self, stress, **kwargs):
|
|
256
|
+
"""
|
|
257
|
+
Apply the normality rule
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
stress : StressTensor
|
|
262
|
+
Stress tensor to apply the normality rule
|
|
263
|
+
kwargs : dict
|
|
264
|
+
Keyword arguments passed to the function
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
StrainTensor
|
|
269
|
+
Normalized direction of plastic flow
|
|
270
|
+
"""
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
class VonMisesPlasticity(PlasticityCriterion):
|
|
274
|
+
@staticmethod
|
|
275
|
+
def eq_stress(stress, **kwargs):
|
|
276
|
+
return stress.vonMises()
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def normal(stress, **kwargs):
|
|
262
280
|
eq_stress = stress.vonMises()
|
|
263
|
-
dev_stress= stress.deviatoric_part()
|
|
281
|
+
dev_stress = stress.deviatoric_part()
|
|
264
282
|
gradient_tensor = dev_stress / eq_stress
|
|
265
|
-
return StrainTensor(3/2 * gradient_tensor.matrix)
|
|
266
|
-
|
|
283
|
+
return StrainTensor(3 / 2 * gradient_tensor.matrix)
|
|
284
|
+
|
|
285
|
+
class TrescaPlasticity(PlasticityCriterion):
|
|
286
|
+
@staticmethod
|
|
287
|
+
def eq_stress(stress, **kwargs):
|
|
288
|
+
return stress.Tresca()
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def normal(stress, **kwargs):
|
|
267
292
|
vals, dirs = stress.eig()
|
|
268
|
-
u1 = dirs[...,0]
|
|
269
|
-
u3 = dirs[...,2]
|
|
270
|
-
s1 = vals[...,0]
|
|
293
|
+
u1 = dirs[..., 0]
|
|
294
|
+
u3 = dirs[..., 2]
|
|
295
|
+
s1 = vals[..., 0]
|
|
271
296
|
s2 = vals[..., 1]
|
|
272
|
-
s3 = vals[...,2]
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
normal =
|
|
276
|
-
singular_points = np.logical_or(s2==s1, s2==s3)
|
|
277
|
-
normal[singular_points] =
|
|
278
|
-
normal[np.logical_and(s2==s1, s2==s3)] = 0.0
|
|
297
|
+
s3 = vals[..., 2]
|
|
298
|
+
a = np.einsum('...i,...j->...ij', u1, u1)
|
|
299
|
+
b = np.einsum('...i,...j->...ij', u3, u3)
|
|
300
|
+
normal = a - b
|
|
301
|
+
singular_points = np.logical_or(s2 == s1, s2 == s3)
|
|
302
|
+
normal[singular_points] = VonMisesPlasticity().normal(stress[singular_points]).matrix
|
|
303
|
+
normal[np.logical_and(s2 == s1, s2 == s3)] = 0.0
|
|
279
304
|
strain = StrainTensor(normal)
|
|
280
305
|
return strain / strain.eq_strain()
|
|
281
|
-
|
|
282
|
-
|
|
306
|
+
|
|
307
|
+
class DruckerPrager(PlasticityCriterion):
|
|
308
|
+
def __init__(self, alpha):
|
|
309
|
+
"""
|
|
310
|
+
Create a Drucker-Prager (DG) plasticity criterion.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
alpha : float
|
|
315
|
+
Pressure dependence parameters (see notes for details)
|
|
316
|
+
|
|
317
|
+
Notes
|
|
318
|
+
-----
|
|
319
|
+
The pressure-dependent DG plasticity criterion assumes that the equivalent stress is defined as:
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
self.alpha = alpha
|
|
324
|
+
|
|
325
|
+
def eq_stress(self, stress, **kwargs):
|
|
326
|
+
return (stress.J2**0.5 + self.alpha * stress.I1) / (1/3**0.5 + self.alpha)
|
|
327
|
+
|
|
328
|
+
def normal(self, stress, **kwargs):
|
|
329
|
+
J2 = stress.J2
|
|
330
|
+
gradient = stress.deviatoric_part() / (2 * J2**0.5) + self.alpha * StressTensor.eye(stress.shape)
|
|
331
|
+
strain = StrainTensor(gradient.matrix)
|
|
332
|
+
return strain / strain.eq_strain()
|
Elasticipy/SecondOrderTensor.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
import pandas as pd
|
|
3
5
|
from scipy.spatial.transform import Rotation
|
|
4
|
-
|
|
6
|
+
ALPHABET = 'abcdefghijklmnopqrstuv'
|
|
5
7
|
|
|
6
8
|
class _MatrixProxy:
|
|
7
9
|
def __init__(self, matrix):
|
|
@@ -36,6 +38,45 @@ def _transpose_matrix(matrix):
|
|
|
36
38
|
def _symmetric_part(matrix):
|
|
37
39
|
return 0.5 * (matrix + _transpose_matrix(matrix))
|
|
38
40
|
|
|
41
|
+
def _orientation_shape(g):
|
|
42
|
+
if is_orix_rotation(g):
|
|
43
|
+
return g.shape
|
|
44
|
+
else:
|
|
45
|
+
return (len(g),)
|
|
46
|
+
|
|
47
|
+
def _is_single_rotation(rotation):
|
|
48
|
+
if isinstance(rotation, Rotation):
|
|
49
|
+
return rotation.single
|
|
50
|
+
elif is_orix_rotation(rotation):
|
|
51
|
+
return rotation.size == 1
|
|
52
|
+
else:
|
|
53
|
+
raise TypeError('The input argument must be of class scipy.transform.Rotation or '
|
|
54
|
+
'orix.quaternion.rotation.Rotation')
|
|
55
|
+
|
|
56
|
+
_voigt_numbering = [[0, 0], [1, 1], [2, 2], [1, 2], [0, 2], [0, 1]]
|
|
57
|
+
|
|
58
|
+
def _unmap(array, mapping_convention):
|
|
59
|
+
array = np.asarray(array)
|
|
60
|
+
shape = array.shape
|
|
61
|
+
if shape and (shape[-1] == 6):
|
|
62
|
+
new_shape = shape[:-1] + (3, 3)
|
|
63
|
+
unmapped_matrix = np.zeros(new_shape)
|
|
64
|
+
for i in range(6):
|
|
65
|
+
unmapped_matrix[..., _voigt_numbering[i][0], _voigt_numbering[i][1]] = array[..., i] / mapping_convention[i]
|
|
66
|
+
return unmapped_matrix
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError("array must be of shape (6,) or (...,6) with Voigt vector")
|
|
69
|
+
|
|
70
|
+
def _map(matrix, mapping_convention):
|
|
71
|
+
shape = matrix.shape[:-2] + (6,)
|
|
72
|
+
array = np.zeros(shape)
|
|
73
|
+
for i in range(6):
|
|
74
|
+
j, k = _voigt_numbering[i]
|
|
75
|
+
array[...,i] = matrix[...,j,k]
|
|
76
|
+
return array * mapping_convention
|
|
77
|
+
|
|
78
|
+
kelvin_mapping = [1, 1, 1, np.sqrt(2), np.sqrt(2), np.sqrt(2)]
|
|
79
|
+
|
|
39
80
|
class SecondOrderTensor:
|
|
40
81
|
"""
|
|
41
82
|
Template class for manipulation of second order tensors or arrays of second order tensors
|
|
@@ -251,7 +292,7 @@ class SecondOrderTensor:
|
|
|
251
292
|
I3 : Third invariant of the tensors (det)
|
|
252
293
|
"""
|
|
253
294
|
a = self.I1**2
|
|
254
|
-
b = np.matmul(self.matrix, self.
|
|
295
|
+
b = np.matmul(self.matrix, self._transpose_tensor()).trace(axis1=-1, axis2=-2)
|
|
255
296
|
return 0.5 * (a - b)
|
|
256
297
|
|
|
257
298
|
@property
|
|
@@ -391,13 +432,9 @@ class SecondOrderTensor:
|
|
|
391
432
|
matmul : matrix-like multiplication of tensor arrays
|
|
392
433
|
"""
|
|
393
434
|
if isinstance(B, SecondOrderTensor):
|
|
394
|
-
|
|
395
|
-
return SecondOrderTensor(new_mat)
|
|
435
|
+
return self.dot(B, mode='pair')
|
|
396
436
|
elif isinstance(B, Rotation) or is_orix_rotation(B):
|
|
397
|
-
|
|
398
|
-
new_matrix = np.matmul(np.matmul(transpose_matrices, self.matrix), rotation_matrices)
|
|
399
|
-
# In case of rotation, the property of the transformed tensor is kept
|
|
400
|
-
return self.__class__(new_matrix)
|
|
437
|
+
return self.rotate(B, mode='pair')
|
|
401
438
|
elif isinstance(B, (float, int)):
|
|
402
439
|
return self.__class__(self.matrix * B)
|
|
403
440
|
elif isinstance(B, np.ndarray):
|
|
@@ -413,6 +450,44 @@ class SecondOrderTensor:
|
|
|
413
450
|
else:
|
|
414
451
|
raise ValueError('The input argument must be a tensor, an ndarray, a rotation or a scalar value.')
|
|
415
452
|
|
|
453
|
+
def rotate(self, rotation, mode='pair'):
|
|
454
|
+
"""
|
|
455
|
+
Apply rotation(s) to the tensor(s).
|
|
456
|
+
|
|
457
|
+
The rotations can be applied element-wise, or on each cross-combination (see below).
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
rotation : scipy.spatial.Rotation or orix.quaternion.Rotation
|
|
462
|
+
mode : str, optional
|
|
463
|
+
If 'pair', the rotations are applied element wise. Broadcasting rule applies.
|
|
464
|
+
If 'cross', all the possible combinations are considered. If ``C=A.rotate(rot)``, then
|
|
465
|
+
``C.shape==A.shape + rot.shape``.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
SecondOrderTensor
|
|
470
|
+
"""
|
|
471
|
+
if self.shape == ():
|
|
472
|
+
ein_str = '...li,...kj,lk->...ij'
|
|
473
|
+
elif _is_single_rotation(rotation):
|
|
474
|
+
ein_str = 'li,kj,...lk->...ij'
|
|
475
|
+
else:
|
|
476
|
+
if mode=='pair':
|
|
477
|
+
ein_str = '...li,...kj,...lk->...ij'
|
|
478
|
+
elif mode=='cross':
|
|
479
|
+
ndim_0 = self.ndim
|
|
480
|
+
ndim_1 = len(_orientation_shape(rotation))
|
|
481
|
+
indices_self = ALPHABET[:ndim_0]
|
|
482
|
+
indices_g = ALPHABET[:ndim_1].upper()
|
|
483
|
+
indices_res = indices_self + indices_g
|
|
484
|
+
ein_str = indices_g + 'zw,' + indices_g + 'yx,' + indices_self + 'zy->' + indices_res + 'wx'
|
|
485
|
+
else:
|
|
486
|
+
raise ValueError('Invalid mode. It can be "cross" or "pair".')
|
|
487
|
+
g_mat = rotation_to_matrix(rotation)
|
|
488
|
+
matrix = np.einsum(ein_str, g_mat, g_mat, self.matrix)
|
|
489
|
+
return self.__class__(matrix)
|
|
490
|
+
|
|
416
491
|
def __rmul__(self, other):
|
|
417
492
|
if isinstance(other, (float, int)):
|
|
418
493
|
return self.__mul__(other)
|
|
@@ -431,7 +506,7 @@ class SecondOrderTensor:
|
|
|
431
506
|
raise NotImplementedError('Tensors can only be divided by scalar values or by arrays of the same shape.')
|
|
432
507
|
return self.__class__(new_mat)
|
|
433
508
|
|
|
434
|
-
def __eq__(self, other)
|
|
509
|
+
def __eq__(self, other):
|
|
435
510
|
"""
|
|
436
511
|
Check whether the tensors in the tensor array are equal
|
|
437
512
|
|
|
@@ -442,7 +517,7 @@ class SecondOrderTensor:
|
|
|
442
517
|
|
|
443
518
|
Returns
|
|
444
519
|
-------
|
|
445
|
-
|
|
520
|
+
numpy.ndarray
|
|
446
521
|
True element is True if the corresponding tensors are equal.
|
|
447
522
|
"""
|
|
448
523
|
if isinstance(other, SecondOrderTensor):
|
|
@@ -453,6 +528,73 @@ class SecondOrderTensor:
|
|
|
453
528
|
else:
|
|
454
529
|
raise ValueError('The value to compare must be an array of shape {} or {}'.format(self.shape, self.shape + (3,3)))
|
|
455
530
|
|
|
531
|
+
def dot(self, other, mode='pair'):
|
|
532
|
+
"""
|
|
533
|
+
Perform contraction product ("dot product") between tensor.
|
|
534
|
+
|
|
535
|
+
On tensor arrays, the product contraction can be performed element-wise, or considering all cross-combinations
|
|
536
|
+
(see below).
|
|
537
|
+
|
|
538
|
+
Parameters
|
|
539
|
+
----------
|
|
540
|
+
other : SecondOrderTensor
|
|
541
|
+
tensor or tensor array to compute the product from
|
|
542
|
+
mode : str, optional
|
|
543
|
+
If 'pair' (default), the contraction products of tensor arrays are applied element-wise. Broadcasting rule
|
|
544
|
+
applies.
|
|
545
|
+
|
|
546
|
+
If 'cross', all combinations of contraction product are considered. If ``C=A.dot(B,mode='cross')``, then
|
|
547
|
+
``C.shape==A.shape + B.shape``
|
|
548
|
+
|
|
549
|
+
Returns
|
|
550
|
+
-------
|
|
551
|
+
SecondOrderTensor
|
|
552
|
+
|
|
553
|
+
Examples
|
|
554
|
+
--------
|
|
555
|
+
>>> from Elasticipy.SecondOrderTensor import SecondOrderTensor
|
|
556
|
+
>>> A=SecondOrderTensor.rand(10)
|
|
557
|
+
>>> B=SecondOrderTensor.rand(10)
|
|
558
|
+
>>> AB_pair = A.dot(B)
|
|
559
|
+
>>> AB_pair.shape
|
|
560
|
+
(10,)
|
|
561
|
+
|
|
562
|
+
>>> AB_cross = A.dot(B, mode='cross')
|
|
563
|
+
>>> AB_cross.shape
|
|
564
|
+
(10, 10)
|
|
565
|
+
|
|
566
|
+
We can for instance check that:
|
|
567
|
+
|
|
568
|
+
>>> AB_pair[5] == A[5].dot(B[5])
|
|
569
|
+
True
|
|
570
|
+
|
|
571
|
+
and:
|
|
572
|
+
|
|
573
|
+
>>> AB_cross[0,1] == A[0].dot(B[1])
|
|
574
|
+
True
|
|
575
|
+
|
|
576
|
+
See Also
|
|
577
|
+
--------
|
|
578
|
+
ddot : Double-contraction product
|
|
579
|
+
"""
|
|
580
|
+
if self.shape == ():
|
|
581
|
+
ein_str = 'ik,...kj->...ij'
|
|
582
|
+
else:
|
|
583
|
+
if mode=='pair':
|
|
584
|
+
ein_str = '...ik,...kj->...ij'
|
|
585
|
+
elif mode=='cross':
|
|
586
|
+
ndim_0 = self.ndim
|
|
587
|
+
ndim_1 = other.ndim
|
|
588
|
+
indices_0 = ALPHABET[:ndim_0]
|
|
589
|
+
indices_1 = ALPHABET[:ndim_1].upper()
|
|
590
|
+
indices_2 = indices_0 + indices_1
|
|
591
|
+
ein_str = indices_0 + 'ik,' + indices_1 + 'kj->' + indices_2 + 'ij'
|
|
592
|
+
else:
|
|
593
|
+
raise ValueError('Invalid mode. Use "pair" or "cross".')
|
|
594
|
+
matrix = np.einsum(ein_str, self.matrix, other.matrix)
|
|
595
|
+
return SecondOrderTensor(matrix)
|
|
596
|
+
|
|
597
|
+
|
|
456
598
|
def matmul(self, other):
|
|
457
599
|
"""
|
|
458
600
|
Perform matrix-like product between tensor arrays. Each "product" is a matrix product between
|
|
@@ -481,28 +623,19 @@ class SecondOrderTensor:
|
|
|
481
623
|
--------
|
|
482
624
|
__mul__ : Element-wise matrix product
|
|
483
625
|
"""
|
|
626
|
+
warnings.warn(
|
|
627
|
+
'matmul() is deprecated and will be removed in a future version. Use dot(tensor,mode="cross") or '
|
|
628
|
+
'rotate(rotation,mode="cross") instead.',
|
|
629
|
+
DeprecationWarning,
|
|
630
|
+
stacklevel=2)
|
|
484
631
|
if isinstance(other, SecondOrderTensor):
|
|
485
|
-
|
|
632
|
+
return self.dot(other, mode='cross')
|
|
486
633
|
elif isinstance(other, Rotation) or is_orix_rotation(Rotation):
|
|
487
|
-
|
|
488
|
-
else:
|
|
489
|
-
other_matrix = other
|
|
490
|
-
matrix = self.matrix
|
|
491
|
-
shape_matrix = matrix.shape[:-2]
|
|
492
|
-
shape_other = other_matrix.shape[:-2]
|
|
493
|
-
extra_dim_matrix = len(shape_other)
|
|
494
|
-
extra_dim_other = len(shape_matrix)
|
|
495
|
-
matrix_expanded = matrix.reshape(shape_matrix + (1,) * extra_dim_other + (3, 3))
|
|
496
|
-
other_expanded = other_matrix.reshape((1,) * extra_dim_matrix + shape_other + (3, 3))
|
|
497
|
-
if isinstance(other, Rotation):
|
|
498
|
-
other_expanded_t = _transpose_matrix(other_expanded)
|
|
499
|
-
new_mat = np.matmul(np.matmul(other_expanded_t, matrix_expanded), other_expanded)
|
|
500
|
-
return self.__class__(np.squeeze(new_mat))
|
|
634
|
+
return self.rotate(other, mode='cross')
|
|
501
635
|
else:
|
|
502
|
-
|
|
503
|
-
return SecondOrderTensor(np.squeeze(new_mat))
|
|
636
|
+
raise ValueError('The input argument must be either a rotation or a SecondOrderTensor')
|
|
504
637
|
|
|
505
|
-
def
|
|
638
|
+
def transpose_array(self):
|
|
506
639
|
"""
|
|
507
640
|
Transpose the array of tensors
|
|
508
641
|
|
|
@@ -531,19 +664,19 @@ class SecondOrderTensor:
|
|
|
531
664
|
"""
|
|
532
665
|
Transpose the array of tensors.
|
|
533
666
|
|
|
534
|
-
It is actually an alias for
|
|
667
|
+
It is actually an alias for transpose_array()
|
|
535
668
|
|
|
536
669
|
Returns
|
|
537
670
|
-------
|
|
538
671
|
SecondOrderTensor
|
|
539
672
|
Transposed array
|
|
540
673
|
"""
|
|
541
|
-
return self.
|
|
674
|
+
return self.transpose_array()
|
|
542
675
|
|
|
543
|
-
def
|
|
676
|
+
def _transpose_tensor(self):
|
|
544
677
|
return _transpose_matrix(self.matrix)
|
|
545
678
|
|
|
546
|
-
def
|
|
679
|
+
def transpose_tensor(self):
|
|
547
680
|
"""
|
|
548
681
|
Transpose of tensors of the tensor array
|
|
549
682
|
|
|
@@ -554,11 +687,11 @@ class SecondOrderTensor:
|
|
|
554
687
|
|
|
555
688
|
See Also
|
|
556
689
|
--------
|
|
557
|
-
|
|
690
|
+
transpose_array : transpose the array (not the components)
|
|
558
691
|
"""
|
|
559
|
-
return self.__class__(self.
|
|
692
|
+
return self.__class__(self._transpose_tensor())
|
|
560
693
|
|
|
561
|
-
def ddot(self, other):
|
|
694
|
+
def ddot(self, other, mode='pair'):
|
|
562
695
|
"""
|
|
563
696
|
Double dot product (contraction of tensor product, usually denoted ":") of two tensors.
|
|
564
697
|
|
|
@@ -570,6 +703,11 @@ class SecondOrderTensor:
|
|
|
570
703
|
----------
|
|
571
704
|
other : SecondOrderTensor or np.ndarray
|
|
572
705
|
Tensor or tensor array to multiply by before contraction.
|
|
706
|
+
mode : str, optional
|
|
707
|
+
If "pair", the dot products are performed element-wise before contraction. Broadcasting rule applies.
|
|
708
|
+
If "cross", all the cross-combinations are computed, increasing the dimensionality.
|
|
709
|
+
If ``C=A.ddot(B, mode='cross')``, then ``C.shape = A.shape + B.shape``.
|
|
710
|
+
|
|
573
711
|
|
|
574
712
|
Returns
|
|
575
713
|
-------
|
|
@@ -578,10 +716,10 @@ class SecondOrderTensor:
|
|
|
578
716
|
|
|
579
717
|
See Also
|
|
580
718
|
--------
|
|
581
|
-
|
|
719
|
+
dot : contraction product ("dot product") between tensor.
|
|
582
720
|
|
|
583
721
|
"""
|
|
584
|
-
tensor_prod = self.
|
|
722
|
+
tensor_prod = self.transpose_tensor().dot(other, mode=mode)
|
|
585
723
|
return tensor_prod.trace()
|
|
586
724
|
|
|
587
725
|
def _flatten(self):
|
|
@@ -750,7 +888,7 @@ class SecondOrderTensor:
|
|
|
750
888
|
return self
|
|
751
889
|
|
|
752
890
|
def _symmetric_part(self):
|
|
753
|
-
return 0.5 * (self.matrix + self.
|
|
891
|
+
return 0.5 * (self.matrix + self._transpose_tensor())
|
|
754
892
|
|
|
755
893
|
def symmetric_part(self):
|
|
756
894
|
"""
|
|
@@ -776,7 +914,7 @@ class SecondOrderTensor:
|
|
|
776
914
|
SkewSymmetricSecondOrderTensor
|
|
777
915
|
Skew-symmetric tensor
|
|
778
916
|
"""
|
|
779
|
-
new_mat = 0.5 * (self.matrix - self.
|
|
917
|
+
new_mat = 0.5 * (self.matrix - self._transpose_tensor())
|
|
780
918
|
return SkewSymmetricSecondOrderTensor(new_mat)
|
|
781
919
|
|
|
782
920
|
def spherical_part(self):
|
|
@@ -922,7 +1060,7 @@ class SecondOrderTensor:
|
|
|
922
1060
|
|
|
923
1061
|
Parameters
|
|
924
1062
|
----------
|
|
925
|
-
shape : tuple, optional
|
|
1063
|
+
shape : int or tuple, optional
|
|
926
1064
|
Shape of the tensor array. If not provided, a single tensor is returned
|
|
927
1065
|
seed : int, optional
|
|
928
1066
|
Sets the seed for random generation. Useful to ensure reproducibility
|
|
@@ -954,6 +1092,8 @@ class SecondOrderTensor:
|
|
|
954
1092
|
"""
|
|
955
1093
|
if shape is None:
|
|
956
1094
|
shape = (3,3)
|
|
1095
|
+
elif isinstance(shape, int):
|
|
1096
|
+
shape = (shape, 3, 3)
|
|
957
1097
|
else:
|
|
958
1098
|
shape = shape + (3,3)
|
|
959
1099
|
rng = np.random.default_rng(seed)
|
|
@@ -1281,7 +1421,10 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
|
1281
1421
|
>>> a==b
|
|
1282
1422
|
True
|
|
1283
1423
|
"""
|
|
1284
|
-
|
|
1424
|
+
if isinstance(mat, SecondOrderTensor):
|
|
1425
|
+
mat = mat.matrix
|
|
1426
|
+
else:
|
|
1427
|
+
mat = np.asarray(mat, dtype=float)
|
|
1285
1428
|
mat_transposed = _transpose_matrix(mat)
|
|
1286
1429
|
if np.all(np.isclose(mat, mat_transposed)) or force_symmetry:
|
|
1287
1430
|
# The input matrix is symmetric
|
|
@@ -1308,10 +1451,15 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
|
1308
1451
|
----------
|
|
1309
1452
|
array : np.ndarray or list
|
|
1310
1453
|
array to build the SymmetricSecondOrderTensor from. We must have array.ndim>0 and array.shape[-1]==6.
|
|
1454
|
+
|
|
1311
1455
|
Returns
|
|
1312
1456
|
-------
|
|
1313
1457
|
SymmetricSecondOrderTensor
|
|
1314
1458
|
|
|
1459
|
+
See Also
|
|
1460
|
+
--------
|
|
1461
|
+
from_Kelvin : Construct a tensor from vector(s) following the Kelvin notation
|
|
1462
|
+
|
|
1315
1463
|
Examples
|
|
1316
1464
|
--------
|
|
1317
1465
|
>>> from Elasticipy.SecondOrderTensor import SymmetricSecondOrderTensor
|
|
@@ -1320,23 +1468,62 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
|
1320
1468
|
[[11. 12. 13.]
|
|
1321
1469
|
[12. 22. 23.]
|
|
1322
1470
|
[13. 23. 33.]]
|
|
1471
|
+
"""
|
|
1472
|
+
matrix = _unmap(array, cls.voigt_map)
|
|
1473
|
+
return cls(matrix)
|
|
1323
1474
|
|
|
1475
|
+
def to_Voigt(self):
|
|
1324
1476
|
"""
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1477
|
+
Convert the tensor to vector, or slices of vector, following the Voigt convention.
|
|
1478
|
+
|
|
1479
|
+
If the tensor array has shape (m,n,...), the result will be of shape (m,n,...,6).
|
|
1480
|
+
|
|
1481
|
+
Returns
|
|
1482
|
+
-------
|
|
1483
|
+
numpy.ndarray
|
|
1484
|
+
Voigt vector summarizing the components
|
|
1485
|
+
"""
|
|
1486
|
+
return _map(self.matrix, self.voigt_map)
|
|
1487
|
+
|
|
1488
|
+
@classmethod
|
|
1489
|
+
def from_Kelvin(cls, array):
|
|
1490
|
+
"""
|
|
1491
|
+
Build a tensor from the Kelvin vector, or slices of Kelvin vectors
|
|
1492
|
+
|
|
1493
|
+
Parameters
|
|
1494
|
+
----------
|
|
1495
|
+
array : np.ndarray or list
|
|
1496
|
+
Vectors, or slices of vectors, consisting in components following the Kelvin convention
|
|
1497
|
+
Returns
|
|
1498
|
+
-------
|
|
1499
|
+
SymmetricSecondOrderTensor
|
|
1500
|
+
|
|
1501
|
+
See Also
|
|
1502
|
+
--------
|
|
1503
|
+
from_Voigt : construct a tensor from vector(s) following the Voigt notation
|
|
1504
|
+
to_Kelvin : convert the tensor to vector(s) following the Kelvin convention
|
|
1505
|
+
"""
|
|
1506
|
+
matrix = _unmap(array, kelvin_mapping)
|
|
1507
|
+
return cls(matrix)
|
|
1508
|
+
|
|
1509
|
+
def to_Kelvin(self):
|
|
1510
|
+
"""
|
|
1511
|
+
Convert the tensor to vector, or slices of vector, following the Kelvin(-Mandel) convention.
|
|
1512
|
+
|
|
1513
|
+
Returns
|
|
1514
|
+
-------
|
|
1515
|
+
numpy.ndarray
|
|
1516
|
+
|
|
1517
|
+
See Also
|
|
1518
|
+
--------
|
|
1519
|
+
from_Kelvin : Construct a tensor from vector(s) following the Kelvin convention
|
|
1520
|
+
to_Voigt : Convert the tensor to vector(s) following the Voigt convention
|
|
1521
|
+
"""
|
|
1522
|
+
return _map(self.matrix, kelvin_mapping)
|
|
1336
1523
|
|
|
1337
1524
|
def eig(self):
|
|
1338
1525
|
"""
|
|
1339
|
-
Compute the principal values (
|
|
1526
|
+
Compute the principal values (eigenvalues) and principal direction (eigenvectors) of the tensor, sorted in
|
|
1340
1527
|
descending order of principal values
|
|
1341
1528
|
|
|
1342
1529
|
Returns
|
|
@@ -1351,7 +1538,7 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
|
1351
1538
|
eigvals : compute the principal values only
|
|
1352
1539
|
"""
|
|
1353
1540
|
eigvals, eigdir = np.linalg.eigh(self.matrix)
|
|
1354
|
-
return
|
|
1541
|
+
return eigvals[..., ::-1], eigdir[..., :, ::-1]
|
|
1355
1542
|
|
|
1356
1543
|
def eigvals(self):
|
|
1357
1544
|
"""
|
|
@@ -1437,6 +1624,8 @@ def rotation_to_matrix(rotation, return_transpose=False):
|
|
|
1437
1624
|
elif is_orix_rotation(rotation):
|
|
1438
1625
|
inv_rotation = ~rotation
|
|
1439
1626
|
matrix = inv_rotation.to_matrix()
|
|
1627
|
+
if matrix.shape == (1,3,3):
|
|
1628
|
+
matrix = matrix[0]
|
|
1440
1629
|
else:
|
|
1441
1630
|
raise TypeError('The input argument must be of class scipy.transform.Rotation or '
|
|
1442
1631
|
'orix.quaternion.rotation.Rotation')
|