elasticipy 4.0.0__py3-none-any.whl → 4.1.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.
@@ -3,14 +3,7 @@ from Elasticipy.tensors.second_order import SymmetricSecondOrderTensor, rotation
3
3
  SecondOrderTensor, ALPHABET
4
4
  from scipy.spatial.transform import Rotation
5
5
  from copy import deepcopy
6
-
7
- a = np.sqrt(2)
8
- KELVIN_MAPPING_MATRIX = np.array([[1, 1, 1, a, a, a],
9
- [1, 1, 1, a, a, a],
10
- [1, 1, 1, a, a, a],
11
- [a, a, a, 2, 2, 2],
12
- [a, a, a, 2, 2, 2],
13
- [a, a, a, 2, 2, 2], ])
6
+ from Elasticipy.tensors.mapping import KelvinMapping, VoigtMapping
14
7
 
15
8
 
16
9
  def voigt_indices(i, j):
@@ -77,12 +70,12 @@ class FourthOrderTensor:
77
70
 
78
71
  Attributes
79
72
  ----------
80
- matrix : np.ndarray
73
+ _matrix : np.ndarray
81
74
  (6,6) matrix gathering all the components of the tensor, using the Voigt notation.
82
75
  """
83
76
  tensor_name = '4th-order'
84
77
 
85
- def __init__(self, M, mapping='Kelvin', check_minor_symmetry=True, force_minor_symmetry=False):
78
+ def __init__(self, M, mapping=KelvinMapping(), check_minor_symmetry=True, force_minor_symmetry=False):
86
79
  """
87
80
  Construct of Fourth-order tensor with minor symmetry.
88
81
 
@@ -91,7 +84,7 @@ class FourthOrderTensor:
91
84
  M : np.ndarray
92
85
  (6,6) matrix corresponding to the stiffness tensor, written using the Voigt notation, or array of shape
93
86
  (3,3,3,3).
94
- mapping : str or list of list, or numpy/ndarray, optional
87
+ mapping : MappingConvention, optional
95
88
  Mapping convention to translate the (3,3,3,3) array to (6,6) matrix
96
89
  check_minor_symmetry : bool, optional
97
90
  If true (default), check that the input array have minor symmetries (see Notes). Only used if an array of
@@ -108,19 +101,7 @@ class FourthOrderTensor:
108
101
  M_{ijkl}=M_{jikl}=M_{jilk}=M_{ijlk}
109
102
 
110
103
  """
111
- if isinstance(mapping, (list, tuple, np.ndarray)):
112
- self.mapping_matrix = mapping
113
- self.mapping_name = 'custom'
114
- else:
115
- mapping = mapping.capitalize()
116
- self.mapping_name = mapping
117
- if mapping == 'Kelvin':
118
- self.mapping_matrix = KELVIN_MAPPING_MATRIX
119
- elif mapping == 'Voigt':
120
- self.mapping_matrix = np.ones((6,6))
121
- else:
122
- raise ValueError('The mapping to use can be either "Kelvin", "Voigt", or a (6,6) matrix.')
123
-
104
+ self.mapping=mapping
124
105
  M = np.asarray(M)
125
106
  if M.shape[-2:] == (6, 6):
126
107
  matrix = M
@@ -137,11 +118,11 @@ class FourthOrderTensor:
137
118
  matrix = self._full_to_matrix(M)
138
119
  else:
139
120
  raise ValueError('The input matrix must of shape (...,6,6) or (...,3,3,3,3)')
140
- self.matrix = matrix
121
+ self._matrix = matrix
141
122
  for i in range(0, 6):
142
123
  for j in range(0, 6):
143
124
  def getter(obj, I=i, J=j):
144
- return obj.matrix[...,I, J]
125
+ return obj._matrix[...,I, J]
145
126
 
146
127
  getter.__doc__ = f"Returns the ({i + 1},{j + 1}) component of the {self.tensor_name} matrix."
147
128
  component_name = 'C{}{}'.format(i + 1, j + 1)
@@ -149,8 +130,8 @@ class FourthOrderTensor:
149
130
 
150
131
  def __repr__(self):
151
132
  if (self.ndim == 0) or ((self.ndim==1) and self.shape[0]<5):
152
- msg = '{} tensor (in {} mapping):\n'.format(self.tensor_name, self.mapping_name)
153
- msg += self.matrix.__str__()
133
+ msg = '{} tensor (in {} mapping):\n'.format(self.tensor_name, self.mapping.name)
134
+ msg += self._matrix.__str__()
154
135
  else:
155
136
  msg = '{} tensor array of shape {}'.format(self.tensor_name, self.shape)
156
137
  return msg
@@ -164,7 +145,7 @@ class FourthOrderTensor:
164
145
  tuple
165
146
  Shape of the tensor array
166
147
  """
167
- *shape, _, _ = self.matrix.shape
148
+ *shape, _, _ = self._matrix.shape
168
149
  return tuple(shape)
169
150
 
170
151
  def full_tensor(self):
@@ -179,7 +160,7 @@ class FourthOrderTensor:
179
160
  i, j, k, ell = np.indices((3, 3, 3, 3))
180
161
  ij = voigt_indices(i, j)
181
162
  kl = voigt_indices(k, ell)
182
- m = self.matrix[..., ij, kl] / self.mapping_matrix[ij, kl]
163
+ m = self._matrix[..., ij, kl] / self.mapping.matrix[ij, kl]
183
164
  return m
184
165
 
185
166
  def flatten(self):
@@ -197,7 +178,7 @@ class FourthOrderTensor:
197
178
  if shape:
198
179
  t2 = deepcopy(self)
199
180
  p = (np.prod(self.shape), 6, 6)
200
- t2.matrix = self.matrix.reshape(p)
181
+ t2._matrix = self._matrix.reshape(p)
201
182
  return t2
202
183
  else:
203
184
  return self
@@ -206,7 +187,7 @@ class FourthOrderTensor:
206
187
  kl, ij = np.indices((6, 6))
207
188
  i, j = unvoigt_index(ij).T
208
189
  k, ell = unvoigt_index(kl).T
209
- return full_tensor[..., i, j, k, ell] * self.mapping_matrix[ij, kl]
190
+ return full_tensor[..., i, j, k, ell] * self.mapping.matrix[ij, kl]
210
191
 
211
192
  def rotate(self, rotation):
212
193
  """
@@ -224,7 +205,7 @@ class FourthOrderTensor:
224
205
  """
225
206
  t2 = deepcopy(self)
226
207
  rotated_tensor = rotate_tensor(self.full_tensor(), rotation)
227
- t2.matrix = self._full_to_matrix(rotated_tensor)
208
+ t2._matrix = self._full_to_matrix(rotated_tensor)
228
209
  return t2
229
210
 
230
211
  @property
@@ -261,13 +242,13 @@ class FourthOrderTensor:
261
242
  t2 = deepcopy(self)
262
243
  if axis is None:
263
244
  axis = tuple([i for i in range(0,self.ndim)])
264
- t2.matrix = np.mean(self.matrix, axis=axis)
245
+ t2._matrix = np.mean(self._matrix, axis=axis)
265
246
  return t2
266
247
 
267
248
  def __add__(self, other):
268
249
  if isinstance(other, np.ndarray):
269
250
  if other.shape == (6, 6):
270
- mat = self.matrix + other
251
+ mat = self._matrix + other
271
252
  elif other.shape == (3, 3, 3, 3):
272
253
  mat = self._full_to_matrix(self.full_tensor() + other)
273
254
  else:
@@ -279,17 +260,17 @@ class FourthOrderTensor:
279
260
  raise ValueError('The two tensors to add must be of the same class.')
280
261
  else:
281
262
  raise ValueError('I don''t know how to add {} with {}.'.format(type(self), type(other)))
282
- return self.__class__(mat, mapping=self.mapping_matrix)
263
+ return self.__class__(mat, mapping=self.mapping)
283
264
 
284
265
  def __sub__(self, other):
285
266
  if isinstance(other, FourthOrderTensor):
286
- return self.__add__(-other.matrix)
267
+ return self.__add__(-other._matrix)
287
268
  else:
288
269
  return self.__add__(-other)
289
270
 
290
271
  def __neg__(self):
291
272
  t = deepcopy(self)
292
- t.matrix = -t.matrix
273
+ t._matrix = -t._matrix
293
274
  return t
294
275
 
295
276
  def ddot(self, other, mode='pair'):
@@ -348,14 +329,14 @@ class FourthOrderTensor:
348
329
  elif isinstance(other, np.ndarray):
349
330
  shape = other.shape
350
331
  if other.shape == self.shape[-len(shape):]:
351
- matrix = self.matrix * other[...,np.newaxis, np.newaxis]
332
+ matrix = self._matrix * other[...,np.newaxis, np.newaxis]
352
333
  return self.__class__(matrix)
353
334
  else:
354
335
  raise ValueError('The arrays to multiply could not be broadcasted with shapes {} and {}'.format(self.shape, other.shape[:-2]))
355
336
  elif isinstance(other, Rotation) or is_orix_rotation(other):
356
337
  return self.rotate(other)
357
338
  else:
358
- return self.__class__(self.matrix * other)
339
+ return self.__class__(self._matrix * other)
359
340
 
360
341
  def __truediv__(self, other):
361
342
  if isinstance(other, (SecondOrderTensor, FourthOrderTensor)):
@@ -378,7 +359,7 @@ class FourthOrderTensor:
378
359
  return self
379
360
  else:
380
361
  new_axes = tuple(range(ndim))[::-1] + (ndim, ndim + 1)
381
- transposed_matrix = self.matrix.transpose(new_axes)
362
+ transposed_matrix = self._matrix.transpose(new_axes)
382
363
  return self.__class__(transposed_matrix)
383
364
 
384
365
  def __rmul__(self, other):
@@ -389,16 +370,16 @@ class FourthOrderTensor:
389
370
 
390
371
  def __eq__(self, other):
391
372
  if isinstance(other, FourthOrderTensor):
392
- return np.all(self.matrix == other.matrix, axis=(-1,-2))
373
+ return np.all(self._matrix == other._matrix, axis=(-1, -2))
393
374
  elif isinstance(other, (float, int)) or (isinstance(other, np.ndarray) and other.shape[-2:] == (6, 6)):
394
- return np.all(self.matrix == other, axis=(-1,-2))
375
+ return np.all(self._matrix == other, axis=(-1, -2))
395
376
  else:
396
377
  raise NotImplementedError('The element to compare with must be a fourth-order tensor '
397
378
  'or an array of shape (6,6).')
398
379
 
399
380
  def __getitem__(self, item):
400
381
  if self.ndim:
401
- sub_mat= self.matrix[item]
382
+ sub_mat= self._matrix[item]
402
383
  if sub_mat.shape[-2:] != (6,6):
403
384
  raise IndexError('Too many indices for tensor array: array is {}-dimensional, but {} were provided'.format(self.ndim, len(item)))
404
385
  else:
@@ -409,19 +390,19 @@ class FourthOrderTensor:
409
390
  def __setitem__(self, index, value):
410
391
  if isinstance(value, np.ndarray):
411
392
  if value.shape[-2:] == (6,6):
412
- self.matrix[index] = value
393
+ self._matrix[index] = value
413
394
  elif value.shape[-4:] == (3,3,3,3):
414
395
  submatrix = self._full_to_matrix(value)
415
- self.matrix[index] = submatrix
396
+ self._matrix[index] = submatrix
416
397
  else:
417
398
  return ValueError('The R.h.s must be either of shape (...,6,6) or (...,3,3,3,3)')
418
399
  elif isinstance(value, FourthOrderTensor):
419
- self.matrix[index] = value.matrix / value.mapping_matrix * self.mapping_matrix
400
+ self._matrix[index] = value._matrix / value.mapping.matrix * self.mapping.matrix
420
401
  else:
421
402
  raise NotImplementedError('The r.h.s must be either an ndarray or an object of class {}'.format(self.__class__))
422
403
 
423
404
  @classmethod
424
- def identity(cls, shape=(), return_full_tensor=False, mapping='Kelvin'):
405
+ def identity(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
425
406
  """
426
407
  Create a 4th-order identity tensor
427
408
 
@@ -432,6 +413,8 @@ class FourthOrderTensor:
432
413
  return_full_tensor : bool, optional
433
414
  If True, return the full tensor as a (3,3,3,3) or a (...,3,3,3,3) array. Otherwise, the tensor is returned
434
415
  as a SymmetricTensor object.
416
+ mapping : str, optional
417
+ Mapping convention to use. Must be either Kelvin or Voigt.
435
418
 
436
419
  Returns
437
420
  -------
@@ -452,6 +435,82 @@ class FourthOrderTensor:
452
435
  else:
453
436
  return cls(full, mapping=mapping)
454
437
 
438
+ @classmethod
439
+ def identity_spherical_part(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
440
+ """
441
+ Return the spherical part of the identity tensor
442
+
443
+ Parameters
444
+ ----------
445
+ shape : tuple of int, optional
446
+ Shape of the tensor to create
447
+ return_full_tensor : bool, optional
448
+ if true, the full tensor is returned as a (3,3,3,3) or a (...,3,3,3,3) array
449
+ mapping : str, optional
450
+ Mapping convention to use. Must be either Kelvin or Voigt.
451
+
452
+ Returns
453
+ -------
454
+ FourthOrderTensor or SymmetricTensor
455
+ """
456
+ eye = np.eye(3)
457
+ if isinstance(shape, int):
458
+ shape = (shape,)
459
+ if len(shape):
460
+ for n in np.flip(shape):
461
+ eye = np.repeat(eye[np.newaxis,...], n, axis=0)
462
+ J = np.einsum('...ij,...kl->...ijkl',eye, eye)/3
463
+ if return_full_tensor:
464
+ return J
465
+ else:
466
+ return FourthOrderTensor(J, mapping=mapping)
467
+
468
+ @classmethod
469
+ def identity_deviatoric_part(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
470
+ """
471
+ Return the deviatoric part of the identity tensor
472
+
473
+ Parameters
474
+ ----------
475
+ shape : tuple of int, optional
476
+ Shape of the tensor to create
477
+ return_full_tensor : bool, optional
478
+ if true, the full tensor is returned as a (3,3,3,3) or a (...,3,3,3,3) array
479
+ mapping : str, optional
480
+ Mapping convention to use. Must be either Kelvin or Voigt.
481
+
482
+ Returns
483
+ -------
484
+ FourthOrderTensor or SymmetricTensor
485
+ """
486
+ I = FourthOrderTensor.identity(shape, return_full_tensor, mapping)
487
+ J = FourthOrderTensor.identity_spherical_part(shape, return_full_tensor, mapping)
488
+ return I-J
489
+
490
+ def spherical_part(self):
491
+ """
492
+ Return the spherical part of the tensor
493
+
494
+ Returns
495
+ -------
496
+ FourthOrderTensor
497
+ Spherical part of the tensor
498
+ """
499
+ I = self.identity_spherical_part(shape=self.shape)
500
+ return I.ddot(self)
501
+
502
+ def deviatoric_part(self):
503
+ """
504
+ Return the deviatoric part of the tensor
505
+
506
+ Returns
507
+ -------
508
+ FourthOrderTensor
509
+ Deviatoric part of the tensor
510
+ """
511
+ K = self.identity_deviatoric_part(shape=self.shape)
512
+ return K.ddot(self)
513
+
455
514
  def inv(self):
456
515
  """
457
516
  Invert the tensor. The inverted tensors inherits the properties (if any)
@@ -461,10 +520,8 @@ class FourthOrderTensor:
461
520
  FourthOrderTensor
462
521
  Inverse tensor
463
522
  """
464
- t2 = deepcopy(self)
465
- new_matrix = np.linalg.inv(self.matrix)
466
- t2.matrix = new_matrix
467
- return t2
523
+ matrix_inv = np.linalg.inv(self._matrix)
524
+ return self.__class__(matrix_inv, mapping=self.mapping.mapping_inverse)
468
525
 
469
526
  @classmethod
470
527
  def zeros(cls, shape=()):
@@ -486,6 +543,20 @@ class FourthOrderTensor:
486
543
  zeros = np.zeros(shape)
487
544
  return cls(zeros)
488
545
 
546
+ def matrix(self, mapping_convention=None):
547
+ matrix = self._matrix
548
+ if mapping_convention is None:
549
+ return matrix
550
+ else:
551
+ if isinstance(mapping_convention, str):
552
+ if mapping_convention.lower() == 'voigt':
553
+ mapping_convention = VoigtMapping()
554
+ elif mapping_convention.lower() == 'kelvin':
555
+ mapping_convention = KelvinMapping()
556
+ else:
557
+ raise ValueError('Mapping convention must be either Kelvin or Voigt')
558
+ return matrix / self.mapping._matrix * mapping_convention.matrix
559
+
489
560
  class SymmetricFourthOrderTensor(FourthOrderTensor):
490
561
  tensor_name = 'Symmetric 4th-order'
491
562
 
@@ -521,8 +592,8 @@ class SymmetricFourthOrderTensor(FourthOrderTensor):
521
592
  """
522
593
  super().__init__(M, check_minor_symmetry=check_symmetries, force_minor_symmetry=force_symmetries, **kwargs)
523
594
  if force_symmetries:
524
- self.matrix = 0.5*(self.matrix + self.matrix.swapaxes(-1,-2))
525
- elif check_symmetries and not np.all(np.isclose(self.matrix, self.matrix.swapaxes(-1, -2))):
595
+ self._matrix = 0.5 * (self._matrix + self._matrix.swapaxes(-1, -2))
596
+ elif check_symmetries and not np.all(np.isclose(self._matrix, self._matrix.swapaxes(-1, -2))):
526
597
  raise ValueError('The input matrix must be symmetric')
527
598
 
528
599
  def invariants(self, order='all'):
@@ -535,7 +606,7 @@ class SymmetricFourthOrderTensor(FourthOrderTensor):
535
606
  ----------
536
607
  order : str, optional
537
608
  If 'linear', only A1 and A2 are returned
538
- If 'quadratic', A1², A2², B1, B2, B3, B4 and B5 are returned
609
+ If 'quadratic', A1², A2², A1*A2, B1, B2, B3, B4 and B5 are returned
539
610
  If 'all' (default), A1, A2, A1², A2², B1, B2, B3, B4 and B5 are returned
540
611
 
541
612
  Returns
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+
3
+ a = np.sqrt(2)
4
+ KELVIN_MAPPING_MATRIX = np.array([[1, 1, 1, a, a, a],
5
+ [1, 1, 1, a, a, a],
6
+ [1, 1, 1, a, a, a],
7
+ [a, a, a, 2, 2, 2],
8
+ [a, a, a, 2, 2, 2],
9
+ [a, a, a, 2, 2, 2], ])
10
+
11
+ VOIGT_MAPPING_MATRIX_COMPLIANCE = [[1, 1, 1, 2, 2, 2],
12
+ [1, 1, 1, 2, 2, 2],
13
+ [1, 1, 1, 2, 2, 2],
14
+ [2, 2, 2, 4, 4, 4],
15
+ [2, 2, 2, 4, 4, 4],
16
+ [2, 2, 2, 4, 4, 4]]
17
+
18
+ class MappingConvention:
19
+ matrix = np.array(KELVIN_MAPPING_MATRIX)
20
+
21
+ @property
22
+ def mapping_inverse(self):
23
+ return self
24
+
25
+ class KelvinMapping(MappingConvention):
26
+ name = 'Kelvin'
27
+
28
+ class VoigtMapping(MappingConvention):
29
+ name = 'Voigt'
30
+
31
+ def __init__(self, tensor='Stiffness'):
32
+ if tensor == 'Stiffness':
33
+ self.matrix = np.ones((6,6))
34
+ self.tensor_type = 'Stiffness'
35
+ else:
36
+ self.matrix = np.array(VOIGT_MAPPING_MATRIX_COMPLIANCE)
37
+ self.tensor_type = 'Compliance'
38
+
39
+ @property
40
+ def mapping_inverse(self):
41
+ if self.tensor_type == 'Stiffness':
42
+ return VoigtMapping(tensor='Compliance')
43
+ else:
44
+ return VoigtMapping(tensor='Stiffness')
@@ -1,5 +1,5 @@
1
1
  import warnings
2
-
2
+ import matplotlib.pyplot as plt
3
3
  import numpy as np
4
4
  import pandas as pd
5
5
  from scipy.spatial.transform import Rotation
@@ -75,6 +75,15 @@ def _map(matrix, mapping_convention):
75
75
  array[...,i] = matrix[...,j,k]
76
76
  return array * mapping_convention
77
77
 
78
+ def filldraw_circle(ax, center, radius, color, fill=False, alpha=1.):
79
+ theta = np.linspace(0, 2 * np.pi, 500)
80
+ x = center[0] + radius * np.cos(theta)
81
+ y = center[1] + radius * np.sin(theta)
82
+ if fill:
83
+ ax.fill(x, y, color=color, alpha=alpha)
84
+ else:
85
+ ax.plot(x, y, color=color)
86
+
78
87
  kelvin_mapping = [1, 1, 1, np.sqrt(2), np.sqrt(2), np.sqrt(2)]
79
88
 
80
89
  class SecondOrderTensor:
@@ -1380,6 +1389,53 @@ class SecondOrderTensor:
1380
1389
  else:
1381
1390
  return Constructor(self.matrix)
1382
1391
 
1392
+ @classmethod
1393
+ def stack(cls, arrays, axis=0):
1394
+ """
1395
+ Stack tensor arrays along the specified axis.
1396
+
1397
+ Parameters
1398
+ ----------
1399
+ arrays : list of SecondOrderTensor or tuple of SecondOrderTensor
1400
+ List of tensor to stack together
1401
+ axis : int, optional
1402
+ Axis along which to stack
1403
+
1404
+ Returns
1405
+ -------
1406
+ SecondOrderTensor
1407
+ Stacked tensor array
1408
+
1409
+ Examples
1410
+ --------
1411
+ >>> from Elasticipy.tensors.second_order import SecondOrderTensor
1412
+ >>> import numpy as np
1413
+ >>> a = SecondOrderTensor.rand(shape=3)
1414
+ >>> b = SecondOrderTensor.rand(shape=3)
1415
+ >>> c = SecondOrderTensor.stack((a, b))
1416
+ >>> c.shape
1417
+ (2, 3)
1418
+ >>> np.all(c[0] == a)
1419
+ True
1420
+ >>> np.all(c[1] == b)
1421
+ True
1422
+
1423
+ >>> a = SecondOrderTensor.rand(shape=(3, 4))
1424
+ >>> b = SecondOrderTensor.rand(shape=(3, 4))
1425
+ >>> c = SecondOrderTensor.stack((a, b), axis=1)
1426
+ >>> c.shape
1427
+ (3, 2, 4)
1428
+ >>> np.all(c[:,0,:] == a)
1429
+ True
1430
+ >>> np.all(c[:,1,:] == b)
1431
+ True
1432
+ """
1433
+ mat_array = [a.matrix for a in arrays]
1434
+ if axis<0:
1435
+ axis = axis - 2
1436
+ mat_stacked = np.stack(mat_array, axis=axis)
1437
+ return cls(mat_stacked)
1438
+
1383
1439
 
1384
1440
  class SymmetricSecondOrderTensor(SecondOrderTensor):
1385
1441
  voigt_map = [1, 1, 1, 1, 1, 1]
@@ -1440,7 +1496,7 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
1440
1496
  'matrices.')
1441
1497
 
1442
1498
  @classmethod
1443
- def from_Voigt(cls, array):
1499
+ def from_Voigt(cls, array, voigt_map=None):
1444
1500
  """
1445
1501
  Construct a SymmetricSecondOrderTensor from a Voigt vector, or slices of Voigt vectors.
1446
1502
 
@@ -1452,6 +1508,10 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
1452
1508
  array : np.ndarray or list
1453
1509
  array to build the SymmetricSecondOrderTensor from. We must have array.ndim>0 and array.shape[-1]==6.
1454
1510
 
1511
+ voigt_map : list or tuple, optional
1512
+ 6-lenght list of factors to use for mapping. If None (default), the default Voigt map of the constructor is
1513
+ used.
1514
+
1455
1515
  Returns
1456
1516
  -------
1457
1517
  SymmetricSecondOrderTensor
@@ -1469,7 +1529,9 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
1469
1529
  [12. 22. 23.]
1470
1530
  [13. 23. 33.]]
1471
1531
  """
1472
- matrix = _unmap(array, cls.voigt_map)
1532
+ if voigt_map is None:
1533
+ voigt_map = cls.voigt_map
1534
+ matrix = _unmap(array, voigt_map)
1473
1535
  return cls(matrix)
1474
1536
 
1475
1537
  def to_Voigt(self):
@@ -1556,6 +1618,48 @@ class SymmetricSecondOrderTensor(SecondOrderTensor):
1556
1618
  eigvals = np.linalg.eigvalsh(self.matrix)
1557
1619
  return np.flip(eigvals,axis=-1)
1558
1620
 
1621
+ def draw_Mohr_circles(self):
1622
+ """
1623
+ Draw the Mohr circles of the symmetric second-order tensor
1624
+
1625
+ Given a tensor, the Mohr circles are meant to visually illustrate the possible shear components one can find
1626
+ when randomly rotating the tensor. See `here <https://en.wikipedia.org/wiki/Mohr%27s_circle>`_ for details.
1627
+
1628
+ Returns
1629
+ -------
1630
+ fig : matplotlib.figure.Figure
1631
+ The Matplotlib figure containing the plot.
1632
+ ax : matplotlib.axes._axes.Axes
1633
+ The Matplotlib axes of the plot.
1634
+ """
1635
+ c,b,a = self.eigvals()
1636
+
1637
+ # Sizes and locations of circles
1638
+ r1 = (c - b) / 2
1639
+ r2 = (b - a) / 2
1640
+ r3 = (c - a) / 2
1641
+ center1 = ((b + c) /2, 0)
1642
+ center2 = ((a + b) /2, 0)
1643
+ center3 = ((a + c) /2, 0)
1644
+
1645
+ fig, ax = plt.subplots()
1646
+ filldraw_circle(ax, center1, r1, 'skyblue')
1647
+ filldraw_circle(ax, center2, r2, 'lightgreen')
1648
+ filldraw_circle(ax, center3, r3, 'red')
1649
+ filldraw_circle(ax, center3, r3, 'red', fill=True, alpha=0.2)
1650
+ filldraw_circle(ax, center1, r1, 'white', fill=True)
1651
+ filldraw_circle(ax, center2, r2, 'white', fill=True)
1652
+ ax.set_aspect('equal')
1653
+ ax.set_xlabel(f"Normal")
1654
+ ax.set_ylabel(f"Shear")
1655
+ ax.grid(True)
1656
+ xticks = (a,b,c, center1[0], center2[0])
1657
+ ax.set_xticks(np.unique(xticks))
1658
+ yticks = (-r3, -r2, -r1, 0, r1, r2, r3)
1659
+ ax.set_yticks(np.unique(yticks))
1660
+
1661
+ return fig, ax
1662
+
1559
1663
 
1560
1664
  class SkewSymmetricSecondOrderTensor(SecondOrderTensor):
1561
1665
  name = 'Skew-symmetric second-order tensor'
@@ -38,7 +38,7 @@ class StrainTensor(SymmetricSecondOrderTensor):
38
38
  """von Mises equivalent strain"""
39
39
  return np.sqrt(2/3 * self.ddot(self))
40
40
 
41
- def elastic_energy(self, stress):
41
+ def elastic_energy(self, stress, mode='pair'):
42
42
  """
43
43
  Compute the elastic energy.
44
44
 
@@ -46,12 +46,22 @@ class StrainTensor(SymmetricSecondOrderTensor):
46
46
  ----------
47
47
  stress : StressTensor
48
48
  Corresponding stress tensor
49
+ mode : str, optional
50
+ If 'pair' (default), the elastic energies are computed element-wise. Broadcasting rule applies.
51
+ If 'cross', each cross-combination of stress and strain are considered.
49
52
 
50
53
  Returns
51
54
  -------
52
- Volumetric elastic energy
55
+ float or numpy.ndarray
56
+ Volumetric elastic energy
53
57
  """
54
- return 0.5 * self.ddot(stress)
58
+ return 0.5 * self.ddot(stress, mode=mode)
59
+
60
+ def draw_Mohr_circles(self):
61
+ fig, ax = super().draw_Mohr_circles()
62
+ ax.set_xlabel(ax.get_xlabel() + ' strain')
63
+ ax.set_ylabel(ax.get_ylabel() + ' strain')
64
+ return fig, ax
55
65
 
56
66
 
57
67
  class StressTensor(SymmetricSecondOrderTensor):
@@ -135,4 +145,10 @@ class StressTensor(SymmetricSecondOrderTensor):
135
145
  numpy.ndarray
136
146
  Volumetric elastic energy
137
147
  """
138
- return 0.5 * self.ddot(strain, mode=mode)
148
+ return 0.5 * self.ddot(strain, mode=mode)
149
+
150
+ def draw_Mohr_circles(self):
151
+ fig, ax = super().draw_Mohr_circles()
152
+ ax.set_xlabel(ax.get_xlabel() + ' stress')
153
+ ax.set_ylabel(ax.get_ylabel() + ' stress')
154
+ return fig, ax
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elasticipy
3
- Version: 4.0.0
3
+ Version: 4.1.0
4
4
  Summary: A Python library for elasticity tensor computations
5
5
  Author-email: Dorian Depriester <dorian.dep@gmail.com>
6
6
  License: MIT
@@ -15,18 +15,20 @@ Classifier: Programming Language :: Python
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
- Requires-Python: >=3.10
18
+ Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: numpy
22
22
  Requires-Dist: scipy
23
23
  Requires-Dist: matplotlib
24
+ Requires-Dist: qtpy
25
+ Requires-Dist: pandas
24
26
  Provides-Extra: dev
25
27
  Requires-Dist: pytest; extra == "dev"
26
28
  Requires-Dist: pytest-cov; extra == "dev"
27
29
  Requires-Dist: pymatgen; extra == "dev"
28
30
  Requires-Dist: orix; extra == "dev"
29
- Requires-Dist: mp_api; extra == "dev"
31
+ Requires-Dist: mp_api==0.45.9; extra == "dev"
30
32
  Dynamic: license-file
31
33
 
32
34
  [![PyPI - Version](https://img.shields.io/pypi/v/Elasticipy?link=https%3A%2F%2Fpypi.org%2Fproject%2FElasticipy%2F)](https://pypi.org/project/elasticipy/)
@@ -48,7 +50,7 @@ Among other features, this package implements:
48
50
 
49
51
  - Computation of elasticity tensors,
50
52
  - Analysis of elastic anisotropy and wave propagation,
51
- - Working with multidimensional arrays of strain and stress tensors,
53
+ - Working with multidimensional arrays of tensors,
52
54
  - Thermal expansion tensors,
53
55
  - Rotation of tensors,
54
56
  - Integration with crystal symmetry groups,
@@ -79,3 +81,19 @@ Certain parts of the code, particularly those related to graphical user interfac
79
81
  **excluded from code coverage analysis**. This includes the following file:
80
82
 
81
83
  - **`src/Elasticipy/gui.py`**
84
+
85
+ ## Cite this package
86
+ If you use Elasticipy, please cite [![DOI](https://zenodo.org/badge/876162900.svg)](https://doi.org/10.5281/zenodo.14501849)
87
+
88
+ You can use the following BibTeX entry:
89
+ ````bibtex
90
+ @software{Elasticipy,
91
+ author = {Depriester, Dorian},
92
+ doi = {10.5281/zenodo.15188346},
93
+ month = apr,
94
+ title = {{Elasticipy}},
95
+ url = {https://github.com/DorianDepriester/Elasticipy},
96
+ version = {4.0.0},
97
+ year = {2025}
98
+ }
99
+ ````