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