elasticipy 4.0.0__py3-none-any.whl → 4.1.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/gui.py +1 -1
- Elasticipy/interfaces/FEPX.py +119 -0
- Elasticipy/interfaces/PRISMS.py +103 -0
- Elasticipy/plasticity.py +34 -1
- Elasticipy/polefigure.py +21 -0
- Elasticipy/spherical_function.py +2 -14
- Elasticipy/tensors/elasticity.py +171 -79
- Elasticipy/tensors/fourth_order.py +126 -55
- Elasticipy/tensors/mapping.py +44 -0
- Elasticipy/tensors/second_order.py +107 -3
- Elasticipy/tensors/stress_strain.py +20 -4
- {elasticipy-4.0.0.dist-info → elasticipy-4.1.0.dist-info}/METADATA +22 -4
- elasticipy-4.1.0.dist-info/RECORD +23 -0
- {elasticipy-4.0.0.dist-info → elasticipy-4.1.0.dist-info}/WHEEL +1 -1
- elasticipy-4.0.0.dist-info/RECORD +0 -20
- {elasticipy-4.0.0.dist-info → elasticipy-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {elasticipy-4.0.0.dist-info → elasticipy-4.1.0.dist-info}/top_level.txt +0 -0
|
@@ -3,14 +3,7 @@ from Elasticipy.tensors.second_order import SymmetricSecondOrderTensor, rotation
|
|
|
3
3
|
SecondOrderTensor, ALPHABET
|
|
4
4
|
from scipy.spatial.transform import Rotation
|
|
5
5
|
from copy import deepcopy
|
|
6
|
-
|
|
7
|
-
a = np.sqrt(2)
|
|
8
|
-
KELVIN_MAPPING_MATRIX = np.array([[1, 1, 1, a, a, a],
|
|
9
|
-
[1, 1, 1, a, a, a],
|
|
10
|
-
[1, 1, 1, a, a, a],
|
|
11
|
-
[a, a, a, 2, 2, 2],
|
|
12
|
-
[a, a, a, 2, 2, 2],
|
|
13
|
-
[a, a, a, 2, 2, 2], ])
|
|
6
|
+
from Elasticipy.tensors.mapping import KelvinMapping, VoigtMapping
|
|
14
7
|
|
|
15
8
|
|
|
16
9
|
def voigt_indices(i, j):
|
|
@@ -77,12 +70,12 @@ class FourthOrderTensor:
|
|
|
77
70
|
|
|
78
71
|
Attributes
|
|
79
72
|
----------
|
|
80
|
-
|
|
73
|
+
_matrix : np.ndarray
|
|
81
74
|
(6,6) matrix gathering all the components of the tensor, using the Voigt notation.
|
|
82
75
|
"""
|
|
83
76
|
tensor_name = '4th-order'
|
|
84
77
|
|
|
85
|
-
def __init__(self, M, mapping=
|
|
78
|
+
def __init__(self, M, mapping=KelvinMapping(), check_minor_symmetry=True, force_minor_symmetry=False):
|
|
86
79
|
"""
|
|
87
80
|
Construct of Fourth-order tensor with minor symmetry.
|
|
88
81
|
|
|
@@ -91,7 +84,7 @@ class FourthOrderTensor:
|
|
|
91
84
|
M : np.ndarray
|
|
92
85
|
(6,6) matrix corresponding to the stiffness tensor, written using the Voigt notation, or array of shape
|
|
93
86
|
(3,3,3,3).
|
|
94
|
-
mapping :
|
|
87
|
+
mapping : MappingConvention, optional
|
|
95
88
|
Mapping convention to translate the (3,3,3,3) array to (6,6) matrix
|
|
96
89
|
check_minor_symmetry : bool, optional
|
|
97
90
|
If true (default), check that the input array have minor symmetries (see Notes). Only used if an array of
|
|
@@ -108,19 +101,7 @@ class FourthOrderTensor:
|
|
|
108
101
|
M_{ijkl}=M_{jikl}=M_{jilk}=M_{ijlk}
|
|
109
102
|
|
|
110
103
|
"""
|
|
111
|
-
|
|
112
|
-
self.mapping_matrix = mapping
|
|
113
|
-
self.mapping_name = 'custom'
|
|
114
|
-
else:
|
|
115
|
-
mapping = mapping.capitalize()
|
|
116
|
-
self.mapping_name = mapping
|
|
117
|
-
if mapping == 'Kelvin':
|
|
118
|
-
self.mapping_matrix = KELVIN_MAPPING_MATRIX
|
|
119
|
-
elif mapping == 'Voigt':
|
|
120
|
-
self.mapping_matrix = np.ones((6,6))
|
|
121
|
-
else:
|
|
122
|
-
raise ValueError('The mapping to use can be either "Kelvin", "Voigt", or a (6,6) matrix.')
|
|
123
|
-
|
|
104
|
+
self.mapping=mapping
|
|
124
105
|
M = np.asarray(M)
|
|
125
106
|
if M.shape[-2:] == (6, 6):
|
|
126
107
|
matrix = M
|
|
@@ -137,11 +118,11 @@ class FourthOrderTensor:
|
|
|
137
118
|
matrix = self._full_to_matrix(M)
|
|
138
119
|
else:
|
|
139
120
|
raise ValueError('The input matrix must of shape (...,6,6) or (...,3,3,3,3)')
|
|
140
|
-
self.
|
|
121
|
+
self._matrix = matrix
|
|
141
122
|
for i in range(0, 6):
|
|
142
123
|
for j in range(0, 6):
|
|
143
124
|
def getter(obj, I=i, J=j):
|
|
144
|
-
return obj.
|
|
125
|
+
return obj._matrix[...,I, J]
|
|
145
126
|
|
|
146
127
|
getter.__doc__ = f"Returns the ({i + 1},{j + 1}) component of the {self.tensor_name} matrix."
|
|
147
128
|
component_name = 'C{}{}'.format(i + 1, j + 1)
|
|
@@ -149,8 +130,8 @@ class FourthOrderTensor:
|
|
|
149
130
|
|
|
150
131
|
def __repr__(self):
|
|
151
132
|
if (self.ndim == 0) or ((self.ndim==1) and self.shape[0]<5):
|
|
152
|
-
msg = '{} tensor (in {} mapping):\n'.format(self.tensor_name, self.
|
|
153
|
-
msg += self.
|
|
133
|
+
msg = '{} tensor (in {} mapping):\n'.format(self.tensor_name, self.mapping.name)
|
|
134
|
+
msg += self._matrix.__str__()
|
|
154
135
|
else:
|
|
155
136
|
msg = '{} tensor array of shape {}'.format(self.tensor_name, self.shape)
|
|
156
137
|
return msg
|
|
@@ -164,7 +145,7 @@ class FourthOrderTensor:
|
|
|
164
145
|
tuple
|
|
165
146
|
Shape of the tensor array
|
|
166
147
|
"""
|
|
167
|
-
*shape, _, _ = self.
|
|
148
|
+
*shape, _, _ = self._matrix.shape
|
|
168
149
|
return tuple(shape)
|
|
169
150
|
|
|
170
151
|
def full_tensor(self):
|
|
@@ -179,7 +160,7 @@ class FourthOrderTensor:
|
|
|
179
160
|
i, j, k, ell = np.indices((3, 3, 3, 3))
|
|
180
161
|
ij = voigt_indices(i, j)
|
|
181
162
|
kl = voigt_indices(k, ell)
|
|
182
|
-
m = self.
|
|
163
|
+
m = self._matrix[..., ij, kl] / self.mapping.matrix[ij, kl]
|
|
183
164
|
return m
|
|
184
165
|
|
|
185
166
|
def flatten(self):
|
|
@@ -197,7 +178,7 @@ class FourthOrderTensor:
|
|
|
197
178
|
if shape:
|
|
198
179
|
t2 = deepcopy(self)
|
|
199
180
|
p = (np.prod(self.shape), 6, 6)
|
|
200
|
-
t2.
|
|
181
|
+
t2._matrix = self._matrix.reshape(p)
|
|
201
182
|
return t2
|
|
202
183
|
else:
|
|
203
184
|
return self
|
|
@@ -206,7 +187,7 @@ class FourthOrderTensor:
|
|
|
206
187
|
kl, ij = np.indices((6, 6))
|
|
207
188
|
i, j = unvoigt_index(ij).T
|
|
208
189
|
k, ell = unvoigt_index(kl).T
|
|
209
|
-
return full_tensor[..., i, j, k, ell] * self.
|
|
190
|
+
return full_tensor[..., i, j, k, ell] * self.mapping.matrix[ij, kl]
|
|
210
191
|
|
|
211
192
|
def rotate(self, rotation):
|
|
212
193
|
"""
|
|
@@ -224,7 +205,7 @@ class FourthOrderTensor:
|
|
|
224
205
|
"""
|
|
225
206
|
t2 = deepcopy(self)
|
|
226
207
|
rotated_tensor = rotate_tensor(self.full_tensor(), rotation)
|
|
227
|
-
t2.
|
|
208
|
+
t2._matrix = self._full_to_matrix(rotated_tensor)
|
|
228
209
|
return t2
|
|
229
210
|
|
|
230
211
|
@property
|
|
@@ -261,13 +242,13 @@ class FourthOrderTensor:
|
|
|
261
242
|
t2 = deepcopy(self)
|
|
262
243
|
if axis is None:
|
|
263
244
|
axis = tuple([i for i in range(0,self.ndim)])
|
|
264
|
-
t2.
|
|
245
|
+
t2._matrix = np.mean(self._matrix, axis=axis)
|
|
265
246
|
return t2
|
|
266
247
|
|
|
267
248
|
def __add__(self, other):
|
|
268
249
|
if isinstance(other, np.ndarray):
|
|
269
250
|
if other.shape == (6, 6):
|
|
270
|
-
mat = self.
|
|
251
|
+
mat = self._matrix + other
|
|
271
252
|
elif other.shape == (3, 3, 3, 3):
|
|
272
253
|
mat = self._full_to_matrix(self.full_tensor() + other)
|
|
273
254
|
else:
|
|
@@ -279,17 +260,17 @@ class FourthOrderTensor:
|
|
|
279
260
|
raise ValueError('The two tensors to add must be of the same class.')
|
|
280
261
|
else:
|
|
281
262
|
raise ValueError('I don''t know how to add {} with {}.'.format(type(self), type(other)))
|
|
282
|
-
return self.__class__(mat, mapping=self.
|
|
263
|
+
return self.__class__(mat, mapping=self.mapping)
|
|
283
264
|
|
|
284
265
|
def __sub__(self, other):
|
|
285
266
|
if isinstance(other, FourthOrderTensor):
|
|
286
|
-
return self.__add__(-other.
|
|
267
|
+
return self.__add__(-other._matrix)
|
|
287
268
|
else:
|
|
288
269
|
return self.__add__(-other)
|
|
289
270
|
|
|
290
271
|
def __neg__(self):
|
|
291
272
|
t = deepcopy(self)
|
|
292
|
-
t.
|
|
273
|
+
t._matrix = -t._matrix
|
|
293
274
|
return t
|
|
294
275
|
|
|
295
276
|
def ddot(self, other, mode='pair'):
|
|
@@ -348,14 +329,14 @@ class FourthOrderTensor:
|
|
|
348
329
|
elif isinstance(other, np.ndarray):
|
|
349
330
|
shape = other.shape
|
|
350
331
|
if other.shape == self.shape[-len(shape):]:
|
|
351
|
-
matrix = self.
|
|
332
|
+
matrix = self._matrix * other[...,np.newaxis, np.newaxis]
|
|
352
333
|
return self.__class__(matrix)
|
|
353
334
|
else:
|
|
354
335
|
raise ValueError('The arrays to multiply could not be broadcasted with shapes {} and {}'.format(self.shape, other.shape[:-2]))
|
|
355
336
|
elif isinstance(other, Rotation) or is_orix_rotation(other):
|
|
356
337
|
return self.rotate(other)
|
|
357
338
|
else:
|
|
358
|
-
return self.__class__(self.
|
|
339
|
+
return self.__class__(self._matrix * other)
|
|
359
340
|
|
|
360
341
|
def __truediv__(self, other):
|
|
361
342
|
if isinstance(other, (SecondOrderTensor, FourthOrderTensor)):
|
|
@@ -378,7 +359,7 @@ class FourthOrderTensor:
|
|
|
378
359
|
return self
|
|
379
360
|
else:
|
|
380
361
|
new_axes = tuple(range(ndim))[::-1] + (ndim, ndim + 1)
|
|
381
|
-
transposed_matrix = self.
|
|
362
|
+
transposed_matrix = self._matrix.transpose(new_axes)
|
|
382
363
|
return self.__class__(transposed_matrix)
|
|
383
364
|
|
|
384
365
|
def __rmul__(self, other):
|
|
@@ -389,16 +370,16 @@ class FourthOrderTensor:
|
|
|
389
370
|
|
|
390
371
|
def __eq__(self, other):
|
|
391
372
|
if isinstance(other, FourthOrderTensor):
|
|
392
|
-
return np.all(self.
|
|
373
|
+
return np.all(self._matrix == other._matrix, axis=(-1, -2))
|
|
393
374
|
elif isinstance(other, (float, int)) or (isinstance(other, np.ndarray) and other.shape[-2:] == (6, 6)):
|
|
394
|
-
return np.all(self.
|
|
375
|
+
return np.all(self._matrix == other, axis=(-1, -2))
|
|
395
376
|
else:
|
|
396
377
|
raise NotImplementedError('The element to compare with must be a fourth-order tensor '
|
|
397
378
|
'or an array of shape (6,6).')
|
|
398
379
|
|
|
399
380
|
def __getitem__(self, item):
|
|
400
381
|
if self.ndim:
|
|
401
|
-
sub_mat= self.
|
|
382
|
+
sub_mat= self._matrix[item]
|
|
402
383
|
if sub_mat.shape[-2:] != (6,6):
|
|
403
384
|
raise IndexError('Too many indices for tensor array: array is {}-dimensional, but {} were provided'.format(self.ndim, len(item)))
|
|
404
385
|
else:
|
|
@@ -409,19 +390,19 @@ class FourthOrderTensor:
|
|
|
409
390
|
def __setitem__(self, index, value):
|
|
410
391
|
if isinstance(value, np.ndarray):
|
|
411
392
|
if value.shape[-2:] == (6,6):
|
|
412
|
-
self.
|
|
393
|
+
self._matrix[index] = value
|
|
413
394
|
elif value.shape[-4:] == (3,3,3,3):
|
|
414
395
|
submatrix = self._full_to_matrix(value)
|
|
415
|
-
self.
|
|
396
|
+
self._matrix[index] = submatrix
|
|
416
397
|
else:
|
|
417
398
|
return ValueError('The R.h.s must be either of shape (...,6,6) or (...,3,3,3,3)')
|
|
418
399
|
elif isinstance(value, FourthOrderTensor):
|
|
419
|
-
self.
|
|
400
|
+
self._matrix[index] = value._matrix / value.mapping.matrix * self.mapping.matrix
|
|
420
401
|
else:
|
|
421
402
|
raise NotImplementedError('The r.h.s must be either an ndarray or an object of class {}'.format(self.__class__))
|
|
422
403
|
|
|
423
404
|
@classmethod
|
|
424
|
-
def identity(cls, shape=(), return_full_tensor=False, mapping=
|
|
405
|
+
def identity(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
|
|
425
406
|
"""
|
|
426
407
|
Create a 4th-order identity tensor
|
|
427
408
|
|
|
@@ -432,6 +413,8 @@ class FourthOrderTensor:
|
|
|
432
413
|
return_full_tensor : bool, optional
|
|
433
414
|
If True, return the full tensor as a (3,3,3,3) or a (...,3,3,3,3) array. Otherwise, the tensor is returned
|
|
434
415
|
as a SymmetricTensor object.
|
|
416
|
+
mapping : str, optional
|
|
417
|
+
Mapping convention to use. Must be either Kelvin or Voigt.
|
|
435
418
|
|
|
436
419
|
Returns
|
|
437
420
|
-------
|
|
@@ -452,6 +435,82 @@ class FourthOrderTensor:
|
|
|
452
435
|
else:
|
|
453
436
|
return cls(full, mapping=mapping)
|
|
454
437
|
|
|
438
|
+
@classmethod
|
|
439
|
+
def identity_spherical_part(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
|
|
440
|
+
"""
|
|
441
|
+
Return the spherical part of the identity tensor
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
shape : tuple of int, optional
|
|
446
|
+
Shape of the tensor to create
|
|
447
|
+
return_full_tensor : bool, optional
|
|
448
|
+
if true, the full tensor is returned as a (3,3,3,3) or a (...,3,3,3,3) array
|
|
449
|
+
mapping : str, optional
|
|
450
|
+
Mapping convention to use. Must be either Kelvin or Voigt.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
FourthOrderTensor or SymmetricTensor
|
|
455
|
+
"""
|
|
456
|
+
eye = np.eye(3)
|
|
457
|
+
if isinstance(shape, int):
|
|
458
|
+
shape = (shape,)
|
|
459
|
+
if len(shape):
|
|
460
|
+
for n in np.flip(shape):
|
|
461
|
+
eye = np.repeat(eye[np.newaxis,...], n, axis=0)
|
|
462
|
+
J = np.einsum('...ij,...kl->...ijkl',eye, eye)/3
|
|
463
|
+
if return_full_tensor:
|
|
464
|
+
return J
|
|
465
|
+
else:
|
|
466
|
+
return FourthOrderTensor(J, mapping=mapping)
|
|
467
|
+
|
|
468
|
+
@classmethod
|
|
469
|
+
def identity_deviatoric_part(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
|
|
470
|
+
"""
|
|
471
|
+
Return the deviatoric part of the identity tensor
|
|
472
|
+
|
|
473
|
+
Parameters
|
|
474
|
+
----------
|
|
475
|
+
shape : tuple of int, optional
|
|
476
|
+
Shape of the tensor to create
|
|
477
|
+
return_full_tensor : bool, optional
|
|
478
|
+
if true, the full tensor is returned as a (3,3,3,3) or a (...,3,3,3,3) array
|
|
479
|
+
mapping : str, optional
|
|
480
|
+
Mapping convention to use. Must be either Kelvin or Voigt.
|
|
481
|
+
|
|
482
|
+
Returns
|
|
483
|
+
-------
|
|
484
|
+
FourthOrderTensor or SymmetricTensor
|
|
485
|
+
"""
|
|
486
|
+
I = FourthOrderTensor.identity(shape, return_full_tensor, mapping)
|
|
487
|
+
J = FourthOrderTensor.identity_spherical_part(shape, return_full_tensor, mapping)
|
|
488
|
+
return I-J
|
|
489
|
+
|
|
490
|
+
def spherical_part(self):
|
|
491
|
+
"""
|
|
492
|
+
Return the spherical part of the tensor
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
FourthOrderTensor
|
|
497
|
+
Spherical part of the tensor
|
|
498
|
+
"""
|
|
499
|
+
I = self.identity_spherical_part(shape=self.shape)
|
|
500
|
+
return I.ddot(self)
|
|
501
|
+
|
|
502
|
+
def deviatoric_part(self):
|
|
503
|
+
"""
|
|
504
|
+
Return the deviatoric part of the tensor
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
FourthOrderTensor
|
|
509
|
+
Deviatoric part of the tensor
|
|
510
|
+
"""
|
|
511
|
+
K = self.identity_deviatoric_part(shape=self.shape)
|
|
512
|
+
return K.ddot(self)
|
|
513
|
+
|
|
455
514
|
def inv(self):
|
|
456
515
|
"""
|
|
457
516
|
Invert the tensor. The inverted tensors inherits the properties (if any)
|
|
@@ -461,10 +520,8 @@ class FourthOrderTensor:
|
|
|
461
520
|
FourthOrderTensor
|
|
462
521
|
Inverse tensor
|
|
463
522
|
"""
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
t2.matrix = new_matrix
|
|
467
|
-
return t2
|
|
523
|
+
matrix_inv = np.linalg.inv(self._matrix)
|
|
524
|
+
return self.__class__(matrix_inv, mapping=self.mapping.mapping_inverse)
|
|
468
525
|
|
|
469
526
|
@classmethod
|
|
470
527
|
def zeros(cls, shape=()):
|
|
@@ -486,6 +543,20 @@ class FourthOrderTensor:
|
|
|
486
543
|
zeros = np.zeros(shape)
|
|
487
544
|
return cls(zeros)
|
|
488
545
|
|
|
546
|
+
def matrix(self, mapping_convention=None):
|
|
547
|
+
matrix = self._matrix
|
|
548
|
+
if mapping_convention is None:
|
|
549
|
+
return matrix
|
|
550
|
+
else:
|
|
551
|
+
if isinstance(mapping_convention, str):
|
|
552
|
+
if mapping_convention.lower() == 'voigt':
|
|
553
|
+
mapping_convention = VoigtMapping()
|
|
554
|
+
elif mapping_convention.lower() == 'kelvin':
|
|
555
|
+
mapping_convention = KelvinMapping()
|
|
556
|
+
else:
|
|
557
|
+
raise ValueError('Mapping convention must be either Kelvin or Voigt')
|
|
558
|
+
return matrix / self.mapping._matrix * mapping_convention.matrix
|
|
559
|
+
|
|
489
560
|
class SymmetricFourthOrderTensor(FourthOrderTensor):
|
|
490
561
|
tensor_name = 'Symmetric 4th-order'
|
|
491
562
|
|
|
@@ -521,8 +592,8 @@ class SymmetricFourthOrderTensor(FourthOrderTensor):
|
|
|
521
592
|
"""
|
|
522
593
|
super().__init__(M, check_minor_symmetry=check_symmetries, force_minor_symmetry=force_symmetries, **kwargs)
|
|
523
594
|
if force_symmetries:
|
|
524
|
-
self.
|
|
525
|
-
elif check_symmetries and not np.all(np.isclose(self.
|
|
595
|
+
self._matrix = 0.5 * (self._matrix + self._matrix.swapaxes(-1, -2))
|
|
596
|
+
elif check_symmetries and not np.all(np.isclose(self._matrix, self._matrix.swapaxes(-1, -2))):
|
|
526
597
|
raise ValueError('The input matrix must be symmetric')
|
|
527
598
|
|
|
528
599
|
def invariants(self, order='all'):
|
|
@@ -535,7 +606,7 @@ class SymmetricFourthOrderTensor(FourthOrderTensor):
|
|
|
535
606
|
----------
|
|
536
607
|
order : str, optional
|
|
537
608
|
If 'linear', only A1 and A2 are returned
|
|
538
|
-
If 'quadratic', A1², A2², B1, B2, B3, B4 and B5 are returned
|
|
609
|
+
If 'quadratic', A1², A2², A1*A2, B1, B2, B3, B4 and B5 are returned
|
|
539
610
|
If 'all' (default), A1, A2, A1², A2², B1, B2, B3, B4 and B5 are returned
|
|
540
611
|
|
|
541
612
|
Returns
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
a = np.sqrt(2)
|
|
4
|
+
KELVIN_MAPPING_MATRIX = np.array([[1, 1, 1, a, a, a],
|
|
5
|
+
[1, 1, 1, a, a, a],
|
|
6
|
+
[1, 1, 1, a, a, a],
|
|
7
|
+
[a, a, a, 2, 2, 2],
|
|
8
|
+
[a, a, a, 2, 2, 2],
|
|
9
|
+
[a, a, a, 2, 2, 2], ])
|
|
10
|
+
|
|
11
|
+
VOIGT_MAPPING_MATRIX_COMPLIANCE = [[1, 1, 1, 2, 2, 2],
|
|
12
|
+
[1, 1, 1, 2, 2, 2],
|
|
13
|
+
[1, 1, 1, 2, 2, 2],
|
|
14
|
+
[2, 2, 2, 4, 4, 4],
|
|
15
|
+
[2, 2, 2, 4, 4, 4],
|
|
16
|
+
[2, 2, 2, 4, 4, 4]]
|
|
17
|
+
|
|
18
|
+
class MappingConvention:
|
|
19
|
+
matrix = np.array(KELVIN_MAPPING_MATRIX)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def mapping_inverse(self):
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
class KelvinMapping(MappingConvention):
|
|
26
|
+
name = 'Kelvin'
|
|
27
|
+
|
|
28
|
+
class VoigtMapping(MappingConvention):
|
|
29
|
+
name = 'Voigt'
|
|
30
|
+
|
|
31
|
+
def __init__(self, tensor='Stiffness'):
|
|
32
|
+
if tensor == 'Stiffness':
|
|
33
|
+
self.matrix = np.ones((6,6))
|
|
34
|
+
self.tensor_type = 'Stiffness'
|
|
35
|
+
else:
|
|
36
|
+
self.matrix = np.array(VOIGT_MAPPING_MATRIX_COMPLIANCE)
|
|
37
|
+
self.tensor_type = 'Compliance'
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def mapping_inverse(self):
|
|
41
|
+
if self.tensor_type == 'Stiffness':
|
|
42
|
+
return VoigtMapping(tensor='Compliance')
|
|
43
|
+
else:
|
|
44
|
+
return VoigtMapping(tensor='Stiffness')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import warnings
|
|
2
|
-
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from scipy.spatial.transform import Rotation
|
|
@@ -75,6 +75,15 @@ def _map(matrix, mapping_convention):
|
|
|
75
75
|
array[...,i] = matrix[...,j,k]
|
|
76
76
|
return array * mapping_convention
|
|
77
77
|
|
|
78
|
+
def filldraw_circle(ax, center, radius, color, fill=False, alpha=1.):
|
|
79
|
+
theta = np.linspace(0, 2 * np.pi, 500)
|
|
80
|
+
x = center[0] + radius * np.cos(theta)
|
|
81
|
+
y = center[1] + radius * np.sin(theta)
|
|
82
|
+
if fill:
|
|
83
|
+
ax.fill(x, y, color=color, alpha=alpha)
|
|
84
|
+
else:
|
|
85
|
+
ax.plot(x, y, color=color)
|
|
86
|
+
|
|
78
87
|
kelvin_mapping = [1, 1, 1, np.sqrt(2), np.sqrt(2), np.sqrt(2)]
|
|
79
88
|
|
|
80
89
|
class SecondOrderTensor:
|
|
@@ -1380,6 +1389,53 @@ class SecondOrderTensor:
|
|
|
1380
1389
|
else:
|
|
1381
1390
|
return Constructor(self.matrix)
|
|
1382
1391
|
|
|
1392
|
+
@classmethod
|
|
1393
|
+
def stack(cls, arrays, axis=0):
|
|
1394
|
+
"""
|
|
1395
|
+
Stack tensor arrays along the specified axis.
|
|
1396
|
+
|
|
1397
|
+
Parameters
|
|
1398
|
+
----------
|
|
1399
|
+
arrays : list of SecondOrderTensor or tuple of SecondOrderTensor
|
|
1400
|
+
List of tensor to stack together
|
|
1401
|
+
axis : int, optional
|
|
1402
|
+
Axis along which to stack
|
|
1403
|
+
|
|
1404
|
+
Returns
|
|
1405
|
+
-------
|
|
1406
|
+
SecondOrderTensor
|
|
1407
|
+
Stacked tensor array
|
|
1408
|
+
|
|
1409
|
+
Examples
|
|
1410
|
+
--------
|
|
1411
|
+
>>> from Elasticipy.tensors.second_order import SecondOrderTensor
|
|
1412
|
+
>>> import numpy as np
|
|
1413
|
+
>>> a = SecondOrderTensor.rand(shape=3)
|
|
1414
|
+
>>> b = SecondOrderTensor.rand(shape=3)
|
|
1415
|
+
>>> c = SecondOrderTensor.stack((a, b))
|
|
1416
|
+
>>> c.shape
|
|
1417
|
+
(2, 3)
|
|
1418
|
+
>>> np.all(c[0] == a)
|
|
1419
|
+
True
|
|
1420
|
+
>>> np.all(c[1] == b)
|
|
1421
|
+
True
|
|
1422
|
+
|
|
1423
|
+
>>> a = SecondOrderTensor.rand(shape=(3, 4))
|
|
1424
|
+
>>> b = SecondOrderTensor.rand(shape=(3, 4))
|
|
1425
|
+
>>> c = SecondOrderTensor.stack((a, b), axis=1)
|
|
1426
|
+
>>> c.shape
|
|
1427
|
+
(3, 2, 4)
|
|
1428
|
+
>>> np.all(c[:,0,:] == a)
|
|
1429
|
+
True
|
|
1430
|
+
>>> np.all(c[:,1,:] == b)
|
|
1431
|
+
True
|
|
1432
|
+
"""
|
|
1433
|
+
mat_array = [a.matrix for a in arrays]
|
|
1434
|
+
if axis<0:
|
|
1435
|
+
axis = axis - 2
|
|
1436
|
+
mat_stacked = np.stack(mat_array, axis=axis)
|
|
1437
|
+
return cls(mat_stacked)
|
|
1438
|
+
|
|
1383
1439
|
|
|
1384
1440
|
class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
1385
1441
|
voigt_map = [1, 1, 1, 1, 1, 1]
|
|
@@ -1440,7 +1496,7 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
|
1440
1496
|
'matrices.')
|
|
1441
1497
|
|
|
1442
1498
|
@classmethod
|
|
1443
|
-
def from_Voigt(cls, array):
|
|
1499
|
+
def from_Voigt(cls, array, voigt_map=None):
|
|
1444
1500
|
"""
|
|
1445
1501
|
Construct a SymmetricSecondOrderTensor from a Voigt vector, or slices of Voigt vectors.
|
|
1446
1502
|
|
|
@@ -1452,6 +1508,10 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
|
1452
1508
|
array : np.ndarray or list
|
|
1453
1509
|
array to build the SymmetricSecondOrderTensor from. We must have array.ndim>0 and array.shape[-1]==6.
|
|
1454
1510
|
|
|
1511
|
+
voigt_map : list or tuple, optional
|
|
1512
|
+
6-lenght list of factors to use for mapping. If None (default), the default Voigt map of the constructor is
|
|
1513
|
+
used.
|
|
1514
|
+
|
|
1455
1515
|
Returns
|
|
1456
1516
|
-------
|
|
1457
1517
|
SymmetricSecondOrderTensor
|
|
@@ -1469,7 +1529,9 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
|
1469
1529
|
[12. 22. 23.]
|
|
1470
1530
|
[13. 23. 33.]]
|
|
1471
1531
|
"""
|
|
1472
|
-
|
|
1532
|
+
if voigt_map is None:
|
|
1533
|
+
voigt_map = cls.voigt_map
|
|
1534
|
+
matrix = _unmap(array, voigt_map)
|
|
1473
1535
|
return cls(matrix)
|
|
1474
1536
|
|
|
1475
1537
|
def to_Voigt(self):
|
|
@@ -1556,6 +1618,48 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
|
|
|
1556
1618
|
eigvals = np.linalg.eigvalsh(self.matrix)
|
|
1557
1619
|
return np.flip(eigvals,axis=-1)
|
|
1558
1620
|
|
|
1621
|
+
def draw_Mohr_circles(self):
|
|
1622
|
+
"""
|
|
1623
|
+
Draw the Mohr circles of the symmetric second-order tensor
|
|
1624
|
+
|
|
1625
|
+
Given a tensor, the Mohr circles are meant to visually illustrate the possible shear components one can find
|
|
1626
|
+
when randomly rotating the tensor. See `here <https://en.wikipedia.org/wiki/Mohr%27s_circle>`_ for details.
|
|
1627
|
+
|
|
1628
|
+
Returns
|
|
1629
|
+
-------
|
|
1630
|
+
fig : matplotlib.figure.Figure
|
|
1631
|
+
The Matplotlib figure containing the plot.
|
|
1632
|
+
ax : matplotlib.axes._axes.Axes
|
|
1633
|
+
The Matplotlib axes of the plot.
|
|
1634
|
+
"""
|
|
1635
|
+
c,b,a = self.eigvals()
|
|
1636
|
+
|
|
1637
|
+
# Sizes and locations of circles
|
|
1638
|
+
r1 = (c - b) / 2
|
|
1639
|
+
r2 = (b - a) / 2
|
|
1640
|
+
r3 = (c - a) / 2
|
|
1641
|
+
center1 = ((b + c) /2, 0)
|
|
1642
|
+
center2 = ((a + b) /2, 0)
|
|
1643
|
+
center3 = ((a + c) /2, 0)
|
|
1644
|
+
|
|
1645
|
+
fig, ax = plt.subplots()
|
|
1646
|
+
filldraw_circle(ax, center1, r1, 'skyblue')
|
|
1647
|
+
filldraw_circle(ax, center2, r2, 'lightgreen')
|
|
1648
|
+
filldraw_circle(ax, center3, r3, 'red')
|
|
1649
|
+
filldraw_circle(ax, center3, r3, 'red', fill=True, alpha=0.2)
|
|
1650
|
+
filldraw_circle(ax, center1, r1, 'white', fill=True)
|
|
1651
|
+
filldraw_circle(ax, center2, r2, 'white', fill=True)
|
|
1652
|
+
ax.set_aspect('equal')
|
|
1653
|
+
ax.set_xlabel(f"Normal")
|
|
1654
|
+
ax.set_ylabel(f"Shear")
|
|
1655
|
+
ax.grid(True)
|
|
1656
|
+
xticks = (a,b,c, center1[0], center2[0])
|
|
1657
|
+
ax.set_xticks(np.unique(xticks))
|
|
1658
|
+
yticks = (-r3, -r2, -r1, 0, r1, r2, r3)
|
|
1659
|
+
ax.set_yticks(np.unique(yticks))
|
|
1660
|
+
|
|
1661
|
+
return fig, ax
|
|
1662
|
+
|
|
1559
1663
|
|
|
1560
1664
|
class SkewSymmetricSecondOrderTensor(SecondOrderTensor):
|
|
1561
1665
|
name = 'Skew-symmetric second-order tensor'
|
|
@@ -38,7 +38,7 @@ class StrainTensor(SymmetricSecondOrderTensor):
|
|
|
38
38
|
"""von Mises equivalent strain"""
|
|
39
39
|
return np.sqrt(2/3 * self.ddot(self))
|
|
40
40
|
|
|
41
|
-
def elastic_energy(self, stress):
|
|
41
|
+
def elastic_energy(self, stress, mode='pair'):
|
|
42
42
|
"""
|
|
43
43
|
Compute the elastic energy.
|
|
44
44
|
|
|
@@ -46,12 +46,22 @@ class StrainTensor(SymmetricSecondOrderTensor):
|
|
|
46
46
|
----------
|
|
47
47
|
stress : StressTensor
|
|
48
48
|
Corresponding stress tensor
|
|
49
|
+
mode : str, optional
|
|
50
|
+
If 'pair' (default), the elastic energies are computed element-wise. Broadcasting rule applies.
|
|
51
|
+
If 'cross', each cross-combination of stress and strain are considered.
|
|
49
52
|
|
|
50
53
|
Returns
|
|
51
54
|
-------
|
|
52
|
-
|
|
55
|
+
float or numpy.ndarray
|
|
56
|
+
Volumetric elastic energy
|
|
53
57
|
"""
|
|
54
|
-
return 0.5 * self.ddot(stress)
|
|
58
|
+
return 0.5 * self.ddot(stress, mode=mode)
|
|
59
|
+
|
|
60
|
+
def draw_Mohr_circles(self):
|
|
61
|
+
fig, ax = super().draw_Mohr_circles()
|
|
62
|
+
ax.set_xlabel(ax.get_xlabel() + ' strain')
|
|
63
|
+
ax.set_ylabel(ax.get_ylabel() + ' strain')
|
|
64
|
+
return fig, ax
|
|
55
65
|
|
|
56
66
|
|
|
57
67
|
class StressTensor(SymmetricSecondOrderTensor):
|
|
@@ -135,4 +145,10 @@ class StressTensor(SymmetricSecondOrderTensor):
|
|
|
135
145
|
numpy.ndarray
|
|
136
146
|
Volumetric elastic energy
|
|
137
147
|
"""
|
|
138
|
-
return 0.5 * self.ddot(strain, mode=mode)
|
|
148
|
+
return 0.5 * self.ddot(strain, mode=mode)
|
|
149
|
+
|
|
150
|
+
def draw_Mohr_circles(self):
|
|
151
|
+
fig, ax = super().draw_Mohr_circles()
|
|
152
|
+
ax.set_xlabel(ax.get_xlabel() + ' stress')
|
|
153
|
+
ax.set_ylabel(ax.get_ylabel() + ' stress')
|
|
154
|
+
return fig, ax
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: elasticipy
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: A Python library for elasticity tensor computations
|
|
5
5
|
Author-email: Dorian Depriester <dorian.dep@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -15,18 +15,20 @@ Classifier: Programming Language :: Python
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Requires-Python: >=3.
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: scipy
|
|
23
23
|
Requires-Dist: matplotlib
|
|
24
|
+
Requires-Dist: qtpy
|
|
25
|
+
Requires-Dist: pandas
|
|
24
26
|
Provides-Extra: dev
|
|
25
27
|
Requires-Dist: pytest; extra == "dev"
|
|
26
28
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
27
29
|
Requires-Dist: pymatgen; extra == "dev"
|
|
28
30
|
Requires-Dist: orix; extra == "dev"
|
|
29
|
-
Requires-Dist: mp_api; extra == "dev"
|
|
31
|
+
Requires-Dist: mp_api==0.45.9; extra == "dev"
|
|
30
32
|
Dynamic: license-file
|
|
31
33
|
|
|
32
34
|
[](https://pypi.org/project/elasticipy/)
|
|
@@ -48,7 +50,7 @@ Among other features, this package implements:
|
|
|
48
50
|
|
|
49
51
|
- Computation of elasticity tensors,
|
|
50
52
|
- Analysis of elastic anisotropy and wave propagation,
|
|
51
|
-
- Working with multidimensional arrays of
|
|
53
|
+
- Working with multidimensional arrays of tensors,
|
|
52
54
|
- Thermal expansion tensors,
|
|
53
55
|
- Rotation of tensors,
|
|
54
56
|
- Integration with crystal symmetry groups,
|
|
@@ -79,3 +81,19 @@ Certain parts of the code, particularly those related to graphical user interfac
|
|
|
79
81
|
**excluded from code coverage analysis**. This includes the following file:
|
|
80
82
|
|
|
81
83
|
- **`src/Elasticipy/gui.py`**
|
|
84
|
+
|
|
85
|
+
## Cite this package
|
|
86
|
+
If you use Elasticipy, please cite [](https://doi.org/10.5281/zenodo.14501849)
|
|
87
|
+
|
|
88
|
+
You can use the following BibTeX entry:
|
|
89
|
+
````bibtex
|
|
90
|
+
@software{Elasticipy,
|
|
91
|
+
author = {Depriester, Dorian},
|
|
92
|
+
doi = {10.5281/zenodo.15188346},
|
|
93
|
+
month = apr,
|
|
94
|
+
title = {{Elasticipy}},
|
|
95
|
+
url = {https://github.com/DorianDepriester/Elasticipy},
|
|
96
|
+
version = {4.0.0},
|
|
97
|
+
year = {2025}
|
|
98
|
+
}
|
|
99
|
+
````
|