elasticipy 2.9.0__py3-none-any.whl → 4.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.
@@ -0,0 +1,591 @@
1
+ import numpy as np
2
+ from Elasticipy.tensors.second_order import SymmetricSecondOrderTensor, rotation_to_matrix, is_orix_rotation, \
3
+ SecondOrderTensor, ALPHABET
4
+ from scipy.spatial.transform import Rotation
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], ])
14
+
15
+
16
+ def voigt_indices(i, j):
17
+ """
18
+ Translate the two-index notation to one-index notation
19
+
20
+ Parameters
21
+ ----------
22
+ i : int or np.ndarray
23
+ First index
24
+ j : int or np.ndarray
25
+ Second index
26
+
27
+ Returns
28
+ -------
29
+ Index in the vector of length 6
30
+ """
31
+ voigt_mat = np.array([[0, 5, 4],
32
+ [5, 1, 3],
33
+ [4, 3, 2]])
34
+ return voigt_mat[i, j]
35
+
36
+
37
+ def unvoigt_index(i):
38
+ """
39
+ Translate the one-index notation to two-index notation
40
+
41
+ Parameters
42
+ ----------
43
+ i : int or np.ndarray
44
+ Index to translate
45
+ """
46
+ inverse_voigt_mat = np.array([[0, 0],
47
+ [1, 1],
48
+ [2, 2],
49
+ [1, 2],
50
+ [0, 2],
51
+ [0, 1]])
52
+ return inverse_voigt_mat[i]
53
+
54
+ def rotate_tensor(full_tensor, r):
55
+ """
56
+ Rotate a (full) fourth-order tensor.
57
+
58
+ Parameters
59
+ ----------
60
+ full_tensor : numpy.ndarray
61
+ array of shape (3,3,3,3) or (...,3,3,3,3) containing all the components
62
+ r : scipy.spatial.Rotation or orix.quaternion.Rotation
63
+ Rotation, or set of rotations, to apply
64
+
65
+ Returns
66
+ -------
67
+ numpy.ndarray
68
+ Rotated tensor. If r is an array, the corresponding axes will be added as first axes in the result array.
69
+ """
70
+ rot_mat = rotation_to_matrix(r)
71
+ str_ein = '...im,...jn,...ko,...lp,...mnop->...ijkl'
72
+ return np.einsum(str_ein, rot_mat, rot_mat, rot_mat, rot_mat, full_tensor)
73
+
74
+ class FourthOrderTensor:
75
+ """
76
+ Template class for manipulating symmetric fourth-order tensors.
77
+
78
+ Attributes
79
+ ----------
80
+ matrix : np.ndarray
81
+ (6,6) matrix gathering all the components of the tensor, using the Voigt notation.
82
+ """
83
+ tensor_name = '4th-order'
84
+
85
+ def __init__(self, M, mapping='Kelvin', check_minor_symmetry=True, force_minor_symmetry=False):
86
+ """
87
+ Construct of Fourth-order tensor with minor symmetry.
88
+
89
+ Parameters
90
+ ----------
91
+ M : np.ndarray
92
+ (6,6) matrix corresponding to the stiffness tensor, written using the Voigt notation, or array of shape
93
+ (3,3,3,3).
94
+ mapping : str or list of list, or numpy/ndarray, optional
95
+ Mapping convention to translate the (3,3,3,3) array to (6,6) matrix
96
+ check_minor_symmetry : bool, optional
97
+ If true (default), check that the input array have minor symmetries (see Notes). Only used if an array of
98
+ shape (...,3,3,3,3) is passed.
99
+ force_minor_symmetry :
100
+ Ensure that the tensor displays minor symmetry.
101
+
102
+ Notes
103
+ -----
104
+ The minor symmetry is defined so that:
105
+
106
+ .. math::
107
+
108
+ M_{ijkl}=M_{jikl}=M_{jilk}=M_{ijlk}
109
+
110
+ """
111
+ if isinstance(mapping, (list, tuple, np.ndarray)):
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
+
124
+ M = np.asarray(M)
125
+ if M.shape[-2:] == (6, 6):
126
+ matrix = M
127
+ elif M.shape[-4:] == (3, 3, 3, 3):
128
+ Mijlk = np.swapaxes(M, -1, -2)
129
+ Mjikl = np.swapaxes(M, -3, -4)
130
+ Mjilk = np.swapaxes(Mjikl, -1, -2)
131
+ if force_minor_symmetry:
132
+ M = 0.25 * (M + Mijlk + Mjikl + Mjilk)
133
+ elif check_minor_symmetry:
134
+ symmetry = np.all(M == Mijlk) and np.all(M == Mjikl) and np.all(M == Mjilk)
135
+ if not symmetry:
136
+ raise ValueError('The input array does not have minor symmetry')
137
+ matrix = self._full_to_matrix(M)
138
+ else:
139
+ raise ValueError('The input matrix must of shape (...,6,6) or (...,3,3,3,3)')
140
+ self.matrix = matrix
141
+ for i in range(0, 6):
142
+ for j in range(0, 6):
143
+ def getter(obj, I=i, J=j):
144
+ return obj.matrix[...,I, J]
145
+
146
+ getter.__doc__ = f"Returns the ({i + 1},{j + 1}) component of the {self.tensor_name} matrix."
147
+ component_name = 'C{}{}'.format(i + 1, j + 1)
148
+ setattr(self.__class__, component_name, property(getter)) # Dynamically create the property
149
+
150
+ def __repr__(self):
151
+ if (self.ndim == 0) or ((self.ndim==1) and self.shape[0]<5):
152
+ msg = '{} tensor (in {} mapping):\n'.format(self.tensor_name, self.mapping_name)
153
+ msg += self.matrix.__str__()
154
+ else:
155
+ msg = '{} tensor array of shape {}'.format(self.tensor_name, self.shape)
156
+ return msg
157
+
158
+ @property
159
+ def shape(self):
160
+ """
161
+ Return the shape of the tensor array
162
+ Returns
163
+ -------
164
+ tuple
165
+ Shape of the tensor array
166
+ """
167
+ *shape, _, _ = self.matrix.shape
168
+ return tuple(shape)
169
+
170
+ def full_tensor(self):
171
+ """
172
+ Returns the full (unvoigted) tensor, as a [3, 3, 3, 3] array
173
+
174
+ Returns
175
+ -------
176
+ np.ndarray
177
+ Full tensor (4-index notation)
178
+ """
179
+ i, j, k, ell = np.indices((3, 3, 3, 3))
180
+ ij = voigt_indices(i, j)
181
+ kl = voigt_indices(k, ell)
182
+ m = self.matrix[..., ij, kl] / self.mapping_matrix[ij, kl]
183
+ return m
184
+
185
+ def flatten(self):
186
+ """
187
+ Flatten the tensor
188
+
189
+ If the tensor array is of shape (m,n,o...,r), the flattened array will be of shape (m*n*o*...*r,).
190
+
191
+ Returns
192
+ -------
193
+ SymmetricFourthOrderTensor
194
+ Flattened tensor
195
+ """
196
+ shape = self.shape
197
+ if shape:
198
+ t2 = deepcopy(self)
199
+ p = (np.prod(self.shape), 6, 6)
200
+ t2.matrix = self.matrix.reshape(p)
201
+ return t2
202
+ else:
203
+ return self
204
+
205
+ def _full_to_matrix(self, full_tensor):
206
+ kl, ij = np.indices((6, 6))
207
+ i, j = unvoigt_index(ij).T
208
+ k, ell = unvoigt_index(kl).T
209
+ return full_tensor[..., i, j, k, ell] * self.mapping_matrix[ij, kl]
210
+
211
+ def rotate(self, rotation):
212
+ """
213
+ Apply a single rotation to a tensor, and return its component into the rotated frame.
214
+
215
+ Parameters
216
+ ----------
217
+ rotation : Rotation or orix.quaternion.rotation.Rotation
218
+ Rotation to apply
219
+
220
+ Returns
221
+ -------
222
+ SymmetricFourthOrderTensor
223
+ Rotated tensor
224
+ """
225
+ t2 = deepcopy(self)
226
+ rotated_tensor = rotate_tensor(self.full_tensor(), rotation)
227
+ t2.matrix = self._full_to_matrix(rotated_tensor)
228
+ return t2
229
+
230
+ @property
231
+ def ndim(self):
232
+ """
233
+ Returns the dimensionality of the tensor (number of dimensions in the orientation array)
234
+
235
+ Returns
236
+ -------
237
+ int
238
+ Number of dimensions
239
+ """
240
+ shape = self.shape
241
+ if shape:
242
+ return len(shape)
243
+ else:
244
+ return 0
245
+
246
+ def mean(self, axis=None):
247
+ """
248
+ Compute the mean value of the tensor T
249
+
250
+ Parameters
251
+ ----------
252
+ axis : int or list of int or tuple of int, optional
253
+ axis along which to compute the mean. If None, the mean is computed on the flattened tensor
254
+
255
+ Returns
256
+ -------
257
+ numpy.ndarray
258
+ If no axis is given, the result will be of shape (3,3,3,3).
259
+ Otherwise, if T.ndim=m, and len(axis)=n, the returned value will be of shape (...,3,3,3,3), with ndim=m-n+4
260
+ """
261
+ t2 = deepcopy(self)
262
+ if axis is None:
263
+ axis = tuple([i for i in range(0,self.ndim)])
264
+ t2.matrix = np.mean(self.matrix, axis=axis)
265
+ return t2
266
+
267
+ def __add__(self, other):
268
+ if isinstance(other, np.ndarray):
269
+ if other.shape == (6, 6):
270
+ mat = self.matrix + other
271
+ elif other.shape == (3, 3, 3, 3):
272
+ mat = self._full_to_matrix(self.full_tensor() + other)
273
+ else:
274
+ raise ValueError('The input argument must be either a 6x6 matrix or a (3,3,3,3) array.')
275
+ elif isinstance(other, FourthOrderTensor):
276
+ if type(other) == type(self):
277
+ mat = self.full_tensor() + other.full_tensor()
278
+ else:
279
+ raise ValueError('The two tensors to add must be of the same class.')
280
+ else:
281
+ raise ValueError('I don''t know how to add {} with {}.'.format(type(self), type(other)))
282
+ return self.__class__(mat, mapping=self.mapping_matrix)
283
+
284
+ def __sub__(self, other):
285
+ if isinstance(other, FourthOrderTensor):
286
+ return self.__add__(-other.matrix)
287
+ else:
288
+ return self.__add__(-other)
289
+
290
+ def __neg__(self):
291
+ t = deepcopy(self)
292
+ t.matrix = -t.matrix
293
+ return t
294
+
295
+ def ddot(self, other, mode='pair'):
296
+ """
297
+ Perform tensor product contracted twice (":") between two fourth-order tensors
298
+
299
+ Parameters
300
+ ----------
301
+ other : FourthOrderTensor or SecondOrderTensor
302
+ Right-hand side of ":" symbol
303
+ mode : str, optional
304
+ If mode=="pair", the tensors must be broadcastable, and the tensor product are performed on the last axes.
305
+ If mode=="cross", all cross-combinations are considered.
306
+
307
+ Returns
308
+ -------
309
+ FourthOrderTensor or numpy.ndarray
310
+ If both the tensors are 0D (no orientation), the return value will be of type SymmetricTensor
311
+ Otherwise, the return value will be the full tensor, of shape (...,3,3,3,3).
312
+ """
313
+ if isinstance(other, FourthOrderTensor):
314
+ if self.ndim == 0 and other.ndim == 0:
315
+ return FourthOrderTensor(np.einsum('ijmn,nmkl->ijkl', self.full_tensor(), other.full_tensor()))
316
+ else:
317
+ if mode == 'pair':
318
+ ein_str = '...ijmn,...nmkl->...ijkl'
319
+ else:
320
+ ndim_0 = self.ndim
321
+ ndim_1 = other.ndim
322
+ indices_0 = ALPHABET[:ndim_0]
323
+ indices_1 = ALPHABET[:ndim_1].upper()
324
+ indices_2 = indices_0 + indices_1
325
+ ein_str = indices_0 + 'wxXY,' + indices_1 + 'YXyz->' + indices_2 + 'wxyz'
326
+ matrix = np.einsum(ein_str, self.full_tensor(), other.full_tensor())
327
+ return FourthOrderTensor(matrix)
328
+ elif isinstance(other, SecondOrderTensor):
329
+ if self.ndim == 0 and other.ndim == 0:
330
+ return SymmetricSecondOrderTensor(np.einsum('ijkl,kl->ij', self.full_tensor(), other.matrix))
331
+ else:
332
+ if mode == 'pair':
333
+ ein_str = '...ijkl,...kl->...ij'
334
+ else:
335
+ ndim_0 = self.ndim
336
+ ndim_1 = other.ndim
337
+ indices_0 = ALPHABET[:ndim_0]
338
+ indices_1 = ALPHABET[:ndim_1].upper()
339
+ indices_2 = indices_0 + indices_1
340
+ ein_str = indices_0 + 'wxXY,' + indices_1 + 'XY->' + indices_2 + 'wx'
341
+ matrix = np.einsum(ein_str, self.full_tensor(), other.matrix)
342
+ return SecondOrderTensor(matrix)
343
+
344
+
345
+ def __mul__(self, other):
346
+ if isinstance(other, (FourthOrderTensor, SecondOrderTensor)):
347
+ return self.ddot(other)
348
+ elif isinstance(other, np.ndarray):
349
+ shape = other.shape
350
+ if other.shape == self.shape[-len(shape):]:
351
+ matrix = self.matrix * other[...,np.newaxis, np.newaxis]
352
+ return self.__class__(matrix)
353
+ else:
354
+ raise ValueError('The arrays to multiply could not be broadcasted with shapes {} and {}'.format(self.shape, other.shape[:-2]))
355
+ elif isinstance(other, Rotation) or is_orix_rotation(other):
356
+ return self.rotate(other)
357
+ else:
358
+ return self.__class__(self.matrix * other)
359
+
360
+ def __truediv__(self, other):
361
+ if isinstance(other, (SecondOrderTensor, FourthOrderTensor)):
362
+ return self * other.inv()
363
+ else:
364
+ return self * (1 / other)
365
+
366
+
367
+ def transpose_array(self):
368
+ """
369
+ Transpose the orientations of the tensor array
370
+
371
+ Returns
372
+ -------
373
+ FourthOrderTensor
374
+ The same tensor, but with transposed axes
375
+ """
376
+ ndim = self.ndim
377
+ if ndim==0 or ndim==1:
378
+ return self
379
+ else:
380
+ new_axes = tuple(range(ndim))[::-1] + (ndim, ndim + 1)
381
+ transposed_matrix = self.matrix.transpose(new_axes)
382
+ return self.__class__(transposed_matrix)
383
+
384
+ def __rmul__(self, other):
385
+ if isinstance(other, (Rotation, float, int, np.number)) or is_orix_rotation(other):
386
+ return self * other
387
+ else:
388
+ raise NotImplementedError('A fourth order tensor can be left-multiplied by rotations or scalar only.')
389
+
390
+ def __eq__(self, other):
391
+ if isinstance(other, FourthOrderTensor):
392
+ return np.all(self.matrix == other.matrix, axis=(-1,-2))
393
+ elif isinstance(other, (float, int)) or (isinstance(other, np.ndarray) and other.shape[-2:] == (6, 6)):
394
+ return np.all(self.matrix == other, axis=(-1,-2))
395
+ else:
396
+ raise NotImplementedError('The element to compare with must be a fourth-order tensor '
397
+ 'or an array of shape (6,6).')
398
+
399
+ def __getitem__(self, item):
400
+ if self.ndim:
401
+ sub_mat= self.matrix[item]
402
+ if sub_mat.shape[-2:] != (6,6):
403
+ raise IndexError('Too many indices for tensor array: array is {}-dimensional, but {} were provided'.format(self.ndim, len(item)))
404
+ else:
405
+ return self.__class__(sub_mat)
406
+ else:
407
+ raise IndexError('A single tensor cannot be subindexed')
408
+
409
+ def __setitem__(self, index, value):
410
+ if isinstance(value, np.ndarray):
411
+ if value.shape[-2:] == (6,6):
412
+ self.matrix[index] = value
413
+ elif value.shape[-4:] == (3,3,3,3):
414
+ submatrix = self._full_to_matrix(value)
415
+ self.matrix[index] = submatrix
416
+ else:
417
+ return ValueError('The R.h.s must be either of shape (...,6,6) or (...,3,3,3,3)')
418
+ elif isinstance(value, FourthOrderTensor):
419
+ self.matrix[index] = value.matrix / value.mapping_matrix * self.mapping_matrix
420
+ else:
421
+ raise NotImplementedError('The r.h.s must be either an ndarray or an object of class {}'.format(self.__class__))
422
+
423
+ @classmethod
424
+ def identity(cls, shape=(), return_full_tensor=False, mapping='Kelvin'):
425
+ """
426
+ Create a 4th-order identity tensor
427
+
428
+ Parameters
429
+ ----------
430
+ shape : int or tuple, optional
431
+ Shape of the tensor to create
432
+ return_full_tensor : bool, optional
433
+ 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
+ as a SymmetricTensor object.
435
+
436
+ Returns
437
+ -------
438
+ numpy.ndarray or SymmetricTensor
439
+ Identity tensor
440
+ """
441
+ eye = np.eye(3)
442
+ if isinstance(shape, int):
443
+ shape = (shape,)
444
+ if len(shape):
445
+ for n in np.flip(shape):
446
+ eye = np.repeat(eye[np.newaxis,...], n, axis=0)
447
+ a = np.einsum('...ik,...jl->...ijkl', eye, eye)
448
+ b = np.einsum('...il,...jk->...ijkl', eye, eye)
449
+ full = 0.5*(a + b)
450
+ if return_full_tensor:
451
+ return full
452
+ else:
453
+ return cls(full, mapping=mapping)
454
+
455
+ def inv(self):
456
+ """
457
+ Invert the tensor. The inverted tensors inherits the properties (if any)
458
+
459
+ Returns
460
+ -------
461
+ FourthOrderTensor
462
+ Inverse tensor
463
+ """
464
+ t2 = deepcopy(self)
465
+ new_matrix = np.linalg.inv(self.matrix)
466
+ t2.matrix = new_matrix
467
+ return t2
468
+
469
+ @classmethod
470
+ def zeros(cls, shape=()):
471
+ """
472
+ Create a fourth-order tensor populated with zeros
473
+
474
+ Parameters
475
+ ----------
476
+ shape : int or tuple, optional
477
+ Shape of the tensor to create
478
+ Returns
479
+ -------
480
+ FourthOrderTensor
481
+ """
482
+ if isinstance(shape, int):
483
+ shape = (shape, 6, 6)
484
+ else:
485
+ shape = shape + (6,6)
486
+ zeros = np.zeros(shape)
487
+ return cls(zeros)
488
+
489
+ class SymmetricFourthOrderTensor(FourthOrderTensor):
490
+ tensor_name = 'Symmetric 4th-order'
491
+
492
+ def __init__(self, M, check_symmetries=True, force_symmetries=False, **kwargs):
493
+ """
494
+ Construct a fully symmetric fourth-order tensor from a (...,6,6) or a (3,3,3,3) array.
495
+
496
+ The input matrix must be symmetric, otherwise an error is thrown (except if check_symmetry==False, see below)
497
+
498
+ Parameters
499
+ ----------
500
+ M : np.ndarray
501
+ (6,6) matrix corresponding to the stiffness tensor, written using the Voigt notation, or array of shape
502
+ (3,3,3,3).
503
+ check_symmetries : bool, optional
504
+ Whether to check or not that the tensor to built displays both major and minor symmetries (see Notes).
505
+ force_symmetries : bool, optional
506
+ If true, ensure that the tensor displays both minor and major symmetries.
507
+
508
+ Notes
509
+ -----
510
+ The major symmetry is defined so that:
511
+
512
+ .. math::
513
+
514
+ M_{ijkl}=M_{klij}
515
+
516
+ whereas the minor symmetry is:
517
+
518
+ .. math::
519
+
520
+ M_{ijkl}=M_{jikl}=M_{jilk}=M_{ijlk}
521
+ """
522
+ super().__init__(M, check_minor_symmetry=check_symmetries, force_minor_symmetry=force_symmetries, **kwargs)
523
+ if force_symmetries:
524
+ self.matrix = 0.5*(self.matrix + self.matrix.swapaxes(-1,-2))
525
+ elif check_symmetries and not np.all(np.isclose(self.matrix, self.matrix.swapaxes(-1, -2))):
526
+ raise ValueError('The input matrix must be symmetric')
527
+
528
+ def invariants(self, order='all'):
529
+ """
530
+ Compute the invariants of the tensor.
531
+
532
+ Compute the linear or/and quadratic invariant of the fourth-order tensor (see notes)
533
+
534
+ Parameters
535
+ ----------
536
+ order : str, optional
537
+ If 'linear', only A1 and A2 are returned
538
+ If 'quadratic', A1², A2², B1, B2, B3, B4 and B5 are returned
539
+ If 'all' (default), A1, A2, A1², A2², B1, B2, B3, B4 and B5 are returned
540
+
541
+ Returns
542
+ -------
543
+ tuple
544
+ invariants of the given order (see above)
545
+
546
+ Notes
547
+ -----
548
+ The nomenclature of the invariants follows that of [4]_. The linear invariants are:
549
+
550
+ .. math::
551
+
552
+ A_1=C_{ijij}
553
+
554
+ A_2=C_{iijj}
555
+
556
+ whereas the quadratic invariants are:
557
+
558
+ .. math::
559
+
560
+ B_1 = C_{ijkl}C_{ijkl}
561
+
562
+ B_2 = C_{iikl}C_{jjkl}
563
+
564
+ B_3 = C_{iikl}C_{jkjl}
565
+
566
+ B_4 = C_{kiil}C_{kjjl}
567
+
568
+ B_5 = C_{ijkl}C_{ikjl}
569
+
570
+ References
571
+ ----------
572
+ .. [4] Norris, A. N. (22 May 2007). "Quadratic invariants of elastic moduli". The Quarterly Journal of Mechanics
573
+ and Applied Mathematics. 60 (3): 367–389. doi:10.1093/qjmam/hbm007
574
+ """
575
+ t = self.full_tensor()
576
+ order = order.lower()
577
+ A1 = np.einsum('...ijij->...',t)
578
+ A2 = np.einsum('...iijj->...',t)
579
+ lin_inv = (A1, A2)
580
+ if order == 'linear':
581
+ return lin_inv
582
+ B1 = np.einsum('...ijkl,...ijkl->...',t, t)
583
+ B2 = np.einsum('...iikl,...jjkl->...', t, t)
584
+ B3 = np.einsum('...iikl,...jkjl->...', t, t)
585
+ B4 = np.einsum('...kiil,...kjjl->...', t, t)
586
+ B5 = np.einsum('...ijkl,...ikjl->...', t, t)
587
+ quad_inv = (A1**2, A2**2, A1*A2, B1, B2, B3, B4, B5)
588
+ if order == 'quadratic':
589
+ return quad_inv
590
+ else:
591
+ return lin_inv + quad_inv