elasticipy 2.9.0__py3-none-any.whl → 4.0.0__py3-none-any.whl

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