elasticipy 2.9.0__py3-none-any.whl → 3.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.
Elasticipy/Plasticity.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  from Elasticipy.StressStrainTensors import StrainTensor, StressTensor
3
3
 
4
+
4
5
  class IsotropicHardening:
5
6
  """
6
7
  Template class for isotropic hardening plasticity models
@@ -11,15 +12,19 @@ class IsotropicHardening:
11
12
 
12
13
  Parameters
13
14
  ----------
14
- criterion : str, optional
15
+ criterion : str or PlasticityCriterion
15
16
  Plasticity criterion to use. Can be 'von Mises', 'Tresca' or 'J2'. J2 is the same as von Mises.
16
17
  """
17
- criterion = criterion.lower()
18
- if criterion in ('von mises', 'mises', 'vonmises', 'j2'):
19
- criterion = 'j2'
20
- elif criterion != 'tresca':
21
- raise ValueError('The criterion can be "Tresca", "von Mises" or "J2".')
22
- self.criterion = criterion.lower()
18
+ if isinstance(criterion, str):
19
+ criterion = criterion.lower()
20
+ if criterion in ('von mises', 'mises', 'vonmises', 'j2'):
21
+ self.criterion = VonMisesPlasticity
22
+ elif criterion == 'tresca':
23
+ self.criterion = TrescaPlasticity
24
+ else:
25
+ raise ValueError('The criterion can be "Tresca", "von Mises" or "J2".')
26
+ else:
27
+ self.criterion = criterion
23
28
  self.plastic_strain = 0.0
24
29
 
25
30
  def flow_stress(self, strain, **kwargs):
@@ -60,12 +65,6 @@ class IsotropicHardening:
60
65
  def reset_strain(self):
61
66
  self.plastic_strain = 0.0
62
67
 
63
- def eq_stress(self, stress):
64
- if self.criterion == 'j2':
65
- return stress.vonMises()
66
- else:
67
- return stress.Tresca()
68
-
69
68
 
70
69
  class JohnsonCook(IsotropicHardening):
71
70
  def __init__(self, A, B, n, C=None, eps_dot_ref=1.0, m=None, T0=25, Tm=None, criterion='von Mises'):
@@ -93,7 +92,7 @@ class JohnsonCook(IsotropicHardening):
93
92
  Reference temperature
94
93
  Tm : float, optional
95
94
  Melting temperature (at which the flow stress is zero)
96
- criterion : str, optional
95
+ criterion : str or PlasticityCriterion, optional
97
96
  Plasticity criterion to use. It can be 'von Mises' or 'Tresca'.
98
97
 
99
98
  Notes
@@ -194,7 +193,7 @@ class JohnsonCook(IsotropicHardening):
194
193
  apply_strain : apply strain to the JC model and updates its hardening value
195
194
  """
196
195
  if isinstance(stress, StressTensor):
197
- eq_stress = self.eq_stress(stress)
196
+ eq_stress = self.criterion.eq_stress(stress)
198
197
  else:
199
198
  eq_stress = stress
200
199
  if T is None:
@@ -223,7 +222,7 @@ class JohnsonCook(IsotropicHardening):
223
222
  self.apply_strain(strain_increment)
224
223
 
225
224
  if isinstance(stress, StressTensor):
226
- n = normality_rule(stress, criterion=criterion)
225
+ n = self.criterion.normal(stress)
227
226
  return n * strain_increment
228
227
  else:
229
228
  return strain_increment
@@ -235,48 +234,99 @@ class JohnsonCook(IsotropicHardening):
235
234
  self.plastic_strain = 0.0
236
235
 
237
236
 
238
- def normality_rule(stress, criterion='von Mises'):
239
- """
240
- Apply the normality rule for plastic flow, given a yield criterion.
241
-
242
- The stress can be a single tensor, or an array of tensors.
243
-
244
- Parameters
245
- ----------
246
- stress : StressTensor
247
- Stress tensor to apply the normality rule from
248
- criterion : str, optional
249
- Name of the criterion to use. Can be either 'von Mises' or 'Tresca'
250
-
251
- Returns
252
- -------
253
- StrainTensor
254
- If a single stress tensor is passed, the returned array will be of shape
255
-
256
- Notes
257
- -----
258
- The singular points for the Tresca criterion are treated as the von Mises criterion, which is equivalent to the
259
- average of the two adjacent normals of the domain.
260
- """
261
- if criterion.lower()=='von mises':
237
+ class PlasticityCriterion:
238
+ @staticmethod
239
+ def eq_stress(stress, **kwargs):
240
+ """
241
+ Return the equivalent stress, with respect to the plasticity criterion.
242
+
243
+ Parameters
244
+ ----------
245
+ stress : StressTensor
246
+ Stress to compute the equivalent stress from
247
+ kwargs : dict
248
+ keyword arguments passed to the function
249
+ Returns
250
+ -------
251
+ float or numpy.ndarray
252
+ """
253
+ pass
254
+
255
+ def normal(self, stress, **kwargs):
256
+ """
257
+ Apply the normality rule
258
+
259
+ Parameters
260
+ ----------
261
+ stress : StressTensor
262
+ Stress tensor to apply the normality rule
263
+ kwargs : dict
264
+ Keyword arguments passed to the function
265
+
266
+ Returns
267
+ -------
268
+ StrainTensor
269
+ Normalized direction of plastic flow
270
+ """
271
+ pass
272
+
273
+ class VonMisesPlasticity(PlasticityCriterion):
274
+ @staticmethod
275
+ def eq_stress(stress, **kwargs):
276
+ return stress.vonMises()
277
+
278
+ @staticmethod
279
+ def normal(stress, **kwargs):
262
280
  eq_stress = stress.vonMises()
263
- dev_stress= stress.deviatoric_part()
281
+ dev_stress = stress.deviatoric_part()
264
282
  gradient_tensor = dev_stress / eq_stress
265
- return StrainTensor(3/2 * gradient_tensor.matrix)
266
- elif criterion.lower()=='tresca':
283
+ return StrainTensor(3 / 2 * gradient_tensor.matrix)
284
+
285
+ class TrescaPlasticity(PlasticityCriterion):
286
+ @staticmethod
287
+ def eq_stress(stress, **kwargs):
288
+ return stress.Tresca()
289
+
290
+ @staticmethod
291
+ def normal(stress, **kwargs):
267
292
  vals, dirs = stress.eig()
268
- u1 = dirs[...,0]
269
- u3 = dirs[...,2]
270
- s1 = vals[...,0]
293
+ u1 = dirs[..., 0]
294
+ u3 = dirs[..., 2]
295
+ s1 = vals[..., 0]
271
296
  s2 = vals[..., 1]
272
- s3 = vals[...,2]
273
- A = np.einsum('...i,...j->...ij',u1, u1)
274
- B = np.einsum('...i,...j->...ij',u3, u3)
275
- normal = A - B
276
- singular_points = np.logical_or(s2==s1, s2==s3)
277
- normal[singular_points] = normality_rule(stress[singular_points], criterion='von Mises').matrix
278
- normal[np.logical_and(s2==s1, s2==s3)] = 0.0
297
+ s3 = vals[..., 2]
298
+ a = np.einsum('...i,...j->...ij', u1, u1)
299
+ b = np.einsum('...i,...j->...ij', u3, u3)
300
+ normal = a - b
301
+ singular_points = np.logical_or(s2 == s1, s2 == s3)
302
+ normal[singular_points] = VonMisesPlasticity().normal(stress[singular_points]).matrix
303
+ normal[np.logical_and(s2 == s1, s2 == s3)] = 0.0
279
304
  strain = StrainTensor(normal)
280
305
  return strain / strain.eq_strain()
281
- else:
282
- raise NotImplementedError('The normality rule is only implemented for von Mises (J2) and Tresca criteria.')
306
+
307
+ class DruckerPrager(PlasticityCriterion):
308
+ def __init__(self, alpha):
309
+ """
310
+ Create a Drucker-Prager (DG) plasticity criterion.
311
+
312
+ Parameters
313
+ ----------
314
+ alpha : float
315
+ Pressure dependence parameters (see notes for details)
316
+
317
+ Notes
318
+ -----
319
+ The pressure-dependent DG plasticity criterion assumes that the equivalent stress is defined as:
320
+
321
+
322
+ """
323
+ self.alpha = alpha
324
+
325
+ def eq_stress(self, stress, **kwargs):
326
+ return (stress.J2**0.5 + self.alpha * stress.I1) / (1/3**0.5 + self.alpha)
327
+
328
+ def normal(self, stress, **kwargs):
329
+ J2 = stress.J2
330
+ gradient = stress.deviatoric_part() / (2 * J2**0.5) + self.alpha * StressTensor.eye(stress.shape)
331
+ strain = StrainTensor(gradient.matrix)
332
+ return strain / strain.eq_strain()
@@ -1,7 +1,9 @@
1
+ import warnings
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
  from scipy.spatial.transform import Rotation
4
-
6
+ ALPHABET = 'abcdefghijklmnopqrstuv'
5
7
 
6
8
  class _MatrixProxy:
7
9
  def __init__(self, matrix):
@@ -36,6 +38,45 @@ def _transpose_matrix(matrix):
36
38
  def _symmetric_part(matrix):
37
39
  return 0.5 * (matrix + _transpose_matrix(matrix))
38
40
 
41
+ def _orientation_shape(g):
42
+ if is_orix_rotation(g):
43
+ return g.shape
44
+ else:
45
+ return (len(g),)
46
+
47
+ def _is_single_rotation(rotation):
48
+ if isinstance(rotation, Rotation):
49
+ return rotation.single
50
+ elif is_orix_rotation(rotation):
51
+ return rotation.size == 1
52
+ else:
53
+ raise TypeError('The input argument must be of class scipy.transform.Rotation or '
54
+ 'orix.quaternion.rotation.Rotation')
55
+
56
+ _voigt_numbering = [[0, 0], [1, 1], [2, 2], [1, 2], [0, 2], [0, 1]]
57
+
58
+ def _unmap(array, mapping_convention):
59
+ array = np.asarray(array)
60
+ shape = array.shape
61
+ if shape and (shape[-1] == 6):
62
+ new_shape = shape[:-1] + (3, 3)
63
+ unmapped_matrix = np.zeros(new_shape)
64
+ for i in range(6):
65
+ unmapped_matrix[..., _voigt_numbering[i][0], _voigt_numbering[i][1]] = array[..., i] / mapping_convention[i]
66
+ return unmapped_matrix
67
+ else:
68
+ raise ValueError("array must be of shape (6,) or (...,6) with Voigt vector")
69
+
70
+ def _map(matrix, mapping_convention):
71
+ shape = matrix.shape[:-2] + (6,)
72
+ array = np.zeros(shape)
73
+ for i in range(6):
74
+ j, k = _voigt_numbering[i]
75
+ array[...,i] = matrix[...,j,k]
76
+ return array * mapping_convention
77
+
78
+ kelvin_mapping = [1, 1, 1, np.sqrt(2), np.sqrt(2), np.sqrt(2)]
79
+
39
80
  class SecondOrderTensor:
40
81
  """
41
82
  Template class for manipulation of second order tensors or arrays of second order tensors
@@ -251,7 +292,7 @@ class SecondOrderTensor:
251
292
  I3 : Third invariant of the tensors (det)
252
293
  """
253
294
  a = self.I1**2
254
- b = np.matmul(self.matrix, self._transposeTensor()).trace(axis1=-1, axis2=-2)
295
+ b = np.matmul(self.matrix, self._transpose_tensor()).trace(axis1=-1, axis2=-2)
255
296
  return 0.5 * (a - b)
256
297
 
257
298
  @property
@@ -391,13 +432,9 @@ class SecondOrderTensor:
391
432
  matmul : matrix-like multiplication of tensor arrays
392
433
  """
393
434
  if isinstance(B, SecondOrderTensor):
394
- new_mat = np.matmul(self.matrix, B.matrix)
395
- return SecondOrderTensor(new_mat)
435
+ return self.dot(B, mode='pair')
396
436
  elif isinstance(B, Rotation) or is_orix_rotation(B):
397
- rotation_matrices, transpose_matrices = rotation_to_matrix(B, return_transpose=True)
398
- new_matrix = np.matmul(np.matmul(transpose_matrices, self.matrix), rotation_matrices)
399
- # In case of rotation, the property of the transformed tensor is kept
400
- return self.__class__(new_matrix)
437
+ return self.rotate(B, mode='pair')
401
438
  elif isinstance(B, (float, int)):
402
439
  return self.__class__(self.matrix * B)
403
440
  elif isinstance(B, np.ndarray):
@@ -413,6 +450,44 @@ class SecondOrderTensor:
413
450
  else:
414
451
  raise ValueError('The input argument must be a tensor, an ndarray, a rotation or a scalar value.')
415
452
 
453
+ def rotate(self, rotation, mode='pair'):
454
+ """
455
+ Apply rotation(s) to the tensor(s).
456
+
457
+ The rotations can be applied element-wise, or on each cross-combination (see below).
458
+
459
+ Parameters
460
+ ----------
461
+ rotation : scipy.spatial.Rotation or orix.quaternion.Rotation
462
+ mode : str, optional
463
+ If 'pair', the rotations are applied element wise. Broadcasting rule applies.
464
+ If 'cross', all the possible combinations are considered. If ``C=A.rotate(rot)``, then
465
+ ``C.shape==A.shape + rot.shape``.
466
+
467
+ Returns
468
+ -------
469
+ SecondOrderTensor
470
+ """
471
+ if self.shape == ():
472
+ ein_str = '...li,...kj,lk->...ij'
473
+ elif _is_single_rotation(rotation):
474
+ ein_str = 'li,kj,...lk->...ij'
475
+ else:
476
+ if mode=='pair':
477
+ ein_str = '...li,...kj,...lk->...ij'
478
+ elif mode=='cross':
479
+ ndim_0 = self.ndim
480
+ ndim_1 = len(_orientation_shape(rotation))
481
+ indices_self = ALPHABET[:ndim_0]
482
+ indices_g = ALPHABET[:ndim_1].upper()
483
+ indices_res = indices_self + indices_g
484
+ ein_str = indices_g + 'zw,' + indices_g + 'yx,' + indices_self + 'zy->' + indices_res + 'wx'
485
+ else:
486
+ raise ValueError('Invalid mode. It can be "cross" or "pair".')
487
+ g_mat = rotation_to_matrix(rotation)
488
+ matrix = np.einsum(ein_str, g_mat, g_mat, self.matrix)
489
+ return self.__class__(matrix)
490
+
416
491
  def __rmul__(self, other):
417
492
  if isinstance(other, (float, int)):
418
493
  return self.__mul__(other)
@@ -431,7 +506,7 @@ class SecondOrderTensor:
431
506
  raise NotImplementedError('Tensors can only be divided by scalar values or by arrays of the same shape.')
432
507
  return self.__class__(new_mat)
433
508
 
434
- def __eq__(self, other) -> np.ndarray:
509
+ def __eq__(self, other):
435
510
  """
436
511
  Check whether the tensors in the tensor array are equal
437
512
 
@@ -442,7 +517,7 @@ class SecondOrderTensor:
442
517
 
443
518
  Returns
444
519
  -------
445
- np.array of bool
520
+ numpy.ndarray
446
521
  True element is True if the corresponding tensors are equal.
447
522
  """
448
523
  if isinstance(other, SecondOrderTensor):
@@ -453,6 +528,73 @@ class SecondOrderTensor:
453
528
  else:
454
529
  raise ValueError('The value to compare must be an array of shape {} or {}'.format(self.shape, self.shape + (3,3)))
455
530
 
531
+ def dot(self, other, mode='pair'):
532
+ """
533
+ Perform contraction product ("dot product") between tensor.
534
+
535
+ On tensor arrays, the product contraction can be performed element-wise, or considering all cross-combinations
536
+ (see below).
537
+
538
+ Parameters
539
+ ----------
540
+ other : SecondOrderTensor
541
+ tensor or tensor array to compute the product from
542
+ mode : str, optional
543
+ If 'pair' (default), the contraction products of tensor arrays are applied element-wise. Broadcasting rule
544
+ applies.
545
+
546
+ If 'cross', all combinations of contraction product are considered. If ``C=A.dot(B,mode='cross')``, then
547
+ ``C.shape==A.shape + B.shape``
548
+
549
+ Returns
550
+ -------
551
+ SecondOrderTensor
552
+
553
+ Examples
554
+ --------
555
+ >>> from Elasticipy.SecondOrderTensor import SecondOrderTensor
556
+ >>> A=SecondOrderTensor.rand(10)
557
+ >>> B=SecondOrderTensor.rand(10)
558
+ >>> AB_pair = A.dot(B)
559
+ >>> AB_pair.shape
560
+ (10,)
561
+
562
+ >>> AB_cross = A.dot(B, mode='cross')
563
+ >>> AB_cross.shape
564
+ (10, 10)
565
+
566
+ We can for instance check that:
567
+
568
+ >>> AB_pair[5] == A[5].dot(B[5])
569
+ True
570
+
571
+ and:
572
+
573
+ >>> AB_cross[0,1] == A[0].dot(B[1])
574
+ True
575
+
576
+ See Also
577
+ --------
578
+ ddot : Double-contraction product
579
+ """
580
+ if self.shape == ():
581
+ ein_str = 'ik,...kj->...ij'
582
+ else:
583
+ if mode=='pair':
584
+ ein_str = '...ik,...kj->...ij'
585
+ elif mode=='cross':
586
+ ndim_0 = self.ndim
587
+ ndim_1 = other.ndim
588
+ indices_0 = ALPHABET[:ndim_0]
589
+ indices_1 = ALPHABET[:ndim_1].upper()
590
+ indices_2 = indices_0 + indices_1
591
+ ein_str = indices_0 + 'ik,' + indices_1 + 'kj->' + indices_2 + 'ij'
592
+ else:
593
+ raise ValueError('Invalid mode. Use "pair" or "cross".')
594
+ matrix = np.einsum(ein_str, self.matrix, other.matrix)
595
+ return SecondOrderTensor(matrix)
596
+
597
+
456
598
  def matmul(self, other):
457
599
  """
458
600
  Perform matrix-like product between tensor arrays. Each "product" is a matrix product between
@@ -481,28 +623,19 @@ class SecondOrderTensor:
481
623
  --------
482
624
  __mul__ : Element-wise matrix product
483
625
  """
626
+ warnings.warn(
627
+ 'matmul() is deprecated and will be removed in a future version. Use dot(tensor,mode="cross") or '
628
+ 'rotate(rotation,mode="cross") instead.',
629
+ DeprecationWarning,
630
+ stacklevel=2)
484
631
  if isinstance(other, SecondOrderTensor):
485
- other_matrix = other.matrix
632
+ return self.dot(other, mode='cross')
486
633
  elif isinstance(other, Rotation) or is_orix_rotation(Rotation):
487
- other_matrix = rotation_to_matrix(other)
488
- else:
489
- other_matrix = other
490
- matrix = self.matrix
491
- shape_matrix = matrix.shape[:-2]
492
- shape_other = other_matrix.shape[:-2]
493
- extra_dim_matrix = len(shape_other)
494
- extra_dim_other = len(shape_matrix)
495
- matrix_expanded = matrix.reshape(shape_matrix + (1,) * extra_dim_other + (3, 3))
496
- other_expanded = other_matrix.reshape((1,) * extra_dim_matrix + shape_other + (3, 3))
497
- if isinstance(other, Rotation):
498
- other_expanded_t = _transpose_matrix(other_expanded)
499
- new_mat = np.matmul(np.matmul(other_expanded_t, matrix_expanded), other_expanded)
500
- return self.__class__(np.squeeze(new_mat))
634
+ return self.rotate(other, mode='cross')
501
635
  else:
502
- new_mat = np.matmul(matrix_expanded, other_expanded)
503
- return SecondOrderTensor(np.squeeze(new_mat))
636
+ raise ValueError('The input argument must be either a rotation or a SecondOrderTensor')
504
637
 
505
- def transposeArray(self):
638
+ def transpose_array(self):
506
639
  """
507
640
  Transpose the array of tensors
508
641
 
@@ -531,19 +664,19 @@ class SecondOrderTensor:
531
664
  """
532
665
  Transpose the array of tensors.
533
666
 
534
- It is actually an alias for transposeArray()
667
+ It is actually an alias for transpose_array()
535
668
 
536
669
  Returns
537
670
  -------
538
671
  SecondOrderTensor
539
672
  Transposed array
540
673
  """
541
- return self.transposeArray()
674
+ return self.transpose_array()
542
675
 
543
- def _transposeTensor(self):
676
+ def _transpose_tensor(self):
544
677
  return _transpose_matrix(self.matrix)
545
678
 
546
- def transposeTensor(self):
679
+ def transpose_tensor(self):
547
680
  """
548
681
  Transpose of tensors of the tensor array
549
682
 
@@ -554,11 +687,11 @@ class SecondOrderTensor:
554
687
 
555
688
  See Also
556
689
  --------
557
- Transpose : transpose the array (not the components)
690
+ transpose_array : transpose the array (not the components)
558
691
  """
559
- return self.__class__(self._transposeTensor())
692
+ return self.__class__(self._transpose_tensor())
560
693
 
561
- def ddot(self, other):
694
+ def ddot(self, other, mode='pair'):
562
695
  """
563
696
  Double dot product (contraction of tensor product, usually denoted ":") of two tensors.
564
697
 
@@ -570,6 +703,11 @@ class SecondOrderTensor:
570
703
  ----------
571
704
  other : SecondOrderTensor or np.ndarray
572
705
  Tensor or tensor array to multiply by before contraction.
706
+ mode : str, optional
707
+ If "pair", the dot products are performed element-wise before contraction. Broadcasting rule applies.
708
+ If "cross", all the cross-combinations are computed, increasing the dimensionality.
709
+ If ``C=A.ddot(B, mode='cross')``, then ``C.shape = A.shape + B.shape``.
710
+
573
711
 
574
712
  Returns
575
713
  -------
@@ -578,10 +716,10 @@ class SecondOrderTensor:
578
716
 
579
717
  See Also
580
718
  --------
581
- matmul : matrix-like product between two tensor arrays.
719
+ dot : contraction product ("dot product") between tensor.
582
720
 
583
721
  """
584
- tensor_prod = self.transposeTensor()*other
722
+ tensor_prod = self.transpose_tensor().dot(other, mode=mode)
585
723
  return tensor_prod.trace()
586
724
 
587
725
  def _flatten(self):
@@ -750,7 +888,7 @@ class SecondOrderTensor:
750
888
  return self
751
889
 
752
890
  def _symmetric_part(self):
753
- return 0.5 * (self.matrix + self._transposeTensor())
891
+ return 0.5 * (self.matrix + self._transpose_tensor())
754
892
 
755
893
  def symmetric_part(self):
756
894
  """
@@ -776,7 +914,7 @@ class SecondOrderTensor:
776
914
  SkewSymmetricSecondOrderTensor
777
915
  Skew-symmetric tensor
778
916
  """
779
- new_mat = 0.5 * (self.matrix - self._transposeTensor())
917
+ new_mat = 0.5 * (self.matrix - self._transpose_tensor())
780
918
  return SkewSymmetricSecondOrderTensor(new_mat)
781
919
 
782
920
  def spherical_part(self):
@@ -922,7 +1060,7 @@ class SecondOrderTensor:
922
1060
 
923
1061
  Parameters
924
1062
  ----------
925
- shape : tuple, optional
1063
+ shape : int or tuple, optional
926
1064
  Shape of the tensor array. If not provided, a single tensor is returned
927
1065
  seed : int, optional
928
1066
  Sets the seed for random generation. Useful to ensure reproducibility
@@ -954,6 +1092,8 @@ class SecondOrderTensor:
954
1092
  """
955
1093
  if shape is None:
956
1094
  shape = (3,3)
1095
+ elif isinstance(shape, int):
1096
+ shape = (shape, 3, 3)
957
1097
  else:
958
1098
  shape = shape + (3,3)
959
1099
  rng = np.random.default_rng(seed)
@@ -1281,7 +1421,10 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
1281
1421
  >>> a==b
1282
1422
  True
1283
1423
  """
1284
- mat = np.asarray(mat, dtype=float)
1424
+ if isinstance(mat, SecondOrderTensor):
1425
+ mat = mat.matrix
1426
+ else:
1427
+ mat = np.asarray(mat, dtype=float)
1285
1428
  mat_transposed = _transpose_matrix(mat)
1286
1429
  if np.all(np.isclose(mat, mat_transposed)) or force_symmetry:
1287
1430
  # The input matrix is symmetric
@@ -1308,10 +1451,15 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
1308
1451
  ----------
1309
1452
  array : np.ndarray or list
1310
1453
  array to build the SymmetricSecondOrderTensor from. We must have array.ndim>0 and array.shape[-1]==6.
1454
+
1311
1455
  Returns
1312
1456
  -------
1313
1457
  SymmetricSecondOrderTensor
1314
1458
 
1459
+ See Also
1460
+ --------
1461
+ from_Kelvin : Construct a tensor from vector(s) following the Kelvin notation
1462
+
1315
1463
  Examples
1316
1464
  --------
1317
1465
  >>> from Elasticipy.SecondOrderTensor import SymmetricSecondOrderTensor
@@ -1320,23 +1468,62 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
1320
1468
  [[11. 12. 13.]
1321
1469
  [12. 22. 23.]
1322
1470
  [13. 23. 33.]]
1471
+ """
1472
+ matrix = _unmap(array, cls.voigt_map)
1473
+ return cls(matrix)
1323
1474
 
1475
+ def to_Voigt(self):
1324
1476
  """
1325
- array = np.asarray(array)
1326
- shape = array.shape
1327
- if shape and (shape[-1] == 6):
1328
- new_shape = shape[:-1] + (3, 3)
1329
- unvoigted_matrix = np.zeros(new_shape)
1330
- voigt = [[0, 0], [1, 1], [2, 2], [1, 2], [0, 2], [0, 1]]
1331
- for i in range(6):
1332
- unvoigted_matrix[..., voigt[i][0], voigt[i][1]] = array[..., i] / cls.voigt_map[i]
1333
- return cls(unvoigted_matrix)
1334
- else:
1335
- raise ValueError("array must be of shape (6,) or (...,6) with Voigt vector")
1477
+ Convert the tensor to vector, or slices of vector, following the Voigt convention.
1478
+
1479
+ If the tensor array has shape (m,n,...), the result will be of shape (m,n,...,6).
1480
+
1481
+ Returns
1482
+ -------
1483
+ numpy.ndarray
1484
+ Voigt vector summarizing the components
1485
+ """
1486
+ return _map(self.matrix, self.voigt_map)
1487
+
1488
+ @classmethod
1489
+ def from_Kelvin(cls, array):
1490
+ """
1491
+ Build a tensor from the Kelvin vector, or slices of Kelvin vectors
1492
+
1493
+ Parameters
1494
+ ----------
1495
+ array : np.ndarray or list
1496
+ Vectors, or slices of vectors, consisting in components following the Kelvin convention
1497
+ Returns
1498
+ -------
1499
+ SymmetricSecondOrderTensor
1500
+
1501
+ See Also
1502
+ --------
1503
+ from_Voigt : construct a tensor from vector(s) following the Voigt notation
1504
+ to_Kelvin : convert the tensor to vector(s) following the Kelvin convention
1505
+ """
1506
+ matrix = _unmap(array, kelvin_mapping)
1507
+ return cls(matrix)
1508
+
1509
+ def to_Kelvin(self):
1510
+ """
1511
+ Convert the tensor to vector, or slices of vector, following the Kelvin(-Mandel) convention.
1512
+
1513
+ Returns
1514
+ -------
1515
+ numpy.ndarray
1516
+
1517
+ See Also
1518
+ --------
1519
+ from_Kelvin : Construct a tensor from vector(s) following the Kelvin convention
1520
+ to_Voigt : Convert the tensor to vector(s) following the Voigt convention
1521
+ """
1522
+ return _map(self.matrix, kelvin_mapping)
1336
1523
 
1337
1524
  def eig(self):
1338
1525
  """
1339
- Compute the principal values (eigenvaleues) and principal direction (eigenvectors) of the tensor, sorted in
1526
+ Compute the principal values (eigenvalues) and principal direction (eigenvectors) of the tensor, sorted in
1340
1527
  descending order of principal values
1341
1528
 
1342
1529
  Returns
@@ -1351,7 +1538,7 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
1351
1538
  eigvals : compute the principal values only
1352
1539
  """
1353
1540
  eigvals, eigdir = np.linalg.eigh(self.matrix)
1354
- return np.flip(eigvals,axis=-1), np.flip(eigdir,axis=-1)
1541
+ return eigvals[..., ::-1], eigdir[..., :, ::-1]
1355
1542
 
1356
1543
  def eigvals(self):
1357
1544
  """
@@ -1437,6 +1624,8 @@ def rotation_to_matrix(rotation, return_transpose=False):
1437
1624
  elif is_orix_rotation(rotation):
1438
1625
  inv_rotation = ~rotation
1439
1626
  matrix = inv_rotation.to_matrix()
1627
+ if matrix.shape == (1,3,3):
1628
+ matrix = matrix[0]
1440
1629
  else:
1441
1630
  raise TypeError('The input argument must be of class scipy.transform.Rotation or '
1442
1631
  'orix.quaternion.rotation.Rotation')