elasticipy 4.2.0__py3-none-any.whl → 5.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.
@@ -5,6 +5,7 @@ from scipy.spatial.transform import Rotation
5
5
  from copy import deepcopy
6
6
  from Elasticipy.tensors.mapping import KelvinMapping, VoigtMapping
7
7
 
8
+ kelvin_mapping = KelvinMapping()
8
9
 
9
10
  def voigt_indices(i, j):
10
11
  """
@@ -44,48 +45,48 @@ def unvoigt_index(i):
44
45
  [0, 1]])
45
46
  return inverse_voigt_mat[i]
46
47
 
47
- def rotate_tensor(full_tensor, r):
48
- """
49
- Rotate a (full) fourth-order tensor.
50
-
51
- Parameters
52
- ----------
53
- full_tensor : numpy.ndarray
54
- array of shape (3,3,3,3) or (...,3,3,3,3) containing all the components
55
- r : scipy.spatial.Rotation or orix.quaternion.Rotation
56
- Rotation, or set of rotations, to apply
57
-
58
- Returns
59
- -------
60
- numpy.ndarray
61
- Rotated tensor. If r is an array, the corresponding axes will be added as first axes in the result array.
62
- """
48
+ def _rotate_tensor(full_tensor, r):
63
49
  rot_mat = rotation_to_matrix(r)
64
50
  str_ein = '...im,...jn,...ko,...lp,...mnop->...ijkl'
65
51
  return np.einsum(str_ein, rot_mat, rot_mat, rot_mat, rot_mat, full_tensor)
66
52
 
53
+ def _isotropic_matrix(C11, C12, C44):
54
+ C11 = np.asarray(C11)
55
+ C12 = np.asarray(C12)
56
+ C44 = np.asarray(C44)
57
+ shape = np.broadcast_shapes(C11.shape, C12.shape, C44.shape)
58
+ matrix = np.zeros(shape=shape + (6, 6))
59
+ matrix[..., 0, 0] = C11
60
+ matrix[..., 1, 1] = C11
61
+ matrix[..., 2, 2] = C11
62
+ matrix[..., 0, 1] = matrix[..., 1, 0] = C12
63
+ matrix[..., 0, 2] = matrix[..., 2, 0] = C12
64
+ matrix[..., 1, 2] = matrix[..., 2, 1] = C12
65
+ matrix[..., 3, 3] = C44
66
+ matrix[..., 4, 4] = C44
67
+ matrix[..., 5, 5] = C44
68
+ return matrix
69
+
67
70
  class FourthOrderTensor:
68
71
  """
69
72
  Template class for manipulating symmetric fourth-order tensors.
70
-
71
- Attributes
72
- ----------
73
- _matrix : np.ndarray
74
- (6,6) matrix gathering all the components of the tensor, using the Voigt notation.
75
73
  """
76
- tensor_name = '4th-order'
74
+ _tensor_name = '4th-order'
77
75
 
78
- def __init__(self, M, mapping=KelvinMapping(), check_minor_symmetry=True, force_minor_symmetry=False):
76
+ def _array_to_Kelvin(self, matrix):
77
+ return matrix / self.mapping.matrix * kelvin_mapping.matrix
78
+
79
+ def __init__(self, M, mapping=kelvin_mapping, check_minor_symmetry=True, force_minor_symmetry=False):
79
80
  """
80
81
  Construct of Fourth-order tensor with minor symmetry.
81
82
 
82
83
  Parameters
83
84
  ----------
84
- M : np.ndarray
85
- (6,6) matrix corresponding to the stiffness tensor, written using the Voigt notation, or array of shape
86
- (3,3,3,3).
85
+ M : np.ndarray or FourthOrderTensor
86
+ (...,6,6) matrix corresponding to the stiffness tensor, written using the Voigt notation, or array of shape
87
+ (...,3,3,3,3).
87
88
  mapping : MappingConvention, optional
88
- Mapping convention to translate the (3,3,3,3) array to (6,6) matrix
89
+ Mapping convention to translate the (3,3,3,3) array to (6,6) matrix.
89
90
  check_minor_symmetry : bool, optional
90
91
  If true (default), check that the input array have minor symmetries (see Notes). Only used if an array of
91
92
  shape (...,3,3,3,3) is passed.
@@ -100,46 +101,168 @@ class FourthOrderTensor:
100
101
 
101
102
  M_{ijkl}=M_{jikl}=M_{jilk}=M_{ijlk}
102
103
 
104
+ Given a generic 4th-order tensor T, the corresponding matrix with respect to Kelvin convention is:
105
+
106
+ .. math::
107
+
108
+ T =
109
+ \\begin{bmatrix}
110
+ T_{1111} & T_{1122} & T_{1133} & \\sqrt{2}T_{1123} & \\sqrt{2}T_{1113} & \\sqrt{2}T_{1112}\\\\
111
+ T_{2211} & T_{2222} & T_{2233} & \\sqrt{2}T_{2223} & \\sqrt{2}T_{2213} & \\sqrt{2}T_{2212}\\\\
112
+ T_{3311} & T_{3322} & T_{3333} & \\sqrt{2}T_{3323} & \\sqrt{2}T_{3313} & \\sqrt{2}T_{3312}\\\\
113
+ \\sqrt{2}T_{2311} & \\sqrt{2}T_{2322} & \\sqrt{2}T_{2333} & 2T_{2323} & 2T_{2313} & 2T_{2312}\\\\
114
+ \\sqrt{2}T_{1311} & \\sqrt{2}T_{1322} & \\sqrt{2}T_{1333} & 2T_{423} & 2T_{1313} & 2T_{1312}\\\\
115
+ \\sqrt{2}T_{1211} & \\sqrt{2}T_{1222} & \\sqrt{2}T_{1233} & 2T_{1223} & 2T_{1223} & 2T_{1212}\\\\
116
+ \\end{bmatrix}
117
+
118
+ Examples
119
+ --------
120
+ Consider a Fourth-order tensor, whose Kelvin matrix is:
121
+
122
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
123
+ >>> import numpy as np
124
+ >>> mat = np.array([[100, 200, 300, 0, 0, 0],
125
+ ... [-200, 100, 50, 0, 0, 0],
126
+ ... [-300, -50, 100, 0, 0, 0],
127
+ ... [0, 0, 0, 150, 0, 0],
128
+ ... [0, 0, 0, 0, 150, 0],
129
+ ... [0, 0, 0, 0, 0, 150]])
130
+ >>> T = FourthOrderTensor(mat)
131
+ >>> print(T)
132
+ 4th-order tensor (in Kelvin mapping):
133
+ [[ 100. 200. 300. 0. 0. 0.]
134
+ [-200. 100. 50. 0. 0. 0.]
135
+ [-300. -50. 100. 0. 0. 0.]
136
+ [ 0. 0. 0. 150. 0. 0.]
137
+ [ 0. 0. 0. 0. 150. 0.]
138
+ [ 0. 0. 0. 0. 0. 150.]]
139
+
140
+ If one wants to evaluate the tensor as a (full) (3,3,3,3) array:
141
+
142
+ >>> T_array = T.full_tensor
143
+
144
+ For instance:
145
+
146
+ >>> T_array[0,0,0,0]
147
+ 100.0
148
+
149
+ whereas
150
+
151
+ >>> T_array[0,1,0,1] # Corresponds to T_{66}/2
152
+ 75.0
153
+
154
+ The half factor comes from the Kelvin mapping convention (see Notes). One can also use the Voigt mapping to
155
+ avoid this:
156
+
157
+ >>> from Elasticipy.tensors.mapping import VoigtMapping
158
+ >>> T_voigt = FourthOrderTensor(mat, mapping=VoigtMapping())
159
+ >>> print(T_voigt)
160
+ 4th-order tensor (in Voigt mapping):
161
+ [[ 100. 200. 300. 0. 0. 0.]
162
+ [-200. 100. 50. 0. 0. 0.]
163
+ [-300. -50. 100. 0. 0. 0.]
164
+ [ 0. 0. 0. 150. 0. 0.]
165
+ [ 0. 0. 0. 0. 150. 0.]
166
+ [ 0. 0. 0. 0. 0. 150.]]
167
+
168
+ Although T and T_voigt appear to be the same, note that they are not expressed using the same mapping
169
+ convention. Indeed:
170
+
171
+ >>> T_voigt.full_tensor[0,0,0,0]
172
+ 100.0
173
+
174
+ whereas
175
+
176
+ >>> T_voigt.full_tensor[0,1,0,1]
177
+ 150.0
178
+
179
+ Alternatively, the differences can be checked with:
180
+
181
+ >>> T == T_voigt
182
+ False
183
+
184
+ Conversely, let consider the following Voigt matrix:
185
+
186
+ >>> mat = np.array([[100, 200, 300, 0, 0, 0],
187
+ ... [-200, 100, 50, 0, 0, 0],
188
+ ... [-300, -50, 100, 0, 0, 0],
189
+ ... [0, 0, 0, 75, 0, 0],
190
+ ... [0, 0, 0, 0, 75, 0],
191
+ ... [0, 0, 0, 0, 0, 75]])
192
+ >>> T_voigt2 = FourthOrderTensor(mat, mapping=VoigtMapping())
193
+ >>> print(T_voigt2)
194
+ 4th-order tensor (in Voigt mapping):
195
+ [[ 100. 200. 300. 0. 0. 0.]
196
+ [-200. 100. 50. 0. 0. 0.]
197
+ [-300. -50. 100. 0. 0. 0.]
198
+ [ 0. 0. 0. 75. 0. 0.]
199
+ [ 0. 0. 0. 0. 75. 0.]
200
+ [ 0. 0. 0. 0. 0. 75.]]
201
+
202
+ Although T and T_voigt2 are not written using the same mapping, we can compare them:
203
+
204
+ >>> T == T_voigt2 # Same tensors, but different mapping
205
+ True
206
+
207
+ whereas
208
+
209
+ >>> T == T_voigt # Different tensors, but same mapping
210
+ False
211
+
212
+ This property comes from the fact that the comparison is made independently of the underlying mapping convention.
103
213
  """
214
+ if isinstance(mapping, str):
215
+ if mapping.lower() == 'voigt':
216
+ mapping = VoigtMapping()
217
+ elif mapping.lower() == 'kelvin':
218
+ mapping = kelvin_mapping
219
+ else:
220
+ raise ValueError('Mapping must be either "voigt" or "kelvin"')
104
221
  self.mapping=mapping
105
- M = np.asarray(M)
106
- if M.shape[-2:] == (6, 6):
107
- matrix = M
108
- elif M.shape[-4:] == (3, 3, 3, 3):
109
- Mijlk = np.swapaxes(M, -1, -2)
110
- Mjikl = np.swapaxes(M, -3, -4)
111
- Mjilk = np.swapaxes(Mjikl, -1, -2)
112
- if force_minor_symmetry:
113
- M = 0.25 * (M + Mijlk + Mjikl + Mjilk)
114
- elif check_minor_symmetry:
115
- symmetry = np.all(M == Mijlk) and np.all(M == Mjikl) and np.all(M == Mjilk)
116
- if not symmetry:
117
- raise ValueError('The input array does not have minor symmetry')
118
- matrix = self._full_to_matrix(M)
222
+ if isinstance(M, FourthOrderTensor):
223
+ self._matrix = M._matrix
119
224
  else:
120
- raise ValueError('The input matrix must of shape (...,6,6) or (...,3,3,3,3)')
121
- self._matrix = matrix
225
+ M = np.asarray(M)
226
+ if M.shape[-2:] == (6, 6):
227
+ matrix = self._array_to_Kelvin(M)
228
+ elif M.shape[-4:] == (3, 3, 3, 3):
229
+ Mijlk = np.swapaxes(M, -1, -2)
230
+ Mjikl = np.swapaxes(M, -3, -4)
231
+ Mjilk = np.swapaxes(Mjikl, -1, -2)
232
+ if force_minor_symmetry:
233
+ M = 0.25 * (M + Mijlk + Mjikl + Mjilk)
234
+ elif check_minor_symmetry:
235
+ symmetry = np.all(M == Mijlk) and np.all(M == Mjikl) and np.all(M == Mjilk)
236
+ if not symmetry:
237
+ raise ValueError('The input array does not have minor symmetry')
238
+ matrix = self._full_to_matrix(M)
239
+ else:
240
+ raise ValueError('The input matrix must of shape (...,6,6) or (...,3,3,3,3)')
241
+ self._matrix = matrix
122
242
  for i in range(0, 6):
123
243
  for j in range(0, 6):
124
244
  def getter(obj, I=i, J=j):
125
- return obj._matrix[...,I, J]
245
+ new_matrix = obj._matrix / kelvin_mapping.matrix * self.mapping.matrix
246
+ return new_matrix[...,I, J]
126
247
 
127
- getter.__doc__ = f"Returns the ({i + 1},{j + 1}) component of the {self.tensor_name} matrix."
248
+ getter.__doc__ = f"Returns the ({i + 1},{j + 1}) component of the {self._tensor_name} matrix."
128
249
  component_name = 'C{}{}'.format(i + 1, j + 1)
129
250
  setattr(self.__class__, component_name, property(getter)) # Dynamically create the property
130
251
 
131
252
  def __repr__(self):
132
253
  if (self.ndim == 0) or ((self.ndim==1) and self.shape[0]<5):
133
- msg = '{} tensor (in {} mapping):\n'.format(self.tensor_name, self.mapping.name)
134
- msg += self._matrix.__str__()
254
+ msg = '{} tensor (in {} mapping):\n'.format(self._tensor_name, self.mapping.name)
255
+ matrix = self.matrix(self.mapping)
256
+ msg += matrix.__str__()
135
257
  else:
136
- msg = '{} tensor array of shape {}'.format(self.tensor_name, self.shape)
258
+ msg = '{} tensor array of shape {}'.format(self._tensor_name, self.shape)
137
259
  return msg
138
260
 
139
261
  @property
140
262
  def shape(self):
141
263
  """
142
264
  Return the shape of the tensor array
265
+
143
266
  Returns
144
267
  -------
145
268
  tuple
@@ -148,19 +271,46 @@ class FourthOrderTensor:
148
271
  *shape, _, _ = self._matrix.shape
149
272
  return tuple(shape)
150
273
 
274
+ @property
151
275
  def full_tensor(self):
152
276
  """
153
- Returns the full (unvoigted) tensor, as a [3, 3, 3, 3] array
277
+ Returns the full (unvoigted) tensor as a (3, 3, 3, 3) or (..., 3, 3, 3, 3) array
154
278
 
155
279
  Returns
156
280
  -------
157
281
  np.ndarray
158
282
  Full tensor (4-index notation)
283
+
284
+ Examples
285
+ --------
286
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
287
+ >>> I = FourthOrderTensor.eye() # 4th order identity tensor
288
+ >>> print(I)
289
+ 4th-order tensor (in Kelvin mapping):
290
+ [[1. 0. 0. 0. 0. 0.]
291
+ [0. 1. 0. 0. 0. 0.]
292
+ [0. 0. 1. 0. 0. 0.]
293
+ [0. 0. 0. 1. 0. 0.]
294
+ [0. 0. 0. 0. 1. 0.]
295
+ [0. 0. 0. 0. 0. 1.]]
296
+
297
+ >>> I_full = I.full_tensor
298
+ >>> type(I_full)
299
+ <class 'numpy.ndarray'>
300
+ >>> I_full.shape
301
+ (3, 3, 3, 3)
302
+
303
+ When working on tensor arrays, the shape of the resulting numpy array will change accordlingly. E.g.:
304
+
305
+ >>> I_array = FourthOrderTensor.eye(shape=(5,6)) # Array of 4th order identity tensor
306
+ >>> I_array.full_tensor.shape
307
+ (5, 6, 3, 3, 3, 3)
159
308
  """
160
309
  i, j, k, ell = np.indices((3, 3, 3, 3))
161
310
  ij = voigt_indices(i, j)
162
311
  kl = voigt_indices(k, ell)
163
- m = self._matrix[..., ij, kl] / self.mapping.matrix[ij, kl]
312
+ matrix = self._matrix / kelvin_mapping.matrix
313
+ m = matrix[..., ij, kl]
164
314
  return m
165
315
 
166
316
  def flatten(self):
@@ -173,6 +323,15 @@ class FourthOrderTensor:
173
323
  -------
174
324
  SymmetricFourthOrderTensor
175
325
  Flattened tensor
326
+
327
+ Examples
328
+ --------
329
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
330
+ >>> T = FourthOrderTensor.rand(shape=(5,6))
331
+ >>> T
332
+ 4th-order tensor array of shape (5, 6)
333
+ >>> T.flatten()
334
+ 4th-order tensor array of shape (30,)
176
335
  """
177
336
  shape = self.shape
178
337
  if shape:
@@ -187,7 +346,7 @@ class FourthOrderTensor:
187
346
  kl, ij = np.indices((6, 6))
188
347
  i, j = unvoigt_index(ij).T
189
348
  k, ell = unvoigt_index(kl).T
190
- return full_tensor[..., i, j, k, ell] * self.mapping.matrix[ij, kl]
349
+ return full_tensor[..., i, j, k, ell] * kelvin_mapping.matrix[ij, kl]
191
350
 
192
351
  def rotate(self, rotation):
193
352
  """
@@ -202,9 +361,72 @@ class FourthOrderTensor:
202
361
  -------
203
362
  SymmetricFourthOrderTensor
204
363
  Rotated tensor
364
+
365
+ Examples
366
+ --------
367
+ Let start from a given tensor, (say ones):
368
+
369
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
370
+ >>> T = FourthOrderTensor.ones()
371
+ >>> T
372
+ 4th-order tensor (in Kelvin mapping):
373
+ [[1. 1. 1. 1.41421356 1.41421356 1.41421356]
374
+ [1. 1. 1. 1.41421356 1.41421356 1.41421356]
375
+ [1. 1. 1. 1.41421356 1.41421356 1.41421356]
376
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]
377
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]
378
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]]
379
+
380
+ Define a rotation. E.g.:
381
+
382
+ >>> from scipy.spatial.transform import Rotation
383
+ >>> g = Rotation.from_euler('X', 90, degrees=True)
384
+
385
+ Then , apply rotation:
386
+
387
+ >>> Trotated = T.rotate(g)
388
+ >>> Trotated
389
+ 4th-order tensor (in Kelvin mapping):
390
+ [[ 1. 1. 1. -1.41421356 1.41421356 -1.41421356]
391
+ [ 1. 1. 1. -1.41421356 1.41421356 -1.41421356]
392
+ [ 1. 1. 1. -1.41421356 1.41421356 -1.41421356]
393
+ [-1.41421356 -1.41421356 -1.41421356 2. -2. 2. ]
394
+ [ 1.41421356 1.41421356 1.41421356 -2. 2. -2. ]
395
+ [-1.41421356 -1.41421356 -1.41421356 2. -2. 2. ]]
396
+
397
+ Actually, a more simple syntax is:
398
+
399
+ >>> T * g
400
+ 4th-order tensor (in Kelvin mapping):
401
+ [[ 1. 1. 1. -1.41421356 1.41421356 -1.41421356]
402
+ [ 1. 1. 1. -1.41421356 1.41421356 -1.41421356]
403
+ [ 1. 1. 1. -1.41421356 1.41421356 -1.41421356]
404
+ [-1.41421356 -1.41421356 -1.41421356 2. -2. 2. ]
405
+ [ 1.41421356 1.41421356 1.41421356 -2. 2. -2. ]
406
+ [-1.41421356 -1.41421356 -1.41421356 2. -2. 2. ]]
407
+
408
+ Obviously, the original tensor can be retrieved by applying the reverse rotation:
409
+
410
+ >>> Trotated * g.inv()
411
+ 4th-order tensor (in Kelvin mapping):
412
+ [[1. 1. 1. 1.41421356 1.41421356 1.41421356]
413
+ [1. 1. 1. 1.41421356 1.41421356 1.41421356]
414
+ [1. 1. 1. 1.41421356 1.41421356 1.41421356]
415
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]
416
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]
417
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]]
418
+
419
+ If ``g`` is composed of multiple rotations, this will result in a tensor array, corresponding to each rotation:
420
+
421
+ >>> import numpy as np
422
+ >>> theta = np.linspace(0, 90, 100)
423
+ >>> g = Rotation.from_euler('X', theta, degrees=True)
424
+ >>> Trotated = T * g
425
+ >>> Trotated
426
+ 4th-order tensor array of shape (100,)
205
427
  """
206
428
  t2 = deepcopy(self)
207
- rotated_tensor = rotate_tensor(self.full_tensor(), rotation)
429
+ rotated_tensor = _rotate_tensor(self.full_tensor, rotation)
208
430
  t2._matrix = self._full_to_matrix(rotated_tensor)
209
431
  return t2
210
432
 
@@ -237,7 +459,31 @@ class FourthOrderTensor:
237
459
  -------
238
460
  numpy.ndarray
239
461
  If no axis is given, the result will be of shape (3,3,3,3).
240
- Otherwise, if T.ndim=m, and len(axis)=n, the returned value will be of shape (...,3,3,3,3), with ndim=m-n+4
462
+
463
+ Examples
464
+ --------
465
+ Create a random tensor array of shape (5,6):
466
+
467
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
468
+ >>> T = FourthOrderTensor.rand(shape=(5,6))
469
+ >>> Overall_mean = T.mean()
470
+ >>> Overall_mean.shape
471
+ ()
472
+ >>> Overall_mean # doctest: +SKIP
473
+ 4th-order tensor (in Kelvin mapping):
474
+ [[0.514295 0.52259217 0.42899181 0.77148692 0.64073221 0.73211491]
475
+ [0.49422678 0.43718365 0.40786118 0.8170971 0.68435571 0.67262655]
476
+ [0.48753674 0.51142541 0.44650454 0.76310921 0.67724973 0.69430165]
477
+ [0.53946846 0.75101474 0.73578098 1.04338905 1.21598419 0.99489014]
478
+ [0.75354555 0.61193555 0.82341479 1.11197826 0.89183143 1.20986243]
479
+ [0.66078807 0.70126535 0.63719147 0.87567139 1.05671229 1.03004098]]
480
+
481
+ >>> axis_0_mean = T.mean(axis=0)
482
+ >>> axis_0_mean.shape
483
+ (6,)
484
+ >>> axis_1_mean = T.mean(axis=1)
485
+ >>> axis_1_mean.shape
486
+ (5,)
241
487
  """
242
488
  t2 = deepcopy(self)
243
489
  if axis is None:
@@ -246,27 +492,26 @@ class FourthOrderTensor:
246
492
  return t2
247
493
 
248
494
  def __add__(self, other):
495
+ new_tensor = deepcopy(self)
249
496
  if isinstance(other, np.ndarray):
250
- if other.shape == (6, 6):
251
- mat = self._matrix + other
497
+ if other.shape[-2:] == (6, 6):
498
+ mat = self._matrix + self._array_to_Kelvin(other)
252
499
  elif other.shape == (3, 3, 3, 3):
253
- mat = self._full_to_matrix(self.full_tensor() + other)
500
+ mat = self._full_to_matrix(self.full_tensor + other)
254
501
  else:
255
502
  raise ValueError('The input argument must be either a 6x6 matrix or a (3,3,3,3) array.')
256
503
  elif isinstance(other, FourthOrderTensor):
257
504
  if type(other) == type(self):
258
- mat = self.full_tensor() + other.full_tensor()
505
+ mat = self._matrix + other._matrix
259
506
  else:
260
507
  raise ValueError('The two tensors to add must be of the same class.')
261
508
  else:
262
509
  raise ValueError('I don''t know how to add {} with {}.'.format(type(self), type(other)))
263
- return self.__class__(mat, mapping=self.mapping)
510
+ new_tensor._matrix = mat
511
+ return new_tensor
264
512
 
265
513
  def __sub__(self, other):
266
- if isinstance(other, FourthOrderTensor):
267
- return self.__add__(-other._matrix)
268
- else:
269
- return self.__add__(-other)
514
+ return self.__add__(-other)
270
515
 
271
516
  def __neg__(self):
272
517
  t = deepcopy(self)
@@ -287,13 +532,49 @@ class FourthOrderTensor:
287
532
 
288
533
  Returns
289
534
  -------
290
- FourthOrderTensor or numpy.ndarray
291
- If both the tensors are 0D (no orientation), the return value will be of type SymmetricTensor
292
- Otherwise, the return value will be the full tensor, of shape (...,3,3,3,3).
535
+ FourthOrderTensor
536
+ Result from double-contraction
537
+
538
+ Examples
539
+ --------
540
+ First, let consider two random arrays of Fourth-order tensors:
541
+
542
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
543
+ >>> T1 = FourthOrderTensor.rand(shape=(2,3))
544
+ >>> T2 = FourthOrderTensor.rand(shape=3)
545
+ >>> T1T2_pair = T1.ddot(T2)
546
+ >>> T1T2_pair
547
+ 4th-order tensor array of shape (2, 3)
548
+
549
+ whereas:
550
+
551
+ >>> T1T2_cross = T1.ddot(T2, mode='cross')
552
+ >>> T1T2_cross
553
+ 4th-order tensor array of shape (2, 3, 3)
554
+
555
+ The command above is equivalent (but way faster) to:
556
+
557
+ >>> T1T2_cross_loop = FourthOrderTensor.zeros(shape=(2,3,3))
558
+ >>> for i in range(2):
559
+ ... for j in range(3):
560
+ ... for k in range(3):
561
+ ... T1T2_cross_loop[i,j,k] = T1[i,j].ddot(T2[k])
562
+
563
+ One can check that the results are consistent with:
564
+
565
+ >>> T1T2_cross_loop == T1T2_cross
566
+ array([[[ True, True, True],
567
+ [ True, True, True],
568
+ [ True, True, True]],
569
+ <BLANKLINE>
570
+ [[ True, True, True],
571
+ [ True, True, True],
572
+ [ True, True, True]]])
573
+
293
574
  """
294
575
  if isinstance(other, FourthOrderTensor):
295
576
  if self.ndim == 0 and other.ndim == 0:
296
- return FourthOrderTensor(np.einsum('ijmn,nmkl->ijkl', self.full_tensor(), other.full_tensor()))
577
+ return FourthOrderTensor(np.einsum('ijmn,nmkl->ijkl', self.full_tensor, other.full_tensor))
297
578
  else:
298
579
  if mode == 'pair':
299
580
  ein_str = '...ijmn,...nmkl->...ijkl'
@@ -304,11 +585,11 @@ class FourthOrderTensor:
304
585
  indices_1 = ALPHABET[:ndim_1].upper()
305
586
  indices_2 = indices_0 + indices_1
306
587
  ein_str = indices_0 + 'wxXY,' + indices_1 + 'YXyz->' + indices_2 + 'wxyz'
307
- matrix = np.einsum(ein_str, self.full_tensor(), other.full_tensor())
588
+ matrix = np.einsum(ein_str, self.full_tensor, other.full_tensor)
308
589
  return FourthOrderTensor(matrix)
309
590
  elif isinstance(other, SecondOrderTensor):
310
591
  if self.ndim == 0 and other.ndim == 0:
311
- return SymmetricSecondOrderTensor(np.einsum('ijkl,kl->ij', self.full_tensor(), other.matrix))
592
+ return SymmetricSecondOrderTensor(np.einsum('ijkl,kl->ij', self.full_tensor, other.matrix))
312
593
  else:
313
594
  if mode == 'pair':
314
595
  ein_str = '...ijkl,...kl->...ij'
@@ -319,9 +600,36 @@ class FourthOrderTensor:
319
600
  indices_1 = ALPHABET[:ndim_1].upper()
320
601
  indices_2 = indices_0 + indices_1
321
602
  ein_str = indices_0 + 'wxXY,' + indices_1 + 'XY->' + indices_2 + 'wx'
322
- matrix = np.einsum(ein_str, self.full_tensor(), other.matrix)
603
+ matrix = np.einsum(ein_str, self.full_tensor, other.matrix)
323
604
  return SecondOrderTensor(matrix)
324
605
 
606
+ @classmethod
607
+ def rand(cls, shape=None, **kwargs):
608
+ """
609
+ Populate a Fourth-order tensor with random values in half-open interval [0.0, 1.0).
610
+
611
+ Parameters
612
+ ----------
613
+ shape : tuple or int, optional
614
+ Set the shape of the tensor array. If None, the returned tensor will be single.
615
+ kwargs
616
+ Keyword arguments passed to the Fourth-order tensor constructor.
617
+
618
+ Returns
619
+ -------
620
+ FourthOrderTensor
621
+ Fourth-order tensor
622
+ """
623
+ if shape is None:
624
+ shape = (6,6)
625
+ elif isinstance(shape, int):
626
+ shape = (shape, 6, 6)
627
+ else:
628
+ shape = tuple(shape) + (6,6)
629
+ mat = np.random.random_sample(shape)
630
+ t = FourthOrderTensor(mat, **kwargs)
631
+ t._matrix = t._matrix * t.mapping.matrix
632
+ return t
325
633
 
326
634
  def __mul__(self, other):
327
635
  if isinstance(other, (FourthOrderTensor, SecondOrderTensor)):
@@ -336,7 +644,9 @@ class FourthOrderTensor:
336
644
  elif isinstance(other, Rotation) or is_orix_rotation(other):
337
645
  return self.rotate(other)
338
646
  else:
339
- return self.__class__(self._matrix * other)
647
+ new_tensor = deepcopy(self)
648
+ new_tensor._matrix = self._matrix * other
649
+ return new_tensor
340
650
 
341
651
  def __truediv__(self, other):
342
652
  if isinstance(other, (SecondOrderTensor, FourthOrderTensor)):
@@ -353,14 +663,22 @@ class FourthOrderTensor:
353
663
  -------
354
664
  FourthOrderTensor
355
665
  The same tensor, but with transposed axes
666
+
667
+ Examples
668
+ --------
669
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
670
+ >>> A = FourthOrderTensor.rand(shape=(3,4))
671
+ >>> A.transpose_array()
672
+ 4th-order tensor array of shape (4, 3)
356
673
  """
357
674
  ndim = self.ndim
358
675
  if ndim==0 or ndim==1:
359
676
  return self
360
677
  else:
678
+ new_array = deepcopy(self)
361
679
  new_axes = tuple(range(ndim))[::-1] + (ndim, ndim + 1)
362
- transposed_matrix = self._matrix.transpose(new_axes)
363
- return self.__class__(transposed_matrix)
680
+ new_array._matrix = self._matrix.transpose(new_axes)
681
+ return new_array
364
682
 
365
683
  def __rmul__(self, other):
366
684
  if isinstance(other, (Rotation, float, int, np.number)) or is_orix_rotation(other):
@@ -379,18 +697,16 @@ class FourthOrderTensor:
379
697
 
380
698
  def __getitem__(self, item):
381
699
  if self.ndim:
382
- sub_mat= self._matrix[item]
383
- if sub_mat.shape[-2:] != (6,6):
384
- raise IndexError('Too many indices for tensor array: array is {}-dimensional, but {} were provided'.format(self.ndim, len(item)))
385
- else:
386
- return self.__class__(sub_mat)
700
+ sub_tensor = deepcopy(self)
701
+ sub_tensor._matrix = self._matrix[item]
702
+ return sub_tensor
387
703
  else:
388
704
  raise IndexError('A single tensor cannot be subindexed')
389
705
 
390
706
  def __setitem__(self, index, value):
391
707
  if isinstance(value, np.ndarray):
392
708
  if value.shape[-2:] == (6,6):
393
- self._matrix[index] = value
709
+ self._matrix[index] = value / self.mapping.matrix * kelvin_mapping.matrix
394
710
  elif value.shape[-4:] == (3,3,3,3):
395
711
  submatrix = self._full_to_matrix(value)
396
712
  self._matrix[index] = submatrix
@@ -401,90 +717,294 @@ class FourthOrderTensor:
401
717
  else:
402
718
  raise NotImplementedError('The r.h.s must be either an ndarray or an object of class {}'.format(self.__class__))
403
719
 
720
+
721
+ @classmethod
722
+ def identity(cls, **kwargs):
723
+ """
724
+ Construct the Fourth-order identity tensor.
725
+
726
+ This is actually an alias for eye().
727
+
728
+ Parameters
729
+ ----------
730
+ kwargs
731
+ Keyword arguments passed to the Fourth-order tensor constructor.
732
+
733
+ Returns
734
+ -------
735
+ FourthOrderTensor
736
+
737
+ See Also
738
+ --------
739
+ eye : Fourth-order identity tensor
740
+ """
741
+ return cls.eye(**kwargs)
742
+
404
743
  @classmethod
405
- def identity(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
744
+ def _broadcast_matrix(cls, M, shape=None, **kwargs):
745
+ if shape is None:
746
+ new_shape = M.shape
747
+ elif isinstance(shape, int):
748
+ new_shape = (shape,) + M.shape
749
+ else:
750
+ new_shape = shape + M.shape
751
+ M_repeat = np.broadcast_to(M, new_shape)
752
+ t = cls(M_repeat, **kwargs)
753
+ t._matrix = t._matrix * t.mapping.matrix / kelvin_mapping.matrix
754
+ return t
755
+
756
+ @classmethod
757
+ def eye(cls, shape=(), **kwargs):
406
758
  """
407
- Create a 4th-order identity tensor
759
+ Create a 4th-order identity tensor.
760
+
761
+ See notes for definition.
408
762
 
409
763
  Parameters
410
764
  ----------
411
765
  shape : int or tuple, optional
412
766
  Shape of the tensor to create
413
- return_full_tensor : bool, optional
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
415
- as a SymmetricTensor object.
416
- mapping : str, optional
767
+ mapping : Kelvin mapping, optional
417
768
  Mapping convention to use. Must be either Kelvin or Voigt.
418
769
 
419
770
  Returns
420
771
  -------
421
- numpy.ndarray or SymmetricTensor
772
+ FourthOrderTensor or SymmetricFourthOrderTensor
422
773
  Identity tensor
774
+
775
+ Notes
776
+ -----
777
+
778
+ The Fourth-order identity tensor is defined as:
779
+
780
+ .. math::
781
+
782
+ I_{ijkl} = \\frac12\\left( \\delta_{ik}\\delta_{jl} + \\delta_{il}\\delta_{jk}\\right)
783
+
784
+ Examples
785
+ --------
786
+ Create a (single) identity tensor:
787
+
788
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
789
+ >>> I = FourthOrderTensor.eye()
790
+ >>> print(I)
791
+ 4th-order tensor (in Kelvin mapping):
792
+ [[1. 0. 0. 0. 0. 0.]
793
+ [0. 1. 0. 0. 0. 0.]
794
+ [0. 0. 1. 0. 0. 0.]
795
+ [0. 0. 0. 1. 0. 0.]
796
+ [0. 0. 0. 0. 1. 0.]
797
+ [0. 0. 0. 0. 0. 1.]]
798
+
799
+ Alternatively, one can use another mapping convention, e.g. Voigt:
800
+
801
+ >>> from Elasticipy.tensors.mapping import VoigtMapping
802
+ >>> Iv = FourthOrderTensor.eye(mapping=VoigtMapping())
803
+ >>> print(Iv)
804
+ 4th-order tensor (in Voigt mapping):
805
+ [[1. 0. 0. 0. 0. 0. ]
806
+ [0. 1. 0. 0. 0. 0. ]
807
+ [0. 0. 1. 0. 0. 0. ]
808
+ [0. 0. 0. 0.5 0. 0. ]
809
+ [0. 0. 0. 0. 0.5 0. ]
810
+ [0. 0. 0. 0. 0. 0.5]]
811
+
812
+ Still, we have:
813
+
814
+ >>> I == Iv
815
+ True
816
+
817
+ as they correspond to the same tensor, but expressed as a matrix with different mapping conventions. Indeed,
818
+ one can check that:
819
+
820
+ >>> import numpy as np
821
+ >>> np.array_equal(I.full_tensor, Iv.full_tensor)
822
+ True
423
823
  """
424
- eye = np.eye(3)
425
- if isinstance(shape, int):
426
- shape = (shape,)
427
- if len(shape):
428
- for n in np.flip(shape):
429
- eye = np.repeat(eye[np.newaxis,...], n, axis=0)
430
- a = np.einsum('...ik,...jl->...ijkl', eye, eye)
431
- b = np.einsum('...il,...jk->...ijkl', eye, eye)
432
- full = 0.5*(a + b)
433
- if return_full_tensor:
434
- return full
435
- else:
436
- return cls(full, mapping=mapping)
824
+ return cls._broadcast_matrix(np.eye(6), shape=shape, **kwargs)
437
825
 
438
826
  @classmethod
439
- def identity_spherical_part(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
827
+ def ones(cls, shape=None, **kwargs):
440
828
  """
441
- Return the spherical part of the identity tensor
829
+ Create a 4th-order tensor full of ones.
442
830
 
443
831
  Parameters
444
832
  ----------
445
- shape : tuple of int, optional
833
+ shape : int or tuple, optional
446
834
  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.
835
+ kwargs
836
+ keyword arguments passed to the constructor
451
837
 
452
838
  Returns
453
839
  -------
454
- FourthOrderTensor or SymmetricTensor
840
+ FourthOrderTensor
841
+
842
+ Examples
843
+ --------
844
+ >>> tensor_of_ones = FourthOrderTensor.ones()
845
+ >>> tensor_of_ones
846
+ 4th-order tensor (in Kelvin mapping):
847
+ [[1. 1. 1. 1.41421356 1.41421356 1.41421356]
848
+ [1. 1. 1. 1.41421356 1.41421356 1.41421356]
849
+ [1. 1. 1. 1.41421356 1.41421356 1.41421356]
850
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]
851
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]
852
+ [1.41421356 1.41421356 1.41421356 2. 2. 2. ]]
853
+
854
+ At first sight, the tensor may appear not full of ones at all, but the representation above uses the Kelvin
855
+ mapping convention. Indeed, one can check that the full tensor is actually full of ones. E.g.:
856
+
857
+ >>> tensor_of_ones.full_tensor[0,1,0,2]
858
+ 1.0
859
+
860
+ Alternatively, the Voigt mapping convention may help figuring it out:
861
+
862
+ >>> from Elasticipy.tensors.mapping import VoigtMapping
863
+ >>> tensor_of_ones_voigt = FourthOrderTensor.ones(mapping=VoigtMapping())
864
+ >>> tensor_of_ones_voigt
865
+ 4th-order tensor (in Voigt mapping):
866
+ [[1. 1. 1. 1. 1. 1.]
867
+ [1. 1. 1. 1. 1. 1.]
868
+ [1. 1. 1. 1. 1. 1.]
869
+ [1. 1. 1. 1. 1. 1.]
870
+ [1. 1. 1. 1. 1. 1.]
871
+ [1. 1. 1. 1. 1. 1.]]
872
+
873
+ although both tensors are actually the same:
874
+
875
+ >>> tensor_of_ones == tensor_of_ones_voigt
876
+ True
455
877
  """
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)
878
+ return cls._broadcast_matrix(kelvin_mapping.matrix, shape=shape, **kwargs)
467
879
 
468
880
  @classmethod
469
- def identity_deviatoric_part(cls, shape=(), return_full_tensor=False, mapping=KelvinMapping()):
881
+ def identity_spherical_part(cls, shape=(), **kwargs):
470
882
  """
471
- Return the deviatoric part of the identity tensor
883
+ Return the spherical part of the identity tensor.
884
+
885
+ See Notes for mathematical definition.
472
886
 
473
887
  Parameters
474
888
  ----------
475
889
  shape : tuple of int, optional
476
890
  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.
891
+ kwargs
892
+ Keyword arguments passed to the Fourth-order tensor constructor.
893
+
894
+ Returns
895
+ -------
896
+ FourthOrderTensor
897
+
898
+ See Also
899
+ --------
900
+ identity_tensor : return the identity tensor
901
+ identity_deviatoric_part : return the deviatoric part of the identity tensor
902
+
903
+ Notes
904
+ -----
905
+ The spherical part of the identity tensor is defined as:
906
+
907
+ .. math::
908
+
909
+ J_{ijkl} = \\frac13 \\delta_{ij}\\delta_{kl}
910
+
911
+ Examples
912
+ --------
913
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
914
+ >>> J = FourthOrderTensor.identity_spherical_part()
915
+ >>> print(J)
916
+ 4th-order tensor (in Kelvin mapping):
917
+ [[0.33333333 0.33333333 0.33333333 0. 0. 0. ]
918
+ [0.33333333 0.33333333 0.33333333 0. 0. 0. ]
919
+ [0.33333333 0.33333333 0.33333333 0. 0. 0. ]
920
+ [0. 0. 0. 0. 0. 0. ]
921
+ [0. 0. 0. 0. 0. 0. ]
922
+ [0. 0. 0. 0. 0. 0. ]]
923
+
924
+ On can check that J has zero deviatoric part:
925
+
926
+ >>> J.deviatoric_part()
927
+ 4th-order tensor (in Kelvin mapping):
928
+ [[2.77555756e-17 2.77555756e-17 2.77555756e-17 0.00000000e+00
929
+ 0.00000000e+00 0.00000000e+00]
930
+ [2.77555756e-17 2.77555756e-17 2.77555756e-17 0.00000000e+00
931
+ 0.00000000e+00 0.00000000e+00]
932
+ [2.77555756e-17 2.77555756e-17 2.77555756e-17 0.00000000e+00
933
+ 0.00000000e+00 0.00000000e+00]
934
+ [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
935
+ 0.00000000e+00 0.00000000e+00]
936
+ [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
937
+ 0.00000000e+00 0.00000000e+00]
938
+ [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
939
+ 0.00000000e+00 0.00000000e+00]]
940
+ """
941
+ A = np.zeros((6, 6))
942
+ A[:3, :3] = 1 / 3
943
+ return cls._broadcast_matrix(A, shape=shape, **kwargs)
944
+
945
+ @classmethod
946
+ def identity_deviatoric_part(cls, **kwargs):
947
+ """
948
+ Return the deviatoric part of the identity tensor.
949
+
950
+ See notes for the mathematical definition.
951
+
952
+ Parameters
953
+ ----------
954
+ kwargs
955
+ keyword arguments passed to eye constructor
481
956
 
482
957
  Returns
483
958
  -------
484
959
  FourthOrderTensor or SymmetricTensor
960
+
961
+ See Also
962
+ --------
963
+ identity_tensor : return the identity tensor
964
+ identity_spherical_part : return the spherical part of the identity tensor
965
+
966
+ Notes
967
+ -----
968
+ The deviatoric part of the identity tensor is defined as:
969
+
970
+ .. math::
971
+
972
+ K = I - J
973
+
974
+ where :math:`I` and :math:`J` denote the identity and the deviatoric part of the identity tensor, respectively.
975
+
976
+ Examples
977
+ --------
978
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
979
+ >>> K = FourthOrderTensor.identity_deviatoric_part()
980
+ >>> print(K)
981
+ 4th-order tensor (in Kelvin mapping):
982
+ [[ 0.66666667 -0.33333333 -0.33333333 0. 0. 0. ]
983
+ [-0.33333333 0.66666667 -0.33333333 0. 0. 0. ]
984
+ [-0.33333333 -0.33333333 0.66666667 0. 0. 0. ]
985
+ [ 0. 0. 0. 1. 0. 0. ]
986
+ [ 0. 0. 0. 0. 1. 0. ]
987
+ [ 0. 0. 0. 0. 0. 1. ]]
988
+
989
+ One can check that K has zero spherical part:
990
+
991
+ >>> print(K.spherical_part())
992
+ 4th-order tensor (in Kelvin mapping):
993
+ [[2.77555756e-17 2.77555756e-17 2.77555756e-17 0.00000000e+00
994
+ 0.00000000e+00 0.00000000e+00]
995
+ [2.77555756e-17 2.77555756e-17 2.77555756e-17 0.00000000e+00
996
+ 0.00000000e+00 0.00000000e+00]
997
+ [2.77555756e-17 2.77555756e-17 2.77555756e-17 0.00000000e+00
998
+ 0.00000000e+00 0.00000000e+00]
999
+ [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
1000
+ 0.00000000e+00 0.00000000e+00]
1001
+ [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
1002
+ 0.00000000e+00 0.00000000e+00]
1003
+ [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
1004
+ 0.00000000e+00 0.00000000e+00]]
485
1005
  """
486
- I = FourthOrderTensor.identity(shape, return_full_tensor, mapping)
487
- J = FourthOrderTensor.identity_spherical_part(shape, return_full_tensor, mapping)
1006
+ I = FourthOrderTensor.identity(**kwargs)
1007
+ J = FourthOrderTensor.identity_spherical_part(**kwargs)
488
1008
  return I-J
489
1009
 
490
1010
  def spherical_part(self):
@@ -495,6 +1015,11 @@ class FourthOrderTensor:
495
1015
  -------
496
1016
  FourthOrderTensor
497
1017
  Spherical part of the tensor
1018
+
1019
+ See Also
1020
+ --------
1021
+ identity_tensor : return the identity tensor
1022
+ deviatoric_part : return the deviatoric part of the tensor
498
1023
  """
499
1024
  I = self.identity_spherical_part(shape=self.shape)
500
1025
  return I.ddot(self)
@@ -507,6 +1032,11 @@ class FourthOrderTensor:
507
1032
  -------
508
1033
  FourthOrderTensor
509
1034
  Deviatoric part of the tensor
1035
+
1036
+ See Also
1037
+ --------
1038
+ identity_tensor : return the identity tensor
1039
+ spherical_part : return the spherical part of the tensor
510
1040
  """
511
1041
  K = self.identity_deviatoric_part(shape=self.shape)
512
1042
  return K.ddot(self)
@@ -519,12 +1049,72 @@ class FourthOrderTensor:
519
1049
  -------
520
1050
  FourthOrderTensor
521
1051
  Inverse tensor
1052
+
1053
+ Examples
1054
+ --------
1055
+ Let consider a random Fourth-order tensor:
1056
+
1057
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
1058
+ >>> T = FourthOrderTensor.rand()
1059
+ >>> print(T) # doctest: +SKIP
1060
+
1061
+ >>> Tinv = T.inv()
1062
+ >>> print(Tinv) # doctest: +SKIP
1063
+
1064
+ One can check that ``T.ddot(Tinv)`` and ``Tinv.ddot(T)`` are really close to the identity tensor:
1065
+
1066
+ >>> I = FourthOrderTensor.eye()
1067
+ >>> (T.ddot(Tinv) - I) * 1e16 # doctest: +SKIP
1068
+ 4th-order tensor (in Kelvin mapping):
1069
+ [[ 1.00000000e+00 0.00000000e+00 1.11022302e-16 0.00000000e+00
1070
+ 3.14018492e-16 0.00000000e+00]
1071
+ [-2.22044605e-16 1.00000000e+00 2.77555756e-17 -1.25607397e-15
1072
+ 2.35513869e-16 -7.85046229e-17]
1073
+ [ 1.55431223e-15 0.00000000e+00 1.00000000e+00 -3.14018492e-16
1074
+ 0.00000000e+00 -4.71027738e-16]
1075
+ [-6.28036983e-16 -4.39625888e-15 0.00000000e+00 1.00000000e+00
1076
+ 0.00000000e+00 1.11022302e-16]
1077
+ [-1.25607397e-15 -4.39625888e-15 0.00000000e+00 2.55351296e-15
1078
+ 1.00000000e+00 -1.66533454e-16]
1079
+ [-5.88784672e-16 -1.17756934e-15 -2.62499833e-16 5.96744876e-16
1080
+ 1.24900090e-16 1.00000000e+00]]
1081
+
1082
+ >>> (Tinv.ddot(T) - I) * 1e16 # doctest: +SKIP
1083
+ 4th-order tensor (in Kelvin mapping):
1084
+ [[ 1.00000000e+00 -1.33226763e-15 -3.99680289e-15 -6.90840682e-15
1085
+ -7.53644380e-15 -2.51214793e-15]
1086
+ [ 2.33146835e-15 1.00000000e+00 1.55431223e-15 -7.85046229e-16
1087
+ 1.41308321e-15 9.42055475e-16]
1088
+ [ 3.88578059e-16 1.11022302e-16 1.00000000e+00 -3.92523115e-16
1089
+ -1.57009246e-16 1.57009246e-16]
1090
+ [-5.88784672e-17 -1.86448479e-16 -1.47196168e-16 1.00000000e+00
1091
+ -2.49800181e-16 -2.08166817e-16]
1092
+ [ 5.10280049e-16 7.85046229e-17 -7.85046229e-17 1.27675648e-15
1093
+ 1.00000000e+00 7.77156117e-16]
1094
+ [-7.85046229e-16 -6.28036983e-16 6.28036983e-16 2.44249065e-15
1095
+ 3.55271368e-15 1.00000000e+00]]
1096
+
1097
+ This function obvisouly also works for tensor arrays. E.g.:
1098
+
1099
+ >>> T = FourthOrderTensor.rand(shape=(5,3))
1100
+ >>> Tinv = T.inv()
1101
+ >>> Tinv.shape
1102
+ (5, 3)
1103
+
1104
+ Again, one can check that ``T.ddot(Tinv)`` is close to the array of identity tensors:
1105
+
1106
+ >>> import numpy as np
1107
+ >>> I = FourthOrderTensor.eye(shape=(5,3))
1108
+ >>> np.max(T.ddot(Tinv).matrix() - I.matrix()) # doctest: +SKIP
1109
+ 5.906386491005833e-14
522
1110
  """
523
1111
  matrix_inv = np.linalg.inv(self._matrix)
524
- return self.__class__(matrix_inv, mapping=self.mapping.mapping_inverse)
1112
+ t = self.__class__(matrix_inv, mapping=kelvin_mapping)
1113
+ t.mapping = self.mapping.mapping_inverse
1114
+ return t
525
1115
 
526
1116
  @classmethod
527
- def zeros(cls, shape=()):
1117
+ def zeros(cls, shape=(), **kwargs):
528
1118
  """
529
1119
  Create a fourth-order tensor populated with zeros
530
1120
 
@@ -532,45 +1122,138 @@ class FourthOrderTensor:
532
1122
  ----------
533
1123
  shape : int or tuple, optional
534
1124
  Shape of the tensor to create
1125
+ kwargs
1126
+ Keyword arguments passed to the FourthOrderTensor constructor
1127
+
535
1128
  Returns
536
1129
  -------
537
1130
  FourthOrderTensor
1131
+
1132
+ Examples
1133
+ --------
1134
+ The single-valued null 4th order tensor is just:
1135
+
1136
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
1137
+ >>> FourthOrderTensor.zeros()
1138
+ 4th-order tensor (in Kelvin mapping):
1139
+ [[0. 0. 0. 0. 0. 0.]
1140
+ [0. 0. 0. 0. 0. 0.]
1141
+ [0. 0. 0. 0. 0. 0.]
1142
+ [0. 0. 0. 0. 0. 0.]
1143
+ [0. 0. 0. 0. 0. 0.]
1144
+ [0. 0. 0. 0. 0. 0.]]
1145
+
1146
+ One can also create an array of such tensors:
1147
+
1148
+ >>> zeros_tensor = FourthOrderTensor.zeros(shape=3)
1149
+
1150
+ and check that it populated with zeros:
1151
+
1152
+ >>> zeros_tensor == 0.
1153
+ array([ True, True, True])
538
1154
  """
539
1155
  if isinstance(shape, int):
540
1156
  shape = (shape, 6, 6)
541
1157
  else:
542
1158
  shape = shape + (6,6)
543
1159
  zeros = np.zeros(shape)
544
- return cls(zeros)
1160
+ return cls(zeros, **kwargs)
545
1161
 
546
1162
  def matrix(self, mapping_convention=None):
1163
+ """
1164
+ Returns the components of the tensor as a matrix.
1165
+
1166
+ Parameters
1167
+ ----------
1168
+ mapping_convention : VoigtMapping, optional
1169
+ Mapping convention to use for the returned matrix. If not provided, that of the tensor is used.
1170
+
1171
+ Returns
1172
+ -------
1173
+ numpy.ndarray
1174
+ Components of the tensor as a matrix
1175
+
1176
+ Examples
1177
+ --------
1178
+ Create an identity 4th-order tensor:
1179
+
1180
+ >>> from Elasticipy.tensors.fourth_order import FourthOrderTensor
1181
+ >>> t = FourthOrderTensor.eye()
1182
+
1183
+ Its matrix with respect to Kelvin mapping is:
1184
+
1185
+ >>> t.matrix()
1186
+ array([[1., 0., 0., 0., 0., 0.],
1187
+ [0., 1., 0., 0., 0., 0.],
1188
+ [0., 0., 1., 0., 0., 0.],
1189
+ [0., 0., 0., 1., 0., 0.],
1190
+ [0., 0., 0., 0., 1., 0.],
1191
+ [0., 0., 0., 0., 0., 1.]])
1192
+
1193
+ whereas, when using the Voigt mapping, we have:
1194
+
1195
+ >>> from Elasticipy.tensors.mapping import VoigtMapping
1196
+ >>> t.matrix(mapping_convention=VoigtMapping())
1197
+ array([[1. , 0. , 0. , 0. , 0. , 0. ],
1198
+ [0. , 1. , 0. , 0. , 0. , 0. ],
1199
+ [0. , 0. , 1. , 0. , 0. , 0. ],
1200
+ [0. , 0. , 0. , 0.5, 0. , 0. ],
1201
+ [0. , 0. , 0. , 0. , 0.5, 0. ],
1202
+ [0. , 0. , 0. , 0. , 0. , 0.5]])
1203
+
1204
+ For stiffness tensors, the default mapping convention is Voigt, so that:
1205
+
1206
+ >>> from Elasticipy.tensors.elasticity import StiffnessTensor, ComplianceTensor
1207
+ >>> StiffnessTensor.eye().matrix()
1208
+ array([[1. , 0. , 0. , 0. , 0. , 0. ],
1209
+ [0. , 1. , 0. , 0. , 0. , 0. ],
1210
+ [0. , 0. , 1. , 0. , 0. , 0. ],
1211
+ [0. , 0. , 0. , 0.5, 0. , 0. ],
1212
+ [0. , 0. , 0. , 0. , 0.5, 0. ],
1213
+ [0. , 0. , 0. , 0. , 0. , 0.5]])
1214
+
1215
+ whereas for compliance tensor, the default mapping convention gives:
1216
+
1217
+ >>> ComplianceTensor.eye().matrix()
1218
+ array([[1., 0., 0., 0., 0., 0.],
1219
+ [0., 1., 0., 0., 0., 0.],
1220
+ [0., 0., 1., 0., 0., 0.],
1221
+ [0., 0., 0., 2., 0., 0.],
1222
+ [0., 0., 0., 0., 2., 0.],
1223
+ [0., 0., 0., 0., 0., 2.]])
1224
+ """
547
1225
  matrix = self._matrix
548
1226
  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
1227
+ mapping_convention = self.mapping
1228
+ elif isinstance(mapping_convention, str):
1229
+ if mapping_convention.lower() == 'voigt':
1230
+ mapping_convention = VoigtMapping()
1231
+ elif mapping_convention.lower() == 'kelvin':
1232
+ mapping_convention = kelvin_mapping
1233
+ else:
1234
+ raise ValueError('Mapping convention must be either Kelvin or Voigt')
1235
+ return matrix / kelvin_mapping.matrix * mapping_convention.matrix
1236
+
1237
+ def copy(self):
1238
+ """Create a copy of the tensor"""
1239
+ a = deepcopy(self)
1240
+ return a
559
1241
 
560
1242
  class SymmetricFourthOrderTensor(FourthOrderTensor):
561
- tensor_name = 'Symmetric 4th-order'
1243
+ _tensor_name = 'Symmetric 4th-order'
562
1244
 
563
1245
  def __init__(self, M, check_symmetries=True, force_symmetries=False, **kwargs):
564
1246
  """
565
- Construct a fully symmetric fourth-order tensor from a (...,6,6) or a (3,3,3,3) array.
1247
+ Construct a fully symmetric fourth-order tensor from a (...,6,6) or a (...,3,3,3,3) array.
566
1248
 
567
- The input matrix must be symmetric, otherwise an error is thrown (except if check_symmetry==False, see below)
1249
+ The input matrix must be symmetric, otherwise an error is thrown (except if ``check_symmetry==False``, see
1250
+ below)
568
1251
 
569
1252
  Parameters
570
1253
  ----------
571
- M : np.ndarray
572
- (6,6) matrix corresponding to the stiffness tensor, written using the Voigt notation, or array of shape
573
- (3,3,3,3).
1254
+ M : np.ndarray or FourthOrderTensor
1255
+ (6,6) matrix corresponding to the stiffness tensor, or slices of (6,6) matrices or array of shape
1256
+ (...,3,3,3,3).
574
1257
  check_symmetries : bool, optional
575
1258
  Whether to check or not that the tensor to built displays both major and minor symmetries (see Notes).
576
1259
  force_symmetries : bool, optional
@@ -596,27 +1279,27 @@ class SymmetricFourthOrderTensor(FourthOrderTensor):
596
1279
  elif check_symmetries and not np.all(np.isclose(self._matrix, self._matrix.swapaxes(-1, -2))):
597
1280
  raise ValueError('The input matrix must be symmetric')
598
1281
 
599
- def invariants(self, order='all'):
1282
+ def linear_invariants(self):
600
1283
  """
601
- Compute the invariants of the tensor.
1284
+ Compute the linear invariants of the tensor, or tensor array.
602
1285
 
603
- Compute the linear or/and quadratic invariant of the fourth-order tensor (see notes)
604
-
605
- Parameters
606
- ----------
607
- order : str, optional
608
- If 'linear', only A1 and A2 are returned
609
- If 'quadratic', A1², A2², A1*A2, B1, B2, B3, B4 and B5 are returned
610
- If 'all' (default), A1, A2, A1², A2², B1, B2, B3, B4 and B5 are returned
1286
+ If the object is a tensor array, the linear invariants are returned as arrays of each invariant. See notes for
1287
+ the actual definitions.
611
1288
 
612
1289
  Returns
613
1290
  -------
614
- tuple
615
- invariants of the given order (see above)
1291
+ A1 : float or np.ndarray
1292
+ First linear invariant
1293
+ A2 : float or np.ndarray
1294
+ Second linear invariant
1295
+
1296
+ See Also
1297
+ --------
1298
+ quadratic_invariants : compute the quadratic invariants of a fourth-order tensor
616
1299
 
617
1300
  Notes
618
1301
  -----
619
- The nomenclature of the invariants follows that of [4]_. The linear invariants are:
1302
+ The linear invariants are:
620
1303
 
621
1304
  .. math::
622
1305
 
@@ -624,7 +1307,29 @@ class SymmetricFourthOrderTensor(FourthOrderTensor):
624
1307
 
625
1308
  A_2=C_{iijj}
626
1309
 
627
- whereas the quadratic invariants are:
1310
+ """
1311
+ t = self.full_tensor
1312
+ A1 = np.einsum('...ijij->...',t)
1313
+ A2 = np.einsum('...iijj->...',t)
1314
+ return A1, A2
1315
+
1316
+ def quadratic_invariants(self):
1317
+ """
1318
+ Compute the quadratic invariants of the tensor, or tensor array.
1319
+
1320
+ If the object is a tensor array, the returned values are arrays of each invariant. See notes for definitions.
1321
+
1322
+ Returns
1323
+ -------
1324
+ B1, B2, B3, B4, B5 : float or np.ndarray
1325
+
1326
+ See Also
1327
+ --------
1328
+ linear_invariants : compute the linear invariants of a Fourth-order tensor
1329
+
1330
+ Notes
1331
+ -----
1332
+ The quadratic invariants are defined as [Norris]_:
628
1333
 
629
1334
  .. math::
630
1335
 
@@ -640,23 +1345,41 @@ class SymmetricFourthOrderTensor(FourthOrderTensor):
640
1345
 
641
1346
  References
642
1347
  ----------
643
- .. [4] Norris, A. N. (22 May 2007). "Quadratic invariants of elastic moduli". The Quarterly Journal of Mechanics
1348
+ .. [Norris] Norris, A. N. (22 May 2007). "Quadratic invariants of elastic moduli". The Quarterly Journal of Mechanics
644
1349
  and Applied Mathematics. 60 (3): 367–389. doi:10.1093/qjmam/hbm007
1350
+
645
1351
  """
646
- t = self.full_tensor()
647
- order = order.lower()
648
- A1 = np.einsum('...ijij->...',t)
649
- A2 = np.einsum('...iijj->...',t)
650
- lin_inv = (A1, A2)
651
- if order == 'linear':
652
- return lin_inv
653
- B1 = np.einsum('...ijkl,...ijkl->...',t, t)
1352
+ t = self.full_tensor
1353
+ B1 = np.einsum('...ijkl,...ijkl->...', t, t)
654
1354
  B2 = np.einsum('...iikl,...jjkl->...', t, t)
655
1355
  B3 = np.einsum('...iikl,...jkjl->...', t, t)
656
1356
  B4 = np.einsum('...kiil,...kjjl->...', t, t)
657
1357
  B5 = np.einsum('...ijkl,...ikjl->...', t, t)
658
- quad_inv = (A1**2, A2**2, A1*A2, B1, B2, B3, B4, B5)
659
- if order == 'quadratic':
660
- return quad_inv
661
- else:
662
- return lin_inv + quad_inv
1358
+ return B1, B2, B3, B4, B5
1359
+
1360
+ def infinite_random_average(self):
1361
+ """
1362
+ Compute the average of the tensor, assuming that an infinite number of random orientations is applied.
1363
+
1364
+ Returns
1365
+ -------
1366
+ FourthOrderTensor
1367
+ Average tensor or tensor array. The returned array will be of the same shape as the input object.
1368
+ """
1369
+ new_tensor = deepcopy(self)
1370
+ matrix = self._matrix / kelvin_mapping.matrix
1371
+ A = matrix[..., 0, 0] + matrix[..., 1, 1] + matrix[..., 2, 2]
1372
+ B = matrix[..., 0, 1] + matrix[..., 0, 2] + matrix[..., 1, 2]
1373
+ C = matrix[..., 3, 3] + matrix[..., 4, 4] + matrix[..., 5, 5]
1374
+ C11 = 1/5 * A + 2/15 * B + 4/15 * C
1375
+ C12 = 1/15 * A + 4/15 * B - 2/15 * C
1376
+ C44 = 1/15 * A - 1/15 * B + 1/5 * C
1377
+ new_matrix = _isotropic_matrix(C11, C12, C44)
1378
+ new_tensor._matrix = new_matrix * kelvin_mapping.matrix
1379
+ return new_tensor
1380
+
1381
+ @classmethod
1382
+ def rand(cls, shape=None, **kwargs):
1383
+ t1 = super().rand(shape)
1384
+ return cls(t1, force_symmetries=True, **kwargs)
1385
+