elasticipy 3.0.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.
@@ -1,1779 +1,16 @@
1
- import numpy as np
2
- import re
3
-
4
- from Elasticipy.SecondOrderTensor import SymmetricSecondOrderTensor, rotation_to_matrix, is_orix_rotation, \
5
- SecondOrderTensor, ALPHABET
6
- from Elasticipy.SecondOrderTensor import _orientation_shape, _is_single_rotation
7
- from Elasticipy.StressStrainTensors import StrainTensor, StressTensor
8
- from Elasticipy.SphericalFunction import SphericalFunction, HyperSphericalFunction
9
- from scipy.spatial.transform import Rotation
10
- from Elasticipy.CrystalSymmetries import SYMMETRIES
11
- from copy import deepcopy
12
-
13
- a = np.sqrt(2)
14
- _voigt_to_kelvin_matrix = np.array([[1, 1, 1, a, a, a],
15
- [1, 1, 1, a, a, a],
16
- [1, 1, 1, a, a, a],
17
- [a, a, a, 2, 2, 2],
18
- [a, a, a, 2, 2, 2],
19
- [a, a, a, 2, 2, 2],])
20
-
21
-
22
- def _parse_tensor_components(prefix, **kwargs):
23
- pattern = r'^{}(\d{{2}})$'.format(prefix)
24
- value = dict()
25
- for k, v in kwargs.items():
26
- match = re.match(pattern, k) # Extract 'C11' to '11' and so
27
- if match:
28
- value[match.group(1)] = v
29
- return value
30
-
31
-
32
- def _indices2str(ij):
33
- return f'{ij[0] + 1}{ij[1] + 1}'
34
-
35
-
36
- def voigt_indices(i, j):
37
- """
38
- Translate the two-index notation to one-index notation
39
-
40
- Parameters
41
- ----------
42
- i : int or np.ndarray
43
- First index
44
- j : int or np.ndarray
45
- Second index
46
-
47
- Returns
48
- -------
49
- Index in the vector of length 6
50
- """
51
- voigt_mat = np.array([[0, 5, 4],
52
- [5, 1, 3],
53
- [4, 3, 2]])
54
- return voigt_mat[i, j]
55
-
56
-
57
- def unvoigt_index(i):
58
- """
59
- Translate the one-index notation to two-index notation
60
-
61
- Parameters
62
- ----------
63
- i : int or np.ndarray
64
- Index to translate
65
- """
66
- inverse_voigt_mat = np.array([[0, 0],
67
- [1, 1],
68
- [2, 2],
69
- [1, 2],
70
- [0, 2],
71
- [0, 1]])
72
- return inverse_voigt_mat[i]
73
-
74
-
75
- def _compute_unit_strain_along_direction(S, m, n, direction='longitudinal'):
76
- if not isinstance(S, ComplianceTensor):
77
- S = S.inv()
78
- if direction == 'transverse':
79
- ein_str = 'ijkl,pi,pj,pk,pl->p'
80
- return np.einsum(ein_str, S.full_tensor(), m, m, n, n)
81
- elif direction == 'longitudinal':
82
- ein_str = 'ijkl,pi,pk,pj,pl->p'
83
- return np.einsum(ein_str, S.full_tensor(), m, m, n, n)
84
- else:
85
- ein_str = 'ijkk,pi,pj->p'
86
- return np.einsum(ein_str, S.full_tensor(), m, m)
87
-
88
-
89
- def _isotropic_matrix(C11, C12, C44):
90
- return np.array([[C11, C12, C12, 0, 0, 0],
91
- [C12, C11, C12, 0, 0, 0],
92
- [C12, C12, C11, 0, 0, 0],
93
- [0, 0, 0, C44, 0, 0],
94
- [0, 0, 0, 0, C44, 0],
95
- [0, 0, 0, 0, 0, C44]])
96
-
97
-
98
- def _check_definite_positive(mat):
99
- try:
100
- np.linalg.cholesky(mat)
101
- except np.linalg.LinAlgError:
102
- eigen_val = np.linalg.eigvals(mat)
103
- raise ValueError('The input matrix is not definite positive (eigenvalues: {})'.format(eigen_val))
104
-
105
-
106
- def _rotate_tensor(full_tensor, rotation):
107
- rot_mat = rotation_to_matrix(rotation)
108
- str_ein = '...im,...jn,...ko,...lp,mnop->...ijkl'
109
- return np.einsum(str_ein, rot_mat, rot_mat, rot_mat, rot_mat, full_tensor)
110
-
111
-
112
- class SymmetricTensor:
113
- """
114
- Template class for manipulating symmetric fourth-order tensors.
115
-
116
- Attributes
117
- ----------
118
- matrix : np.ndarray
119
- (6,6) matrix gathering all the components of the tensor, using the Voigt notation.
120
- symmetry : str
121
- Symmetry of the tensor
122
-
123
- """
124
- tensor_name = 'Symmetric'
125
- voigt_map = np.ones((6, 6))
126
- C11_C12_factor = 0.5
127
- C46_C56_factor = 1.0
128
- component_prefix = 'C'
129
-
130
- def __init__(self, M, phase_name=None, symmetry='Triclinic', orientations=None,
131
- check_symmetry=True, check_positive_definite=False):
132
- """
133
- Construct of stiffness tensor from a (6,6) matrix.
134
-
135
- The input matrix must be symmetric, otherwise an error is thrown (except if check_symmetry==False, see below)
136
-
137
- Parameters
138
- ----------
139
- M : np.ndarray
140
- (6,6) matrix corresponding to the stiffness tensor, written using the Voigt notation, or array of shape
141
- (3,3,3,3).
142
- phase_name : str, default None
143
- Name to display
144
- symmetry : str, default Triclinic
145
- Name of the crystal's symmetry
146
- check_symmetry : bool, optional
147
- Whether to check or not that the input matrix is symmetric.
148
- check_positive_definite : bool, optional
149
- Whether to check or not that the input matrix is definite positive
150
- """
151
- M = np.asarray(M)
152
- if M.shape == (6, 6):
153
- matrix = M
154
- elif M.shape == (3, 3, 3, 3):
155
- matrix = self._full_to_matrix(M)
156
- else:
157
- raise ValueError('The input matrix must of shape (6,6)')
158
- if check_symmetry and not np.all(np.isclose(matrix, matrix.T)):
159
- raise ValueError('The input matrix must be symmetric')
160
- if check_positive_definite:
161
- _check_definite_positive(matrix)
162
-
163
- self.matrix = matrix
164
- self.phase_name = phase_name
165
- self.symmetry = symmetry
166
- self.orientations = orientations
167
-
168
- for i in range(0, 6):
169
- for j in range(0, 6):
170
- def getter(obj, I=i, J=j):
171
- return obj.matrix[I, J]
172
-
173
- getter.__doc__ = f"Returns the ({i + 1},{j + 1}) component of the {self.tensor_name} matrix."
174
- component_name = 'C{}{}'.format(i + 1, j + 1)
175
- setattr(self.__class__, component_name, property(getter)) # Dynamically create the property
176
-
177
- def __repr__(self):
178
- if self.phase_name is None:
179
- heading = '{} tensor (in Voigt notation):\n'.format(self.tensor_name)
180
- else:
181
- heading = '{} tensor (in Voigt notation) for {}:\n'.format(self.tensor_name, self.phase_name)
182
- print_symmetry = '\nSymmetry: {}'.format(self.symmetry)
183
- msg = heading + self.matrix.__str__() + print_symmetry
184
- if self.orientations is not None:
185
- shape = self.shape
186
- if len(shape) == 1:
187
- msg = msg + '\n{} orientations'.format(shape[0])
188
- else:
189
- msg = msg + '\n{} orientations'.format(shape)
190
- return msg
191
-
192
- def __len__(self):
193
- o = self.orientations
194
- if o is None:
195
- return 1
196
- else:
197
- if is_orix_rotation(o):
198
- return o.shape[0]
199
- else:
200
- return len(o)
201
-
202
- @property
203
- def shape(self):
204
- """
205
- Return the shape of the tensor array
206
- Returns
207
- -------
208
- tuple
209
- Shape of the tensor array
210
- """
211
- o = self.orientations
212
- if o is None:
213
- return None
214
- else:
215
- return _orientation_shape(o)
216
-
217
- def full_tensor(self):
218
- """
219
- Returns the full (unvoigted) tensor, as a [3, 3, 3, 3] array
220
-
221
- Returns
222
- -------
223
- np.ndarray
224
- Full tensor (4-index notation)
225
- """
226
- i, j, k, ell = np.indices((3, 3, 3, 3))
227
- ij = voigt_indices(i, j)
228
- kl = voigt_indices(k, ell)
229
- m = self.matrix[ij, kl] / self.voigt_map[ij, kl]
230
- if self.orientations is None:
231
- return m
232
- else:
233
- return _rotate_tensor(m, self.orientations)
234
-
235
- def flatten(self):
236
- """
237
- Flatten the tensor
238
-
239
- If the tensor has (m,n,o...,r) orientations, the flattened tensor will have m*n*o*...*r orientations
240
-
241
- Returns
242
- -------
243
- SymmetricTensor
244
- Flattened tensor
245
- """
246
- tensor_flat = self._unrotate()
247
- o = self.orientations
248
- if is_orix_rotation(o):
249
- o_flat = o.flatten()
250
- else:
251
- o_flat = o
252
- tensor_flat.orientations = o_flat
253
- return tensor_flat
254
-
255
- @classmethod
256
- def _full_to_matrix(cls, full_tensor):
257
- ij, kl = np.indices((6, 6))
258
- i, j = unvoigt_index(ij).T
259
- k, ell = unvoigt_index(kl).T
260
- return full_tensor[i, j, k, ell] * cls.voigt_map[ij, kl]
261
-
262
- def rotate(self, rotation):
263
- """
264
- Apply a single rotation to a tensor, and return its component into the rotated frame.
265
-
266
- Parameters
267
- ----------
268
- rotation : Rotation or orix.quaternion.rotation.Rotation
269
- Rotation to apply
270
-
271
- Returns
272
- -------
273
- SymmetricTensor
274
- Rotated tensor
275
- """
276
- if _is_single_rotation(rotation):
277
- rotated_tensor = _rotate_tensor(self.full_tensor(), rotation)
278
- rotated_matrix = self._full_to_matrix(rotated_tensor)
279
- return self.__class__(rotated_matrix)
280
- else:
281
- raise ValueError('The rotation to apply must be single')
282
-
283
- @property
284
- def ndim(self):
285
- """
286
- Returns the dimensionality of the tensor (number of dimensions in the orientation array)
287
-
288
- Returns
289
- -------
290
- int
291
- Number of dimensions
292
- """
293
- shape = self.shape
294
- if shape:
295
- return len(shape)
296
- else:
297
- return 0
298
-
299
- def mean(self, axis=None):
300
- """
301
- Compute the mean value of the tensor T, considering the orientations
302
-
303
- Parameters
304
- ----------
305
- axis : int or list of int or tuple of int, optional
306
- axis along which to compute the mean. If None, the mean is computed on the flattened tensor
307
-
308
- Returns
309
- -------
310
- numpy.ndarray
311
- If no axis is given, the result will be of shape (3,3,3,3).
312
- 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
313
- """
314
- if axis is None:
315
- axis = tuple([i for i in range(self.ndim)])
316
- return np.mean(self.full_tensor(), axis=axis)
317
-
318
- def _unrotate(self):
319
- unrotated_tensor = deepcopy(self)
320
- unrotated_tensor.orientations = None
321
- return unrotated_tensor
322
-
323
- def __add__(self, other):
324
- if isinstance(other, np.ndarray):
325
- if other.shape == (6, 6):
326
- mat = self.matrix + other
327
- elif other.shape == (3, 3, 3, 3):
328
- mat = self._full_to_matrix(self.full_tensor() + other)
329
- else:
330
- raise ValueError('The input argument must be either a 6x6 matrix or a (3,3,3,3) array.')
331
- elif isinstance(other, SymmetricTensor):
332
- if type(other) == type(self):
333
- mat = self.matrix + other.matrix
334
- else:
335
- raise ValueError('The two tensors to add must be of the same class.')
336
- else:
337
- raise ValueError('I don''t know how to add {} with {}.'.format(type(self), type(other)))
338
- return self.__class__(mat)
339
-
340
- def __sub__(self, other):
341
- if isinstance(other, SymmetricTensor):
342
- return self.__add__(-other.matrix)
343
- else:
344
- return self.__add__(-other)
345
-
346
- def __mul__(self, other):
347
- if isinstance(other, SymmetricSecondOrderTensor):
348
- return SymmetricSecondOrderTensor(self * other.matrix)
349
- elif isinstance(other, np.ndarray):
350
- if other.shape == (3, 3):
351
- # other is a single tensor
352
- matrix = np.einsum('...ijkl,kl->...ij', self.full_tensor(), other)
353
- return SecondOrderTensor(matrix)
354
- elif self.shape is None:
355
- # other is an array, but self is single
356
- matrix = np.einsum('ijkl,...kl->...ij', self.full_tensor(), other)
357
- return SecondOrderTensor(matrix)
358
- elif (self.ndim >= (other.ndim - 2)) and self.shape[-other.ndim+2:]==other.shape[:-2]:
359
- # self.shape==(m,n,o,p) and other.shape==(o,p,3,3)
360
- indices_0 = ALPHABET[:self.ndim]
361
- indices_1 = indices_0[-other.ndim+2:]
362
- ein_str = indices_0 + 'IJKL,' + indices_1 + 'KL->' + indices_0 + 'IJ'
363
- matrix = np.einsum(ein_str, self.full_tensor(), other)
364
- return SecondOrderTensor(matrix)
365
- elif self.shape == other.shape[:-2]:
366
- # other and self are arrays of the same shape
367
- matrix = np.einsum('...ijkl,...kl->...ij', self.full_tensor(), other)
368
- return SecondOrderTensor(matrix)
369
- else:
370
- raise ValueError('The arrays to multiply could not be broadcast with shapes {} and {}'.format(self.shape, other.shape[:-2]))
371
- elif isinstance(other, Rotation) or is_orix_rotation(other):
372
- if _is_single_rotation(other):
373
- return self.rotate(other)
374
- else:
375
- return self.__class__(self.matrix, symmetry=self.symmetry, orientations=other,
376
- phase_name=self.phase_name)
377
- else:
378
- return self.__class__(self.matrix * other, symmetry=self.symmetry)
379
-
380
- def transpose_array(self):
381
- """
382
- Transpose the orientations of the tensor array
383
-
384
- Returns
385
- -------
386
- FourthOrderTensor
387
- The same tensor, but with transposed orientations
388
- """
389
- ndim = self.ndim
390
- if ndim==0 or ndim==1:
391
- return self
392
- else:
393
- new_tensor = self._unrotate()
394
- new_order = np.flip(range(ndim))
395
- new_tensor.orientations = self.orientations.transpose(*new_order)
396
- return new_tensor
397
-
398
- def __rmul__(self, other):
399
- if isinstance(other, (Rotation, float, int, np.number)) or is_orix_rotation(other):
400
- return self * other
401
- else:
402
- raise NotImplementedError('A fourth order tensor can be left-multiplied by rotations or scalar only.')
403
-
404
- def __truediv__(self, other):
405
- if isinstance(other, (float, int, np.number)):
406
- return self.__class__(self.matrix / other, symmetry=self.symmetry)
407
- else:
408
- raise NotImplementedError
409
-
410
- def __eq__(self, other):
411
- if isinstance(other, SymmetricTensor):
412
- return np.all(self.matrix == other.matrix) and np.all(self.orientations == other.orientations)
413
- elif isinstance(other, np.ndarray) and other.shape == (6, 6):
414
- return np.all(self.matrix == other)
415
- else:
416
- raise NotImplementedError('The element to compare with must be a fourth-order tensor '
417
- 'or an array of shape (6,6).')
418
-
419
- def matmul(self, other):
420
- if isinstance(other, SecondOrderTensor):
421
- other_matrix = other.matrix
422
- else:
423
- other_matrix = other
424
- indices_0= 'abcdefgh'
425
- indices_1= indices_0.upper()
426
- indices_0 = indices_0[:self.ndim]
427
- indices_1 = indices_1[:other_matrix.ndim-2]
428
- ein_str = indices_0 + 'ijkl,' + indices_1 +'kl->' + indices_0 + indices_1 + 'ij'
429
- new_mat = np.einsum(ein_str, self.full_tensor(), other_matrix)
430
- return StrainTensor(new_mat)
431
-
432
- @classmethod
433
- def _matrixFromCrystalSymmetry(cls, symmetry='Triclinic', point_group=None, diad='y', prefix=None, **kwargs):
434
- if prefix is None:
435
- prefix = cls.component_prefix
436
- values = _parse_tensor_components(prefix, **kwargs)
437
- C = np.zeros((6, 6))
438
- symmetry = symmetry.capitalize()
439
- if ((symmetry == 'tetragonal') or (symmetry == 'trigonal')) and (point_group is None):
440
- raise ValueError('For tetragonal and trigonal symmetries, the point group is mandatory.')
441
- tetra_1 = ['4', '-4', '4/m']
442
- tetra_2 = ['4mm', '-42m', '422', '4/mmm']
443
- trigo_1 = ['3', '-3']
444
- trigo_2 = ['32', '-3m', '3m']
445
- if point_group is not None:
446
- if (point_group in tetra_1) or (point_group in tetra_2):
447
- symmetry = 'Tetragonal'
448
- elif (point_group in trigo_1) or (point_group in trigo_2):
449
- symmetry = 'Trigonal'
450
- symmetry_description = SYMMETRIES[symmetry]
451
- if symmetry == 'Tetragonal':
452
- if point_group in tetra_1:
453
- symmetry_description = symmetry_description[', '.join(tetra_1)]
454
- else:
455
- symmetry_description = symmetry_description[', '.join(tetra_2)]
456
- elif symmetry == 'Trigonal':
457
- if point_group in trigo_1:
458
- symmetry_description = symmetry_description[', '.join(trigo_1)]
459
- else:
460
- symmetry_description = symmetry_description[', '.join(trigo_2)]
461
- elif symmetry == 'Monoclinic':
462
- symmetry_description = symmetry_description["Diad || " + diad]
463
- for required_field in symmetry_description.required:
464
- C[required_field] = values[_indices2str(required_field)]
465
-
466
- # Now apply relationships between components
467
- for equality in symmetry_description.equal:
468
- for index in equality[1]:
469
- C[index] = C[equality[0]]
470
- for opposite in symmetry_description.opposite:
471
- for index in opposite[1]:
472
- C[index] = -C[opposite[0]]
473
- C11_C12 = symmetry_description.C11_C12
474
- if C11_C12:
475
- for index in C11_C12:
476
- C[index] = (C[0, 0] - C[0, 1]) * cls.C11_C12_factor
477
-
478
- if symmetry == 'Trigonal':
479
- C[3, 5] = cls.C46_C56_factor * C[3, 5]
480
- C[4, 5] = cls.C46_C56_factor * C[4, 5]
481
-
482
- return C + np.tril(C.T, -1)
483
-
484
- @classmethod
485
- def fromCrystalSymmetry(cls, symmetry='Triclinic', point_group=None, diad='y', phase_name=None, prefix=None,
486
- **kwargs):
487
- """
488
- Create a fourth-order tensor from limited number of components, taking advantage of crystallographic symmetries
489
-
490
- Parameters
491
- ----------
492
- symmetry : str, default Triclinic
493
- Name of the crystallographic symmetry
494
- point_group : str
495
- Point group of the considered crystal. Only used (and mandatory) for tetragonal and trigonal symmetries.
496
- diad : str {'x', 'y'}, default 'x'
497
- Alignment convention. Sets whether x||a or y||b. Only used for monoclinic symmetry.
498
- phase_name : str, default None
499
- Name to use when printing the tensor
500
- prefix : str, default None
501
- Define the prefix to use when providing the components. By default, it is 'C' for stiffness tensors, 'S' for
502
- compliance.
503
- kwargs
504
- Keywords describing all the necessary components, depending on the crystal's symmetry and the type of tensor.
505
- For Stiffness, they should be named as 'Cij' (e.g. C11=..., C12=...).
506
- For Comliance, they should be named as 'Sij' (e.g. S11=..., S12=...).
507
- See examples below. The behaviour can be overriten with the prefix option (see above)
508
-
509
- Returns
510
- -------
511
- FourthOrderTensor
512
-
513
- See Also
514
- --------
515
- StiffnessTensor.isotropic : creates an isotropic stiffness tensor from two paremeters (e.g. E and v).
516
-
517
- Notes
518
- -----
519
- The relationships between the tensor's components depend on the crystallogrpahic symmetry [1]_.
520
-
521
- References
522
- ----------
523
- .. [1] Nye, J. F. Physical Properties of Crystals. London: Oxford University Press, 1959.
524
-
525
- Examples
526
- --------
527
- >>> from Elasticipy.FourthOrderTensor import StiffnessTensor\n
528
- >>> StiffnessTensor.fromCrystalSymmetry(symmetry='monoclinic', diad='y', phase_name='TiNi',
529
- ... C11=231, C12=127, C13=104,
530
- ... C22=240, C23=131, C33=175,
531
- ... C44=81, C55=11, C66=85,
532
- ... C15=-18, C25=1, C35=-3, C46=3)
533
- Stiffness tensor (in Voigt notation) for TiNi:
534
- [[231. 127. 104. 0. -18. 0.]
535
- [127. 240. 131. 0. 1. 0.]
536
- [104. 131. 175. 0. -3. 0.]
537
- [ 0. 0. 0. 81. 0. 3.]
538
- [-18. 1. -3. 0. 11. 0.]
539
- [ 0. 0. 0. 3. 0. 85.]]
540
- Symmetry: monoclinic
541
-
542
- >>> from Elasticipy.FourthOrderTensor import ComplianceTensor\n
543
- >>> ComplianceTensor.fromCrystalSymmetry(symmetry='monoclinic', diad='y', phase_name='TiNi',
544
- ... S11=8, S12=-3, S13=-2,
545
- ... S22=8, S23=-5, S33=10,
546
- ... S44=12, S55=116, S66=12,
547
- ... S15=14, S25=-8, S35=0, S46=0)
548
- Compliance tensor (in Voigt notation) for TiNi:
549
- [[ 8. -3. -2. 0. 14. 0.]
550
- [ -3. 8. -5. 0. -8. 0.]
551
- [ -2. -5. 10. 0. 0. 0.]
552
- [ 0. 0. 0. 12. 0. 0.]
553
- [ 14. -8. 0. 0. 116. 0.]
554
- [ 0. 0. 0. 0. 0. 12.]]
555
- Symmetry: monoclinic
556
- """
557
- matrix = cls._matrixFromCrystalSymmetry(point_group=point_group, diad=diad, symmetry=symmetry, prefix=prefix,
558
- **kwargs)
559
- return cls(matrix, symmetry=symmetry, phase_name=phase_name)
560
-
561
- @classmethod
562
- def hexagonal(cls, *, C11=0., C12=0., C13=0., C33=0., C44=0., phase_name=None):
563
- """
564
- Create a fourth-order tensor from hexagonal symmetry.
565
-
566
- Parameters
567
- ----------
568
- C11, C12 , C13, C33, C44 : float
569
- Components of the tensor, using the Voigt notation
570
- phase_name : str, optional
571
- Phase name to display
572
- Returns
573
- -------
574
- FourthOrderTensor
575
-
576
- See Also
577
- --------
578
- transverse_isotropic : creates a transverse-isotropic tensor from engineering parameters
579
- cubic : create a tensor from cubic symmetry
580
- tetragonal : create a tensor from tetragonal symmetry
581
- """
582
- return cls.fromCrystalSymmetry(symmetry='hexagonal', C11=C11, C12=C12, C13=C13, C33=C33, C44=C44,
583
- phase_name=phase_name, prefix='C')
584
-
585
- @classmethod
586
- def trigonal(cls, *, C11=0., C12=0., C13=0., C14=0., C33=0., C44=0., C15=0., phase_name=None):
587
- """
588
- Create a fourth-order tensor from trigonal symmetry.
589
-
590
- Parameters
591
- ----------
592
- C11, C12, C13, C14, C33, C44 : float
593
- Components of the tensor, using the Voigt notation
594
- C15 : float, optional
595
- C15 component of the tensor, only used for point groups 3 and -3.
596
- phase_name : str, optional
597
- Phase name to display
598
- Returns
599
- -------
600
- FourthOrderTensor
601
-
602
- See Also
603
- --------
604
- tetragonal : create a tensor from tetragonal symmetry
605
- orthorhombic : create a tensor from orthorhombic symmetry
606
- """
607
- return cls.fromCrystalSymmetry(point_group='3', C11=C11, C12=C12, C13=C13, C14=C14, C15=C15,
608
- C33=C33, C44=C44, phase_name=phase_name, prefix='C')
609
-
610
- @classmethod
611
- def tetragonal(cls, *, C11=0., C12=0., C13=0., C33=0., C44=0., C16=0., C66=0., phase_name=None):
612
- """
613
- Create a fourth-order tensor from tetragonal symmetry.
614
-
615
- Parameters
616
- ----------
617
- C11, C12, C13, C33, C44, C66 : float
618
- Components of the tensor, using the Voigt notation
619
- C16 : float, optional
620
- C16 component in Voigt notation (for point groups 4, -4 and 4/m only)
621
- phase_name : str, optional
622
- Phase name to display
623
-
624
- Returns
625
- -------
626
- FourthOrderTensor
627
-
628
- See Also
629
- --------
630
- trigonal : create a tensor from trigonal symmetry
631
- orthorhombic : create a tensor from orthorhombic symmetry
632
- """
633
- return cls.fromCrystalSymmetry(point_group='4', C11=C11, C12=C12, C13=C13, C16=C16,
634
- C33=C33, C44=C44, C66=C66, phase_name=phase_name, prefix='C')
635
-
636
- @classmethod
637
- def cubic(cls, *, C11=0., C12=0., C44=0., phase_name=None):
638
- """
639
- Create a fourth-order tensor from cubic symmetry.
640
-
641
- Parameters
642
- ----------
643
- C11 , C12, C44 : float
644
- phase_name : str, optional
645
- Phase name to display
646
-
647
- Returns
648
- -------
649
- StiffnessTensor
650
-
651
- See Also
652
- --------
653
- hexagonal : create a tensor from hexagonal symmetry
654
- orthorhombic : create a tensor from orthorhombic symmetry
655
- """
656
- return cls.fromCrystalSymmetry(symmetry='cubic', C11=C11, C12=C12, C44=C44, phase_name=phase_name, prefix='C')
657
-
658
- @classmethod
659
- def orthorhombic(cls, *, C11=0., C12=0., C13=0., C22=0., C23=0., C33=0., C44=0., C55=0., C66=0., phase_name=None):
660
- """
661
- Create a fourth-order tensor from orthorhombic symmetry.
662
-
663
- Parameters
664
- ----------
665
- C11, C12, C13, C22, C23, C33, C44, C55, C66 : float
666
- Components of the tensor, using the Voigt notation
667
- phase_name : str, optional
668
- Phase name to display
669
-
670
- Returns
671
- -------
672
- FourthOrderTensor
673
-
674
- See Also
675
- --------
676
- monoclinic : create a tensor from monoclinic symmetry
677
- orthorhombic : create a tensor from orthorhombic symmetry
678
- """
679
- return cls.fromCrystalSymmetry(symmetry='orthorhombic',
680
- C11=C11, C12=C12, C13=C13, C22=C22, C23=C23, C33=C33, C44=C44, C55=C55, C66=C66,
681
- phase_name=phase_name, prefix='C')
682
-
683
- @classmethod
684
- def monoclinic(cls, *, C11=0., C12=0., C13=0., C22=0., C23=0., C33=0., C44=0., C55=0., C66=0.,
685
- C15=None, C25=None, C35=None, C46=None,
686
- C16=None, C26=None, C36=None, C45=None,
687
- phase_name=None):
688
- """
689
- Create a fourth-order tensor from monoclinic symmetry. It automatically detects whether the components are given
690
- according to the Y or Z diad, depending on the input arguments.
691
-
692
- For Diad || y, C15, C25, C35 and C46 must be provided.
693
- For Diad || z, C16, C26, C36 and C45 must be provided.
694
-
695
- Parameters
696
- ----------
697
- C11, C12 , C13, C22, C23, C33, C44, C55, C66 : float
698
- Components of the tensor, using the Voigt notation
699
- C15 : float, optional
700
- C15 component of the tensor (if Diad || y)
701
- C25 : float, optional
702
- C25 component of the tensor (if Diad || y)
703
- C35 : float, optional
704
- C35 component of the tensor (if Diad || y)
705
- C46 : float, optional
706
- C46 component of the tensor (if Diad || y)
707
- C16 : float, optional
708
- C16 component of the tensor (if Diad || z)
709
- C26 : float, optional
710
- C26 component of the tensor (if Diad || z)
711
- C36 : float, optional
712
- C36 component of the tensor (if Diad || z)
713
- C45 : float, optional
714
- C45 component of the tensor (if Diad || z)
715
- phase_name : str, optional
716
- Name to display
717
-
718
- Returns
719
- -------
720
- FourthOrderTensor
721
-
722
- See Also
723
- --------
724
- triclinic : create a tensor from triclinic symmetry
725
- orthorhombic : create a tensor from orthorhombic symmetry
726
- """
727
- diad_y = not (None in (C15, C25, C35, C46))
728
- diad_z = not (None in (C16, C26, C36, C45))
729
- if diad_y and diad_z:
730
- raise KeyError('Ambiguous diad. Provide either C15, C25, C35 and C46; or C16, C26, C36 and C45')
731
- elif diad_y:
732
- return cls.fromCrystalSymmetry(symmetry='monoclinic', diad='y',
733
- C11=C11, C12=C12, C13=C13, C22=C22, C23=C23, C33=C33, C44=C44, C55=C55,
734
- C66=C66,
735
- C15=C15, C25=C25, C35=C35, C46=C46, phase_name=phase_name, prefix='C')
736
- elif diad_z:
737
- return cls.fromCrystalSymmetry(symmetry='monoclinic', diad='z',
738
- C11=C11, C12=C12, C13=C13, C22=C22, C23=C23, C33=C33, C44=C44, C55=C55,
739
- C66=C66,
740
- C16=C16, C26=C26, C36=C36, C45=C45, phase_name=phase_name, prefix='C')
741
- else:
742
- raise KeyError('For monoclinic symmetry, one should provide either C15, C25, C35 and C46, '
743
- 'or C16, C26, C36 and C45.')
744
-
745
- @classmethod
746
- def triclinic(cls, C11=0., C12=0., C13=0., C14=0., C15=0., C16=0.,
747
- C22=0., C23=0., C24=0., C25=0., C26=0.,
748
- C33=0., C34=0., C35=0., C36=0.,
749
- C44=0., C45=0., C46=0.,
750
- C55=0., C56=0.,
751
- C66=0., phase_name=None):
752
- """
753
-
754
- Parameters
755
- ----------
756
- C11 , C12 , C13 , C14 , C15 , C16 , C22 , C23 , C24 , C25 , C26 , C33 , C34 , C35 , C36 , C44 , C45 , C46 , C55 , C56 , C66 : float
757
- Components of the tensor
758
- phase_name : str, optional
759
- Name to display
760
-
761
- Returns
762
- -------
763
- FourthOrderTensor
764
-
765
- See Also
766
- --------
767
- monoclinic : create a tensor from monoclinic symmetry
768
- orthorhombic : create a tensor from orthorhombic symmetry
769
- """
770
- matrix = np.array([[C11, C12, C13, C14, C15, C16],
771
- [C12, C22, C23, C24, C25, C26],
772
- [C13, C23, C33, C34, C35, C36],
773
- [C14, C24, C34, C44, C45, C46],
774
- [C15, C25, C35, C45, C55, C56],
775
- [C16, C26, C36, C46, C56, C66]])
776
- return cls(matrix, phase_name=phase_name)
777
-
778
- def save_to_txt(self, filename, matrix_only=False):
779
- """
780
- Save the tensor to a text file.
781
-
782
- Parameters
783
- ----------
784
- filename : str
785
- Filename to save the tensor to.
786
- matrix_only : bool, False
787
- If true, only the components of tje stiffness tensor is saved (no data about phase nor symmetry)
788
-
789
- See Also
790
- --------
791
- from_txt_file : create a tensor from text file
792
-
793
- """
794
- with open(filename, 'w') as f:
795
- if not matrix_only:
796
- if self.phase_name is not None:
797
- f.write(f"Phase Name: {self.phase_name}\n")
798
- f.write(f"Symmetry: {self.symmetry}\n")
799
- for row in self.matrix:
800
- f.write(" " + " ".join(f"{value:8.2f}" for value in row) + "\n")
801
-
802
- @classmethod
803
- def from_txt_file(cls, filename):
804
- """
805
- Load the tensor from a text file.
806
-
807
- The two first lines can have data about phase name and symmetry, but this is not mandatory.
808
-
809
- Parameters
810
- ----------
811
- filename : str
812
- Filename to load the tensor from.
813
-
814
- Returns
815
- -------
816
- SymmetricTensor
817
- The reconstructed tensor read from the file.
818
-
819
- See Also
820
- --------
821
- save_to_txt : create a tensor from text file
822
-
823
- """
824
- with open(filename, 'r') as f:
825
- lines = f.readlines()
826
-
827
- # Initialize defaults
828
- phase_name = None
829
- symmetry = 'Triclinic'
830
- matrix_start_index = 0
831
-
832
- # Parse phase name if available
833
- if lines and lines[0].startswith("Phase Name:"):
834
- phase_name = lines[0].split(": ", 1)[1].strip()
835
- matrix_start_index += 1
836
-
837
- # Parse symmetry if available
838
- if len(lines) > matrix_start_index and lines[matrix_start_index].startswith("Symmetry:"):
839
- symmetry = lines[matrix_start_index].split(": ", 1)[1].strip()
840
- matrix_start_index += 1
841
-
842
- # Parse matrix
843
- matrix = np.loadtxt(lines[matrix_start_index:])
844
-
845
- # Return the reconstructed object
846
- return cls(matrix, phase_name=phase_name, symmetry=symmetry)
847
-
848
- def __getitem__(self, item):
849
- if self.orientations is None:
850
- raise IndexError('The tensor has no orientation, therefore it cannot be indexed.')
851
- else:
852
- return self._unrotate() * self.orientations[item]
853
-
854
-
855
- class StiffnessTensor(SymmetricTensor):
856
- """
857
- Class for manipulating fourth-order stiffness tensors.
858
- """
859
- tensor_name = 'Stiffness'
860
- C11_C12_factor = 0.5
861
-
862
- def __init__(self, S, check_positive_definite=True, **kwargs):
863
- super().__init__(S, check_positive_definite=check_positive_definite, **kwargs)
864
-
865
- def __mul__(self, other):
866
- if isinstance(other, StrainTensor):
867
- new_tensor = super().__mul__(other)
868
- return StressTensor(new_tensor.matrix)
869
- elif isinstance(other, StressTensor):
870
- raise ValueError('You cannot multiply a stiffness tensor with a Stress tensor.')
871
- else:
872
- return super().__mul__(other)
873
-
874
- def inv(self):
875
- """
876
- Compute the reciprocal compliance tensor
877
-
878
- Returns
879
- -------
880
- ComplianceTensor
881
- Reciprocal tensor
882
- """
883
- C = np.linalg.inv(self.matrix)
884
- return ComplianceTensor(C, symmetry=self.symmetry, phase_name=self.phase_name, orientations=self.orientations)
885
-
886
- @property
887
- def Young_modulus(self):
888
- """
889
- Directional Young's modulus
890
-
891
- Returns
892
- -------
893
- SphericalFunction
894
- Young's modulus
895
- """
896
-
897
- def compute_young_modulus(n):
898
- eps = _compute_unit_strain_along_direction(self, n, n)
899
- return 1 / eps
900
-
901
- return SphericalFunction(compute_young_modulus)
902
-
903
- @property
904
- def shear_modulus(self):
905
- """
906
- Directional shear modulus
907
-
908
- Returns
909
- -------
910
- HyperSphericalFunction
911
- Shear modulus
912
- """
913
-
914
- def compute_shear_modulus(m, n):
915
- eps = _compute_unit_strain_along_direction(self, m, n)
916
- return 1 / (4 * eps)
917
-
918
- return HyperSphericalFunction(compute_shear_modulus)
919
-
920
- @property
921
- def Poisson_ratio(self):
922
- """
923
- Directional Poisson's ratio
924
-
925
- Returns
926
- -------
927
- HyperSphericalFunction
928
- Poisson's ratio
929
- """
930
-
931
- def compute_PoissonRatio(m, n):
932
- eps1 = _compute_unit_strain_along_direction(self, m, m)
933
- eps2 = _compute_unit_strain_along_direction(self, m, n, direction='transverse')
934
- return -eps2 / eps1
935
-
936
- return HyperSphericalFunction(compute_PoissonRatio)
937
-
938
- @property
939
- def linear_compressibility(self):
940
- """
941
- Compute the directional linear compressibility.
942
-
943
- Returns
944
- -------
945
- SphericalFunction
946
- Directional linear compressibility
947
-
948
- See Also
949
- --------
950
- bulk_modulus : bulk modulus of the material
951
- """
952
-
953
- def compute_linear_compressibility(n):
954
- return _compute_unit_strain_along_direction(self, n, n, direction='spherical')
955
-
956
- return SphericalFunction(compute_linear_compressibility)
957
-
958
- @property
959
- def bulk_modulus(self):
960
- """
961
- Compute the bulk modulus of the material
962
-
963
- Returns
964
- -------
965
- float
966
- Bulk modulus
967
-
968
- See Also
969
- --------
970
- linear_compressibility : directional linear compressibility
971
- """
972
- return self.inv().bulk_modulus
973
-
974
- def Voigt_average(self):
975
- """
976
- Compute the Voigt average of the stiffness tensor. If the tensor contains no orientation, we assume isotropic
977
- behaviour. Otherwise, the mean is computed over all orientations.
978
-
979
- Returns
980
- -------
981
- StiffnessTensor
982
- Voigt average of stiffness tensor
983
-
984
- See Also
985
- --------
986
- Reuss_average : compute the Reuss average
987
- Hill_average : compute the Voigt-Reuss-Hill average
988
- average : generic function for calling either the Voigt, Reuss or Hill average
989
- """
990
- if self.orientations is None:
991
- c = self.matrix
992
- C11 = (c[0, 0] + c[1, 1] + c[2, 2]) / 5 \
993
- + (c[0, 1] + c[0, 2] + c[1, 2]) * 2 / 15 \
994
- + (c[3, 3] + c[4, 4] + c[5, 5]) * 4 / 15
995
- C12 = (c[0, 0] + c[1, 1] + c[2, 2]) / 15 \
996
- + (c[0, 1] + c[0, 2] + c[1, 2]) * 4 / 15 \
997
- - (c[3, 3] + c[4, 4] + c[5, 5]) * 2 / 15
998
- C44 = (c[0, 0] + c[1, 1] + c[2, 2] - c[0, 1] - c[0, 2] - c[1, 2]) / 15 + (c[3, 3] + c[4, 4] + c[5, 5]) / 5
999
- mat = _isotropic_matrix(C11, C12, C44)
1000
- return StiffnessTensor(mat, symmetry='isotropic', phase_name=self.phase_name)
1001
- else:
1002
- return StiffnessTensor(self.mean())
1003
-
1004
- def Reuss_average(self):
1005
- """
1006
- Compute the Reuss average of the stiffness tensor. If the tensor contains no orientation, we assume isotropic
1007
- behaviour. Otherwise, the mean is computed over all orientations.
1008
-
1009
- Returns
1010
- -------
1011
- StiffnessTensor
1012
- Reuss average of stiffness tensor
1013
-
1014
- See Also
1015
- --------
1016
- Voigt_average : compute the Voigt average
1017
- Hill_average : compute the Voigt-Reuss-Hill average
1018
- average : generic function for calling either the Voigt, Reuss or Hill average
1019
- """
1020
- return self.inv().Reuss_average().inv()
1021
-
1022
- def Hill_average(self):
1023
- """
1024
- Compute the (Voigt-Reuss-)Hill average of the stiffness tensor. If the tensor contains no orientation, we assume
1025
- isotropic behaviour. Otherwise, the mean is computed over all orientations.
1026
-
1027
- Returns
1028
- -------
1029
- StiffnessTensor
1030
- Voigt-Reuss-Hill average of tensor
1031
-
1032
- See Also
1033
- --------
1034
- Voigt_average : compute the Voigt average
1035
- Reuss_average : compute the Reuss average
1036
- average : generic function for calling either the Voigt, Reuss or Hill average
1037
- """
1038
- Reuss = self.Reuss_average()
1039
- Voigt = self.Voigt_average()
1040
- return (Reuss + Voigt) * 0.5
1041
-
1042
- def average(self, method):
1043
- """
1044
- Compute either the Voigt, Reuss, or Hill average of the stiffness tensor.
1045
-
1046
- This function is just a shortcut for Voigt_average(), Reuss_average(), or Hill_average() and Hill_average().
1047
-
1048
- Parameters
1049
- ----------
1050
- method : str {'Voigt', 'Reuss', 'Hill'}
1051
- Method to use to compute the average.
1052
-
1053
- Returns
1054
- -------
1055
- StiffnessTensor
1056
-
1057
- See Also
1058
- --------
1059
- Voigt_average : compute the Voigt average
1060
- Reuss_average : compute the Reuss average
1061
- Hill_average : compute the Voigt-Reuss-Hill average
1062
- """
1063
- method = method.capitalize()
1064
- if method in ('Voigt', 'Reuss', 'Hill'):
1065
- fun = getattr(self, method + '_average')
1066
- return fun()
1067
- else:
1068
- raise NotImplementedError('Only Voigt, Reus, and Hill are implemented.')
1069
-
1070
- @classmethod
1071
- def isotropic(cls, E=None, nu=None, lame1=None, lame2=None, phase_name=None):
1072
- """
1073
- Create an isotropic stiffness tensor from two elasticity coefficients, namely: E, nu, lame1, or lame2. Exactly
1074
- two of these coefficients must be provided.
1075
-
1076
- Parameters
1077
- ----------
1078
- E : float, None
1079
- Young modulus
1080
- nu : float, None
1081
- Poisson ratio
1082
- lame1 : float, None
1083
- First Lamé coefficient
1084
- lame2 : float, None
1085
- Second Lamé coefficient
1086
- phase_name : str, None
1087
- Name to print
1088
-
1089
- Returns
1090
- -------
1091
- Corresponding isotropic stiffness tensor
1092
-
1093
- See Also
1094
- --------
1095
- transverse_isotropic : create a transverse-isotropic tensor
1096
-
1097
- Examples
1098
- --------
1099
- On can check that the shear modulus for steel is around 82 GPa:
1100
-
1101
- >>> from Elasticipy.FourthOrderTensor import StiffnessTensor
1102
- >>> C=StiffnessTensor.isotropic(E=210e3, nu=0.28)
1103
- >>> C.shear_modulus
1104
- Hyperspherical function
1105
- Min=82031.24999999997, Max=82031.25000000006
1106
- """
1107
- argument_vector = np.array([E, nu, lame1, lame2])
1108
- if np.count_nonzero(argument_vector) != 2:
1109
- raise ValueError("Exactly two values are required among E, nu, lame1 and lame2.")
1110
- if E is not None:
1111
- if nu is not None:
1112
- lame1 = E * nu / ((1 + nu) * (1 - 2 * nu))
1113
- lame2 = E / (1 + nu) / 2
1114
- elif lame1 is not None:
1115
- R = np.sqrt(E ** 2 + 9 * lame1 ** 2 + 2 * E * lame1)
1116
- lame2 = (E - 3 * lame1 + R) / 4
1117
- elif lame2 is not None:
1118
- lame1 = lame2 * (E - 2 * lame2) / (3 * lame2 - E)
1119
- else:
1120
- raise ValueError('Either nu, lame1 or lame2 must be provided.')
1121
- elif nu is not None:
1122
- if lame1 is not None:
1123
- lame2 = lame1 * (1 - 2 * nu) / (2 * nu)
1124
- elif lame2 is not None:
1125
- lame1 = 2 * lame2 * nu / (1 - 2 * nu)
1126
- else:
1127
- raise ValueError('Either lame1 or lame2 must be provided.')
1128
- C11 = lame1 + 2 * lame2
1129
- C12 = lame1
1130
- C44 = lame2
1131
- matrix = _isotropic_matrix(C11, C12, C44)
1132
- return StiffnessTensor(np.array(matrix), symmetry='isotropic', phase_name=phase_name)
1133
-
1134
- @classmethod
1135
- def orthotropic(cls, *, Ex, Ey, Ez, nu_yx, nu_zx, nu_zy, Gxy, Gxz, Gyz, **kwargs):
1136
- """
1137
- Create a stiffness tensor corresponding to orthotropic symmetry, given the engineering constants.
1138
-
1139
- Parameters
1140
- ----------
1141
- Ex : float
1142
- Young modulus along the x axis
1143
- Ey : float
1144
- Young modulus along the y axis
1145
- Ez : float
1146
- Young modulus along the z axis
1147
- nu_yx : float
1148
- Poisson ratio between x and y axes
1149
- nu_zx : float
1150
- Poisson ratio between x and z axes
1151
- nu_zy : float
1152
- Poisson ratio between y and z axes
1153
- Gxy : float
1154
- Shear modulus in the x-y plane
1155
- Gxz : float
1156
- Shear modulus in the x-z plane
1157
- Gyz : float
1158
- Shear modulus in the y-z plane
1159
- kwargs : dict, optional
1160
- Keyword arguments to pass to the StiffnessTensor constructor
1161
-
1162
- Returns
1163
- -------
1164
- StiffnessTensor
1165
-
1166
- See Also
1167
- --------
1168
- transverse_isotropic : create a stiffness tensor for transverse-isotropic symmetry
1169
- """
1170
- tri_sup = np.array([[1 / Ex, -nu_yx / Ey, -nu_zx / Ez, 0, 0, 0],
1171
- [0, 1 / Ey, -nu_zy / Ez, 0, 0, 0],
1172
- [0, 0, 1 / Ez, 0, 0, 0],
1173
- [0, 0, 0, 1 / Gyz, 0, 0],
1174
- [0, 0, 0, 0, 1 / Gxz, 0],
1175
- [0, 0, 0, 0, 0, 1 / Gxy]])
1176
- S = tri_sup + np.tril(tri_sup.T, -1)
1177
- return StiffnessTensor(np.linalg.inv(S), symmetry='orthotropic', **kwargs)
1178
-
1179
- @classmethod
1180
- def transverse_isotropic(cls, *, Ex, Ez, nu_yx, nu_zx, Gxz, **kwargs):
1181
- """
1182
- Create a stiffness tensor corresponding to the transverse isotropic symmetry, given the engineering constants.
1183
-
1184
- Parameters
1185
- ----------
1186
- Ex : float
1187
- Young modulus along the x axis
1188
- Ez : float
1189
- Young modulus along the y axis
1190
- nu_yx : float
1191
- Poisson ratio between x and y axes
1192
- nu_zx : float
1193
- Poisson ratio between x and z axes
1194
- Gxz : float
1195
- Shear modulus in the x-z plane
1196
- kwargs : dict
1197
- Keyword arguments to pass to the StiffnessTensor constructor
1198
-
1199
- Returns
1200
- -------
1201
- StiffnessTensor
1202
-
1203
- See Also
1204
- --------
1205
- orthotropic : create a stiffness tensor for orthotropic symmetry
1206
- """
1207
- Gxy = Ex / (2 * (1 + nu_yx))
1208
- C = StiffnessTensor.orthotropic(Ex=Ex, Ey=Ex, Ez=Ez,
1209
- nu_yx=nu_yx, nu_zx=nu_zx, nu_zy=nu_zx,
1210
- Gxy=Gxy, Gxz=Gxz, Gyz=Gxz, **kwargs)
1211
- C.symmetry = 'transverse-isotropic'
1212
- return C
1213
-
1214
- def Christoffel_tensor(self, u):
1215
- """
1216
- Create the Christoffel tensor along a given direction, or set or directions.
1217
-
1218
- Parameters
1219
- ----------
1220
- u : list or np.ndarray
1221
- 3D direction(s) to compute the Christoffel tensor along with
1222
-
1223
- Returns
1224
- -------
1225
- Gamma : np.ndarray
1226
- Array of Christoffel tensor(s). if u is a list of directions, Gamma[i] is the Christoffel tensor for
1227
- direction u[i].
1228
-
1229
- See Also
1230
- --------
1231
- wave_velocity : computes the p- and s-wave velocities.
1232
-
1233
- Notes
1234
- -----
1235
- For a given stiffness tensor **C** and a given unit vector **u**, the Christoffel tensor is defined as [2]_ :
1236
-
1237
- .. math:: M_{ij} = C_{iklj}.u_k.u_l
1238
-
1239
- """
1240
- u_vec = np.atleast_2d(u)
1241
- u_vec = (u_vec.T / np.linalg.norm(u_vec, axis=1)).T
1242
- return np.einsum('inmj,pn,pm->pij', self.full_tensor(), u_vec, u_vec)
1243
-
1244
- def wave_velocity(self, rho):
1245
- """
1246
- Compute the wave velocities, given the mass density.
1247
-
1248
- Parameters
1249
- ----------
1250
- rho : float
1251
- mass density. Its unit must be consistent with that of the stiffness tensor. See notes for hints.
1252
-
1253
- See Also
1254
- --------
1255
- ChristoffelTensor : Computes the Christoffel tensor along a given direction
1256
-
1257
- Returns
1258
- -------
1259
- c_p : SphericalFunction
1260
- Velocity of the primary (compressive) wave
1261
- c_s1 : SphericalFunction
1262
- Velocity of the fast secondary (shear) wave
1263
- c_s2 : SphericalFunction
1264
- Velocity of the slow secondary (shear) wave
1265
-
1266
- Notes
1267
- -----
1268
- The estimation of the wave velocities is made by finding the eigenvalues of the Christoffel tensor [2]_.
1269
-
1270
- One should double-check the units. The table below provides hints about the unit you get, depending on the units
1271
- you use for stiffness and the mass density:
1272
-
1273
- +-----------------+--------------+------------+-----------------------+
1274
- | Stiffness | Mass density | Velocities | Notes |
1275
- +=================+==============+============+=======================+
1276
- | Pa (N/m²) | kg/m³ | m/s | SI units |
1277
- +-----------------+--------------+------------+-----------------------+
1278
- | GPa (10⁹ Pa) | kg/dm³ | km/s | Conversion factor |
1279
- +-----------------+--------------+------------+-----------------------+
1280
- | GPa (10³ N/mm²) | kg/mm³ | m/s | Consistent units |
1281
- +-----------------+--------------+------------+-----------------------+
1282
- | MPa (10⁶ Pa) | kg/m³ | km/s | Conversion factor |
1283
- +-----------------+--------------+------------+-----------------------+
1284
- | MPa (10³ N/mm²) | g/mm³ | m/s | Consistent units |
1285
- +-----------------+--------------+------------+-----------------------+
1286
-
1287
- References
1288
- ----------
1289
- .. [2] J. W. Jaeken, S. Cottenier, Solving the Christoffel equation: Phase and group velocities, Computer Physics
1290
- Communications (207), 2016, https://doi.org/10.1016/j.cpc.2016.06.014.
1291
-
1292
- """
1293
-
1294
- def make_fun(index):
1295
- def fun(n):
1296
- Gamma = self.Christoffel_tensor(n)
1297
- eig, _ = np.linalg.eig(Gamma)
1298
- if index == 0:
1299
- eig_of_interest = np.max(eig, axis=-1)
1300
- elif index == 1:
1301
- eig_of_interest = np.median(eig, axis=-1)
1302
- else:
1303
- eig_of_interest = np.min(eig, axis=-1)
1304
- return np.sqrt(eig_of_interest / rho)
1305
-
1306
- return fun
1307
-
1308
- return [SphericalFunction(make_fun(i)) for i in range(3)]
1309
-
1310
- @classmethod
1311
- def from_MP(cls, ids, api_key=None):
1312
- """
1313
- Import stiffness tensor(s) from the Materials Project API, given their material ids.
1314
-
1315
- You need to register to `<https://materialsproject.org>`_ first to get an API key. This key can be explicitly
1316
- passed as an argument (see below), or provided as an environment variable named MP_API_KEY.
1317
-
1318
- Parameters
1319
- ----------
1320
- ids : str or list of str
1321
- ID(s) of the material to import (e.g. "mp-1048")
1322
- api_key : str, optional
1323
- API key to the Materials Project API. If not provided, it should be available as the API_KEY environment
1324
- variable.
1325
-
1326
- Returns
1327
- -------
1328
- list of StiffnessTensor
1329
- If one of the requested material ids was not found, the corresponding value in the list will be None.
1330
- """
1331
- try:
1332
- from mp_api.client import MPRester
1333
- except ImportError:
1334
- raise ModuleNotFoundError('mp_api module is required for this function.')
1335
- if type(ids) is str:
1336
- Cdict = dict.fromkeys([ids])
1337
- else:
1338
- Cdict = dict.fromkeys(ids)
1339
- with MPRester(api_key=api_key) as mpr:
1340
- elasticity_doc = mpr.materials.elasticity.search(material_ids=ids)
1341
- for material in elasticity_doc:
1342
- key = str(material.material_id)
1343
- if material.elastic_tensor is not None:
1344
- matrix = material.elastic_tensor.ieee_format
1345
- symmetry = material.symmetry.crystal_system.value
1346
- phase_name = material.formula_pretty
1347
- C = StiffnessTensor(matrix, symmetry=symmetry, phase_name=phase_name)
1348
- else:
1349
- C = None
1350
- Cdict[key] = C
1351
- if elasticity_doc:
1352
- if isinstance(ids, str):
1353
- return C
1354
- else:
1355
- return [Cdict[id] for id in ids]
1356
- else:
1357
- return None
1358
-
1359
- @classmethod
1360
- def weighted_average(cls, Cs, volume_fractions, method):
1361
- """
1362
- Compute the weighted average of a list of stiffness tensors, with respect to a given method (Voigt, Reuss or
1363
- Hill).
1364
-
1365
- Parameters
1366
- ----------
1367
- Cs : list of StiffnessTensor or list of ComplianceTensor or tuple of StiffnessTensor or tuple of ComplianceTensor
1368
- Series of tensors to compute the average from
1369
- volume_fractions : iterable of floats
1370
- Volume fractions of each phase
1371
- method : str, {'Voigt', 'Reuss', 'Hill'}
1372
- Method to use. It can be 'Voigt', 'Reuss', or 'Hill'.
1373
-
1374
- Returns
1375
- -------
1376
- StiffnessTensor
1377
- Average tensor
1378
- """
1379
- if np.all([isinstance(a, ComplianceTensor) for a in Cs]):
1380
- Cs = [C.inv() for C in Cs]
1381
- if np.all([isinstance(a, StiffnessTensor) for a in Cs]):
1382
- C_stack = np.array([C.matrix for C in Cs])
1383
- method = method.capitalize()
1384
- if method == 'Voigt':
1385
- C_avg = np.average(C_stack, weights=volume_fractions, axis=0)
1386
- return StiffnessTensor(C_avg)
1387
- elif method == 'Reuss':
1388
- S_stack = np.linalg.inv(C_stack)
1389
- S_avg = np.average(S_stack, weights=volume_fractions, axis=0)
1390
- return StiffnessTensor(np.linalg.inv(S_avg))
1391
- elif method == 'Hill':
1392
- C_voigt = cls.weighted_average(Cs, volume_fractions, 'Voigt')
1393
- C_reuss = cls.weighted_average(Cs, volume_fractions, 'Reuss')
1394
- return (C_voigt + C_reuss) * 0.5
1395
- else:
1396
- raise ValueError('Method must be either Voigt, Reuss or Hill.')
1397
- else:
1398
- raise ValueError('The first argument must be either a list of ComplianceTensors or '
1399
- 'a list of StiffnessTensor.')
1400
-
1401
- @property
1402
- def universal_anisotropy(self):
1403
- """
1404
- Compute the universal anisotropy factor.
1405
-
1406
- The larger the value, the more likely the material will behave in an anisotropic way.
1407
-
1408
- Returns
1409
- -------
1410
- float
1411
- The universal anisotropy factor.
1412
-
1413
- Notes
1414
- -----
1415
- The universal anisotropy factor is defined as [3]_:
1416
-
1417
- .. math::
1418
-
1419
- 5\\frac{G_v}{G_r} + \\frac{K_v}{K_r} - 6
1420
-
1421
- References
1422
- ----------
1423
- .. [3] S. I. Ranganathan and M. Ostoja-Starzewski, Universal Elastic Anisotropy Index,
1424
- *Phys. Rev. Lett.*, 101(5), 055504, 2008. https://doi.org/10.1103/PhysRevLett.101.055504
1425
- """
1426
- C = self._unrotate() # Ensure that the averages do not use the orientations
1427
- Cvoigt = C.Voigt_average()
1428
- Creuss = C.Reuss_average()
1429
- Gv = Cvoigt.matrix[3, 3]
1430
- Gr = Creuss.matrix[3, 3]
1431
- Kv = Cvoigt.bulk_modulus
1432
- Kr = Creuss.bulk_modulus
1433
- return 5 * Gv / Gr + Kv / Kr - 6
1434
-
1435
- @property
1436
- def Zener_ratio(self):
1437
- """
1438
- Compute the Zener ratio (Z). Only valid for cubic symmetry.
1439
-
1440
- It is only valid for cubic and isotropic symmetry. Will return NaN for other symmetries.
1441
-
1442
- Returns
1443
- -------
1444
- float
1445
- Zener ratio (NaN is the symmetry is not cubic)
1446
-
1447
- Notes
1448
- -----
1449
- The Zener ratio is defined as:
1450
-
1451
- .. math::
1452
-
1453
- Z=\\frac{ 2C_{44} }{C11 - C12}
1454
-
1455
- See Also
1456
- --------
1457
- universal_anisotropy : compute the universal anisotropy factor
1458
- """
1459
- if self.symmetry == 'isotropic':
1460
- return 1.0
1461
- elif self.symmetry == 'cubic':
1462
- return 2 * self.C44 / (self.C11 - self.C12)
1463
- else:
1464
- return np.nan
1465
-
1466
- def to_pymatgen(self):
1467
- """
1468
- Convert the stiffness tensor (from Elasticipy) to Python Materials Genomics (Pymatgen) format.
1469
-
1470
- Returns
1471
- -------
1472
- pymatgen.analysis.elasticity.elastic.ElasticTensor
1473
- Stiffness tensor for pymatgen
1474
- """
1475
- try:
1476
- from pymatgen.analysis.elasticity import elastic as matgenElast
1477
- except ImportError:
1478
- raise ModuleNotFoundError('pymatgen module is required for this function.')
1479
- return matgenElast.ElasticTensor(self.full_tensor())
1480
-
1481
- def to_Kelvin(self):
1482
- """
1483
- Returns all the tensor components using the Kelvin(-Mandel) mapping convention.
1484
-
1485
- Returns
1486
- -------
1487
- numpy.ndarray
1488
- (6,6) matrix, according to the Kelvin mapping
1489
-
1490
- See Also
1491
- --------
1492
- eig : returns the eigenvalues and the eigenvectors of the Kelvin's matrix
1493
- from_Kelvin : Construct a fourth-order tensor from its (6,6) Kelvin matrix
1494
-
1495
- Notes
1496
- -----
1497
- This mapping convention is discussed in [4]_.
1498
-
1499
- References
1500
- ----------
1501
- .. [4] Helbig, K. (2013). What Kelvin might have written about Elasticity. Geophysical Prospecting, 61(1), 1-20.
1502
- doi: 10.1111/j.1365-2478.2011.01049.x
1503
- """
1504
- return self.matrix /self.voigt_map * _voigt_to_kelvin_matrix
1505
-
1506
- def eig(self):
1507
- """
1508
- Compute the eigenstiffnesses and the eigenstrains.
1509
-
1510
- Solve the eigenvalue problem from the Kelvin matrix of the stiffness tensor (see Notes).
1511
-
1512
- Returns
1513
- -------
1514
- numpy.ndarray
1515
- Array of 6 eigenstiffnesses (eigenvalues of the stiffness matrix)
1516
- numpy.ndarray
1517
- (6,6) array of eigenstrains (eigenvectors of the stiffness matrix)
1518
-
1519
- See Also
1520
- --------
1521
- to_Kelvin : returns the stiffness components as a (6,6) matrix, according to the Kelvin mapping convention.
1522
- eig_stiffnesses : returns the eigenstiffnesses only
1523
- eig_strains : returns the eigenstrains only
1524
-
1525
- Notes
1526
- -----
1527
- The definition for eigenstiffnesses and the eigenstrains are introduced in [4]_.
1528
- """
1529
- return np.linalg.eigh(self.to_Kelvin())
1530
-
1531
- @property
1532
- def eig_stiffnesses(self):
1533
- """
1534
- Compute the eigenstiffnesses given by the Kelvin's matrix for stiffness.
1535
-
1536
- Returns
1537
- -------
1538
- numpy.ndarray
1539
- 6 eigenvalues of the Kelvin's stiffness matrix, in ascending order
1540
-
1541
- See Also
1542
- --------
1543
- eig : returns the eigenstiffnesses and the eigenstrains
1544
- eig_strains : returns the eigenstrains only
1545
- """
1546
- return np.linalg.eigvalsh(self.to_Kelvin())
1547
-
1548
- @property
1549
- def eig_strains(self):
1550
- """
1551
- Compute the eigenstrains from the Kelvin's matrix for stiffness
1552
-
1553
- Returns
1554
- -------
1555
- numpy.ndarray
1556
- (6,6) matrix of eigenstrains, sorted by ascending order of eigenstiffnesses.
1557
-
1558
- See Also
1559
- --------
1560
- eig : returns both the eigenvalues and the eigenvectors of the Kelvin matrix
1561
- """
1562
- return self.eig()[1]
1563
-
1564
- @property
1565
- def eig_compliances(self):
1566
- """
1567
- Compute the eigencompliances from the Kelvin's matrix of stiffness
1568
-
1569
- Returns
1570
- -------
1571
- numpy.ndarray
1572
- Inverses of the 6 eigenvalues of the Kelvin's stiffness matrix, in descending order
1573
-
1574
- See Also
1575
- --------
1576
- eig_stiffnesses : compute the eigenstiffnesses from the Kelvin's matrix of stiffness
1577
- """
1578
- return 1/self.eig_stiffnesses
1579
-
1580
- @classmethod
1581
- def from_Kelvin(cls, matrix, **kwargs):
1582
- """
1583
- Create a tensor from the (6,6) matrix following the Kelvin(-Mandel) mapping convention
1584
-
1585
- Parameters
1586
- ----------
1587
- matrix : list or numpy.ndarray
1588
- (6,6) matrix of components
1589
- kwargs : dict
1590
- keyword arguments passed to the constructor
1591
- Returns
1592
- -------
1593
- StiffnessTensor
1594
- """
1595
- return cls(matrix * cls.voigt_map / _voigt_to_kelvin_matrix, **kwargs)
1596
-
1597
-
1598
- class ComplianceTensor(StiffnessTensor):
1599
- """
1600
- Class for manipulating compliance tensors
1601
- """
1602
- tensor_name = 'Compliance'
1603
- voigt_map = np.array([[1., 1., 1., 2., 2., 2.],
1604
- [1., 1., 1., 2., 2., 2.],
1605
- [1., 1., 1., 2., 2., 2.],
1606
- [2., 2., 2., 4., 4., 4.],
1607
- [2., 2., 2., 4., 4., 4.],
1608
- [2., 2., 2., 4., 4., 4.]])
1609
- C11_C12_factor = 2.0
1610
- component_prefix = 'S'
1611
- C46_C56_factor = 2.0
1612
-
1613
- def __init__(self, C, check_positive_definite=True, **kwargs):
1614
- super().__init__(C, check_positive_definite=check_positive_definite, **kwargs)
1615
-
1616
- def __mul__(self, other):
1617
- if isinstance(other, StressTensor):
1618
- return StrainTensor(self * other.matrix)
1619
- elif isinstance(other, StrainTensor):
1620
- raise ValueError('You cannot multiply a compliance tensor with Strain tensor.')
1621
- else:
1622
- return super().__mul__(other)
1623
-
1624
- def inv(self):
1625
- """
1626
- Compute the reciprocal stiffness tensor
1627
-
1628
- Returns
1629
- -------
1630
- StiffnessTensor
1631
- Reciprocal tensor
1632
- """
1633
- S = np.linalg.inv(self.matrix)
1634
- return StiffnessTensor(S, symmetry=self.symmetry, phase_name=self.phase_name, orientations=self.orientations)
1635
-
1636
- def Reuss_average(self):
1637
- if self.orientations is None:
1638
- s = self.matrix
1639
- S11 = (s[0, 0] + s[1, 1] + s[2, 2]) / 5 \
1640
- + (s[0, 1] + s[0, 2] + s[1, 2]) * 2 / 15 \
1641
- + (s[3, 3] + s[4, 4] + s[5, 5]) * 1 / 15
1642
- S12 = (s[0, 0] + s[1, 1] + s[2, 2]) / 15 \
1643
- + (s[0, 1] + s[0, 2] + s[1, 2]) * 4 / 15 \
1644
- - (s[3, 3] + s[4, 4] + s[5, 5]) * 1 / 30
1645
- S44 = ((s[0, 0] + s[1, 1] + s[2, 2] - s[0, 1] - s[0, 2] - s[1, 2]) * 4 / 15 +
1646
- (s[3, 3] + s[4, 4] + s[5, 5]) / 5)
1647
- mat = _isotropic_matrix(S11, S12, S44)
1648
- return ComplianceTensor(mat, symmetry='isotropic', phase_name=self.phase_name)
1649
- else:
1650
- return ComplianceTensor(self.mean())
1651
-
1652
- def Voigt_average(self):
1653
- return self.inv().Voigt_average().inv()
1654
-
1655
- def Hill_average(self):
1656
- return self.inv().Hill_average().inv()
1657
-
1658
- @classmethod
1659
- def isotropic(cls, E=None, nu=None, lame1=None, lame2=None, phase_name=None):
1660
- return super().isotropic(E=E, nu=nu, lame1=lame1, lame2=lame2, phase_name=None).inv()
1661
-
1662
- @classmethod
1663
- def orthotropic(cls, *args, **kwargs):
1664
- return super().orthotropic(*args, **kwargs).inv()
1665
-
1666
- @classmethod
1667
- def transverse_isotropic(cls, *args, **kwargs):
1668
- return super().transverse_isotropic(*args, **kwargs).inv()
1669
-
1670
- @classmethod
1671
- def weighted_average(cls, *args):
1672
- return super().weighted_average(*args).inv()
1673
-
1674
- @property
1675
- def bulk_modulus(self):
1676
- return 1 / np.sum(self.matrix[0:3, 0:3])
1677
-
1678
- @property
1679
- def universal_anisotropy(self):
1680
- """
1681
- Compute the universal anisotropy factor.
1682
-
1683
- It is actually an alias for inv().universal_anisotropy.
1684
-
1685
- Returns
1686
- -------
1687
- float
1688
- Universal anisotropy factor
1689
- """
1690
- return self.inv().universal_anisotropy
1691
-
1692
- def to_pymatgen(self):
1693
- """
1694
- Convert the compliance tensor (from Elasticipy) to Python Materials Genomics (Pymatgen) format.
1695
-
1696
- Returns
1697
- -------
1698
- pymatgen.analysis.elasticity.elastic.Compliance
1699
- Compliance tensor for pymatgen
1700
- """
1701
- try:
1702
- from pymatgen.analysis.elasticity import elastic as matgenElast
1703
- except ImportError:
1704
- raise ModuleNotFoundError('pymatgen module is required for this function.')
1705
- return matgenElast.ComplianceTensor(self.full_tensor())
1706
-
1707
- def eig(self):
1708
- """
1709
- Compute the eigencompliances and the eigenstresses.
1710
-
1711
- Solve the eigenvalue problem from the Kelvin matrix of the compliance tensor (see Notes).
1712
-
1713
- Returns
1714
- -------
1715
- numpy.ndarray
1716
- Array of 6 eigencompliances (eigenvalues of the stiffness matrix)
1717
- numpy.ndarray
1718
- (6,6) array of eigenstresses (eigenvectors of the stiffness matrix)
1719
-
1720
- See Also
1721
- --------
1722
- Kelvin : returns the stiffness components as a (6,6) matrix, according to the Kelvin mapping convention.
1723
- eig_compliances : returns the eigencompliances only
1724
- eig_stresses : returns the eigenstresses only
1725
-
1726
- Notes
1727
- -----
1728
- The definition for eigencompliances and the eigenstresses are introduced in [4]_.
1729
- """
1730
- return np.linalg.eigh(self.to_Kelvin())
1731
-
1732
- @property
1733
- def eig_compliances(self):
1734
- """
1735
- Compute the eigencompliances given by the Kelvin's matrix for stiffness.
1736
-
1737
- Returns
1738
- -------
1739
- numpy.ndarray
1740
- 6 eigenvalues of the Kelvin's compliance matrix, in ascending order
1741
-
1742
- See Also
1743
- --------
1744
- eig : returns the eigencompliances and the eigenstresses
1745
- eig_strains : returns the eigenstresses only
1746
- """
1747
- return np.linalg.eigvalsh(self.to_Kelvin())
1748
-
1749
- @property
1750
- def eig_stresses(self):
1751
- """
1752
- Compute the eigenstresses from the Kelvin's matrix for stiffness
1753
-
1754
- Returns
1755
- -------
1756
- numpy.ndarray
1757
- (6,6) matrix of eigenstresses, sorted by ascending order of eigencompliances.
1758
-
1759
- See Also
1760
- --------
1761
- eig : returns both the eigencompliances and the eigenstresses
1762
- """
1763
- return self.eig()[1]
1764
-
1765
- @property
1766
- def eig_stiffnesses(self):
1767
- """
1768
- Compute the eigenstiffnesses from the Kelvin's matrix of compliance
1769
-
1770
- Returns
1771
- -------
1772
- numpy.ndarray
1773
- inverses of 6 eigenvalues of the Kelvin's compliance matrix, in descending order
1774
-
1775
- See Also
1776
- --------
1777
- eig_compliances : compute the eigencompliances from the Kelvin's matrix of compliance
1778
- """
1779
- return 1/self.eig_compliances
1
+ import warnings
2
+ from Elasticipy.tensors.elasticity import StiffnessTensor as NewStiffnessTensor
3
+ from Elasticipy.tensors.elasticity import ComplianceTensor as NewComplianceTensor
4
+
5
+ warnings.warn(
6
+ "The module 'Elasticipy.FourthOrderTensor' is deprecated and will be removed in a future release. "
7
+ "Please use 'Elasticipy.tensors.elasticity' instead.",
8
+ DeprecationWarning,
9
+ stacklevel=2
10
+ )
11
+
12
+ class StiffnessTensor(NewStiffnessTensor):
13
+ pass
14
+
15
+ class ComplianceTensor(NewComplianceTensor):
16
+ pass