elasticipy 6.0.0__py3-none-any.whl → 6.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.
- elasticipy/crystal_texture.py +849 -0
- elasticipy/gui/about.py +12 -10
- elasticipy/gui/gui.py +4 -7
- elasticipy/interfaces/FEPX.py +3 -3
- elasticipy/interfaces/PRISMS.py +3 -3
- elasticipy/plasticity.py +1 -1
- elasticipy/resources/logo_text.png +0 -0
- elasticipy/spherical_function.py +5 -3
- elasticipy/tensors/elasticity.py +149 -18
- elasticipy/tensors/fourth_order.py +32 -11
- elasticipy/tensors/second_order.py +119 -18
- elasticipy/tensors/stress_strain.py +41 -25
- {elasticipy-6.0.0.dist-info → elasticipy-6.1.0.dist-info}/METADATA +6 -4
- elasticipy-6.1.0.dist-info/RECORD +31 -0
- {elasticipy-6.0.0.dist-info → elasticipy-6.1.0.dist-info}/WHEEL +1 -1
- elasticipy-6.1.0.dist-info/entry_points.txt +2 -0
- elasticipy/resources/logo_text.svg +0 -126
- elasticipy-6.0.0.dist-info/RECORD +0 -30
- elasticipy-6.0.0.dist-info/entry_points.txt +0 -2
- {elasticipy-6.0.0.dist-info → elasticipy-6.1.0.dist-info}/licenses/LICENSE +0 -0
- {elasticipy-6.0.0.dist-info → elasticipy-6.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
|
|
3
|
+
from matplotlib import pyplot as plt
|
|
4
|
+
from orix.quaternion import Orientation
|
|
5
|
+
from orix.vector import Vector3d, Miller
|
|
6
|
+
from orix.crystal_map import Phase
|
|
7
|
+
from scipy.integrate import quad_vec
|
|
8
|
+
import numpy as np
|
|
9
|
+
from elasticipy.polefigure import add_polefigure
|
|
10
|
+
from elasticipy.tensors.fourth_order import FourthOrderTensor
|
|
11
|
+
from abc import ABC
|
|
12
|
+
from scipy.spatial.transform.rotation import Rotation
|
|
13
|
+
from scipy import __version__ as scipy_version
|
|
14
|
+
|
|
15
|
+
ANGLE_35 = 35.26438968
|
|
16
|
+
ANGLE_37 = 36.6992252
|
|
17
|
+
ANGLE_54 = 54.73561032
|
|
18
|
+
ANGLE_59 = 58.97991646
|
|
19
|
+
ANGLE_63 = 63.43494882
|
|
20
|
+
ANGLE_74 = 74.20683095
|
|
21
|
+
|
|
22
|
+
def _plot_as_pf(orientations, miller, fig, projection, plot_type='plot', ax=None, **kwargs):
|
|
23
|
+
if fig is None:
|
|
24
|
+
fig = plt.figure()
|
|
25
|
+
if ax is None:
|
|
26
|
+
ax = add_polefigure(fig, projection=projection)
|
|
27
|
+
m = orientations.shape[0]
|
|
28
|
+
n = miller.shape[0]
|
|
29
|
+
phi = np.zeros((m, n))
|
|
30
|
+
theta = np.zeros((m, n))
|
|
31
|
+
for i in range(0, n):
|
|
32
|
+
mi = miller[i]
|
|
33
|
+
t = Vector3d(~orientations * mi)
|
|
34
|
+
phi[:,i] = t.azimuth
|
|
35
|
+
theta[:,i] = t.polar
|
|
36
|
+
if plot_type == 'scatter':
|
|
37
|
+
ax.scatter(phi, theta, **kwargs)
|
|
38
|
+
else:
|
|
39
|
+
line, = ax.plot(phi[:, 0], theta[:, 0], **kwargs)
|
|
40
|
+
color = line.get_color()
|
|
41
|
+
ax.plot(phi[:, 1:], theta[:, 1:], color=color)
|
|
42
|
+
ax.set_ylim([0, np.pi / 2])
|
|
43
|
+
return fig, ax
|
|
44
|
+
|
|
45
|
+
class CrystalTexture(ABC):
|
|
46
|
+
_title = 'Abstract class for crystallographic texture'
|
|
47
|
+
|
|
48
|
+
def __init__(self):
|
|
49
|
+
self.weight = 1.
|
|
50
|
+
self._details = None
|
|
51
|
+
|
|
52
|
+
def mean_tensor(self, tensor):
|
|
53
|
+
"""
|
|
54
|
+
Perform the texture-weighted mean of a 4th-order tensor.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
tensor : SymmetricFourthOrderTensor
|
|
59
|
+
Reference tensor (unrotated)
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
SymmetricFourthOrderTensor
|
|
64
|
+
mean value of the rotated tensor
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def __mul__(self, other):
|
|
69
|
+
if isinstance(other, FourthOrderTensor):
|
|
70
|
+
return self.mean_tensor(other)
|
|
71
|
+
else:
|
|
72
|
+
t = deepcopy(self)
|
|
73
|
+
t.weight = other
|
|
74
|
+
return t
|
|
75
|
+
|
|
76
|
+
def __rmul__(self, other):
|
|
77
|
+
return self * other
|
|
78
|
+
|
|
79
|
+
def __add__(self, other):
|
|
80
|
+
# self + other
|
|
81
|
+
if isinstance(other, CrystalTexture):
|
|
82
|
+
return CompositeTexture([self, other])
|
|
83
|
+
elif isinstance(other, CompositeTexture):
|
|
84
|
+
t = deepcopy(other)
|
|
85
|
+
t.texture_list.insert(0, self)
|
|
86
|
+
return t
|
|
87
|
+
|
|
88
|
+
def __repr__(self):
|
|
89
|
+
if self._details is None:
|
|
90
|
+
return self._title
|
|
91
|
+
else:
|
|
92
|
+
return self._title + '\n' + self._details
|
|
93
|
+
|
|
94
|
+
def plot_as_pole_figure(self, miller, projection='lambert', fig=None, ax=None, **kwargs):
|
|
95
|
+
"""
|
|
96
|
+
Plot the pole figure of the crystallographic texture
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
miller : orix.vector.miller.Miller
|
|
101
|
+
Miller indices of directions/planes to plot
|
|
102
|
+
projection : str, optional
|
|
103
|
+
Type of projection to use, it can be either stereographic or Lambert
|
|
104
|
+
fig : matplotlib.figure.Figure, optional
|
|
105
|
+
Handle to existing figure, if needed
|
|
106
|
+
ax : matplotlib.projections.polar.PolarAxes, optional
|
|
107
|
+
Axes to plot on
|
|
108
|
+
kwargs
|
|
109
|
+
Keyword arguments to pass to matplotlib's scatter/plot functions
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
matplotlib.figure.Figure
|
|
114
|
+
Handle to figure
|
|
115
|
+
matplotlib.projections.polar.PolarAxes
|
|
116
|
+
Axes where the pole figure is plotted
|
|
117
|
+
|
|
118
|
+
Examples
|
|
119
|
+
--------
|
|
120
|
+
Plot the [100] pole figure of Goss texture:
|
|
121
|
+
|
|
122
|
+
.. plot::
|
|
123
|
+
|
|
124
|
+
from elasticipy.crystal_texture import DiscreteTexture
|
|
125
|
+
from orix.vector import Miller
|
|
126
|
+
from orix.crystal_map import Phase
|
|
127
|
+
|
|
128
|
+
goss = DiscreteTexture.Goss()
|
|
129
|
+
phase = Phase(point_group='m3m') # BCC symmetry
|
|
130
|
+
miller = Miller([1,0,0], phase=phase)
|
|
131
|
+
goss.plot_as_pole_figure(miller.symmetrise(unique=True))
|
|
132
|
+
|
|
133
|
+
Plot the [110] pole figure of the gamma fibre texture:
|
|
134
|
+
|
|
135
|
+
.. plot::
|
|
136
|
+
|
|
137
|
+
from elasticipy.crystal_texture import FibreTexture
|
|
138
|
+
from orix.vector import Miller
|
|
139
|
+
from orix.crystal_map import Phase
|
|
140
|
+
|
|
141
|
+
gamma = FibreTexture.gamma()
|
|
142
|
+
phase = Phase(point_group='m3m') # BCC symmetry
|
|
143
|
+
miller = Miller([1,1,0], phase=phase)
|
|
144
|
+
gamma.plot_as_pole_figure(miller.symmetrise(unique=True))
|
|
145
|
+
"""
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
def sample(self, num=50, seed=None):
|
|
149
|
+
"""Generate a random sample from the texture component
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
num : int, optional
|
|
154
|
+
Number of random orientations to generate. Default is 50.
|
|
155
|
+
seed : int, optional
|
|
156
|
+
Seed to use for generating random numbers. Default is None.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
orix.quaternion.Orientation
|
|
161
|
+
Random orientations from the given texture
|
|
162
|
+
|
|
163
|
+
Examples
|
|
164
|
+
--------
|
|
165
|
+
Generate 50 orientations from uniform distribution:
|
|
166
|
+
|
|
167
|
+
>>> from elasticipy.crystal_texture import UniformTexture
|
|
168
|
+
>>> o = UniformTexture().sample()
|
|
169
|
+
|
|
170
|
+
Generate 10 orientations from gamma-fibre
|
|
171
|
+
|
|
172
|
+
>>> from elasticipy.crystal_texture import FibreTexture
|
|
173
|
+
>>> o = FibreTexture.gamma().sample(num=10)
|
|
174
|
+
"""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
class UniformTexture(CrystalTexture):
|
|
178
|
+
"""
|
|
179
|
+
Simple class to define the uniform texture over SO(3)
|
|
180
|
+
"""
|
|
181
|
+
_title = 'Uniform texture'
|
|
182
|
+
|
|
183
|
+
def __init__(self):
|
|
184
|
+
"""
|
|
185
|
+
Create a uniform texture over SO(3)
|
|
186
|
+
"""
|
|
187
|
+
super().__init__()
|
|
188
|
+
self._details = 'Uniform distribution over SO(3)'
|
|
189
|
+
|
|
190
|
+
def mean_tensor(self, tensor):
|
|
191
|
+
return tensor.infinite_random_average()
|
|
192
|
+
|
|
193
|
+
def plot_as_pole_figure(self, miller, projection='lambert', fig=None, ax=None, **kwargs):
|
|
194
|
+
if fig is None:
|
|
195
|
+
fig = plt.figure()
|
|
196
|
+
if ax is None:
|
|
197
|
+
ax = add_polefigure(fig, projection=projection)
|
|
198
|
+
phi = np.linspace(0,2*np.pi)
|
|
199
|
+
theta = np.linspace(0,np.pi)
|
|
200
|
+
phi, theta = np.meshgrid(phi, theta)
|
|
201
|
+
r = np.ones_like(phi)
|
|
202
|
+
ax.contourf(phi, theta, r, **kwargs)
|
|
203
|
+
ax.set_ylim([0, np.pi / 2])
|
|
204
|
+
return fig, ax
|
|
205
|
+
|
|
206
|
+
def sample(self, num=50, seed=None):
|
|
207
|
+
if scipy_version >= "1.15.0":
|
|
208
|
+
rand = Rotation.random(num=num, rng=seed)
|
|
209
|
+
else:
|
|
210
|
+
rand = Rotation.random(num=num, random_state=seed)
|
|
211
|
+
return Orientation.from_scipy_rotation(rand)
|
|
212
|
+
|
|
213
|
+
class DiscreteTexture(CrystalTexture):
|
|
214
|
+
"""
|
|
215
|
+
Class to handle classical crystallographic texture.
|
|
216
|
+
|
|
217
|
+
Notes
|
|
218
|
+
-----
|
|
219
|
+
This class implements the crystallographic textures listed by [Lohmuller]_
|
|
220
|
+
|
|
221
|
+
References
|
|
222
|
+
----------
|
|
223
|
+
.. [Lohmuller] Lohmuller, P.; Peltier, L.; Hazotte, A.; Zollinger, J.; Laheurte, P.; Fleury, E. Variations of
|
|
224
|
+
the Elastic Properties of the CoCrFeMnNi High Entropy Alloy Deformed by Groove Cold Rolling.
|
|
225
|
+
Materials 2018, 11, 1337. https://doi.org/10.3390/ma11081337
|
|
226
|
+
"""
|
|
227
|
+
_title = "Crystallographic texture"
|
|
228
|
+
|
|
229
|
+
def __init__(self, orientation):
|
|
230
|
+
"""
|
|
231
|
+
Create a single-orientation crystallographic texture.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
orientation : orix.quaternion.orientation.Orientation
|
|
236
|
+
Orientation of the crystals
|
|
237
|
+
"""
|
|
238
|
+
super().__init__()
|
|
239
|
+
self.orientation = orientation
|
|
240
|
+
self._details = 'φ1={:.2f}°, ϕ={:.2f}°, φ2={:.2f}°'.format(*orientation.to_euler(degrees=True)[0])
|
|
241
|
+
|
|
242
|
+
def mean_tensor(self, tensor):
|
|
243
|
+
return tensor * self.orientation
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def cube(cls):
|
|
247
|
+
"""
|
|
248
|
+
Create a Cube crystallographic texture: {100}<100>
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
DiscreteTexture
|
|
253
|
+
|
|
254
|
+
See Also
|
|
255
|
+
--------
|
|
256
|
+
A : Create a A single-orientation crystallographic texture
|
|
257
|
+
brass : Create a Brass single-orientation crystallographic texture
|
|
258
|
+
copper : Create a Copper single-orientation crystallographic texture
|
|
259
|
+
CuT : Create a CuT single-orientation crystallographic texture
|
|
260
|
+
Goss : Create a Goss single-orientation crystallographic texture
|
|
261
|
+
GossBrass : Create a Goss-Brass single-orientation crystallographic texture
|
|
262
|
+
P : Create a P single-orientation crystallographic texture
|
|
263
|
+
S : Create an S single-orientation crystallographic texture
|
|
264
|
+
"""
|
|
265
|
+
o = Orientation.from_euler([0, 0, 0], degrees=True)
|
|
266
|
+
return DiscreteTexture(o)
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def Goss(cls):
|
|
270
|
+
"""
|
|
271
|
+
Create a Goss crystallographic texture: {110}<100>
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
DiscreteTexture
|
|
276
|
+
|
|
277
|
+
See Also
|
|
278
|
+
--------
|
|
279
|
+
A : Create an A single-orientation crystallographic texture
|
|
280
|
+
brass : Create a Brass single-orientation crystallographic texture
|
|
281
|
+
copper : Create a Copper single-orientation crystallographic texture
|
|
282
|
+
cube : Create a Cube single-orientation crystallographic texture
|
|
283
|
+
CuT : Create a CuT single-orientation crystallographic texture
|
|
284
|
+
GossBrass : Create a Goss-Brass single-orientation crystallographic texture
|
|
285
|
+
P : Create a P single-orientation crystallographic texture
|
|
286
|
+
S : Create an S single-orientation crystallographic texture
|
|
287
|
+
"""
|
|
288
|
+
o = Orientation.from_euler([0, 45, 0], degrees=True)
|
|
289
|
+
return DiscreteTexture(o)
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def brass(cls):
|
|
293
|
+
"""
|
|
294
|
+
Create a Brass crystallographic texture: {110}<112>
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
DiscreteTexture
|
|
299
|
+
|
|
300
|
+
See Also
|
|
301
|
+
--------
|
|
302
|
+
copper : Create a Copper single-orientation crystallographic texture
|
|
303
|
+
cube : Create a Cube single-orientation crystallographic texture
|
|
304
|
+
GossBrass : Create a Goss-Brass single-orientation crystallographic texture
|
|
305
|
+
"""
|
|
306
|
+
o = Orientation.from_euler([ANGLE_35, 45, 0], degrees=True)
|
|
307
|
+
return DiscreteTexture(o)
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def GossBrass(cls):
|
|
311
|
+
"""
|
|
312
|
+
Create a Goss/Brass crystallographic texture: {110}<115>
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
DiscreteTexture
|
|
317
|
+
|
|
318
|
+
See Also
|
|
319
|
+
--------
|
|
320
|
+
brass : Create a Brass single-orientation crystallographic texture
|
|
321
|
+
copper : Create a Copper single-orientation crystallographic texture
|
|
322
|
+
Goss : Create a Goss single-orientation crystallographic texture
|
|
323
|
+
"""
|
|
324
|
+
o = Orientation.from_euler([ANGLE_74, 90, 45], degrees=True)
|
|
325
|
+
return DiscreteTexture(o)
|
|
326
|
+
|
|
327
|
+
@classmethod
|
|
328
|
+
def copper(cls):
|
|
329
|
+
"""
|
|
330
|
+
Create a copper crystallographic texture: {112}<111>
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
DiscreteTexture
|
|
335
|
+
|
|
336
|
+
See Also
|
|
337
|
+
--------
|
|
338
|
+
brass : Create a Brass single-orientation crystallographic texture
|
|
339
|
+
Goss : Create a Goss single-orientation crystallographic texture
|
|
340
|
+
GossBrass : Create a Goss-Brass single-orientation crystallographic texture
|
|
341
|
+
"""
|
|
342
|
+
o = Orientation.from_euler([90, ANGLE_35, 45], degrees=True)
|
|
343
|
+
return DiscreteTexture(o)
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def A(cls):
|
|
347
|
+
"""
|
|
348
|
+
Create an "A" crystallographic texture: {110}<111>
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
DiscreteTexture
|
|
353
|
+
|
|
354
|
+
See Also
|
|
355
|
+
--------
|
|
356
|
+
cube : Create a Cube single-orientation crystallographic texture
|
|
357
|
+
P : Create a P single-orientation crystallographic texture
|
|
358
|
+
S : Create an S single-orientation crystallographic texture
|
|
359
|
+
"""
|
|
360
|
+
o = Orientation.from_euler([ANGLE_35, 90, 45], degrees=True)
|
|
361
|
+
return DiscreteTexture(o)
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def P(cls):
|
|
365
|
+
"""
|
|
366
|
+
Create a "P"" crystallographic texture: {011}<211>
|
|
367
|
+
|
|
368
|
+
Returns
|
|
369
|
+
-------
|
|
370
|
+
DiscreteTexture
|
|
371
|
+
|
|
372
|
+
See Also
|
|
373
|
+
--------
|
|
374
|
+
A : Create an A single-orientation crystallographic texture
|
|
375
|
+
cube : Create a Cube single-orientation crystallographic texture
|
|
376
|
+
S : Create an S single-orientation crystallographic texture
|
|
377
|
+
"""
|
|
378
|
+
o = Orientation.from_euler([ANGLE_54, 90, 45], degrees=True)
|
|
379
|
+
return DiscreteTexture(o)
|
|
380
|
+
|
|
381
|
+
@classmethod
|
|
382
|
+
def CuT(cls):
|
|
383
|
+
"""
|
|
384
|
+
Create a CuT crystallographic texture: {552}<115>
|
|
385
|
+
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
DiscreteTexture
|
|
389
|
+
|
|
390
|
+
See Also
|
|
391
|
+
--------
|
|
392
|
+
A : Create an A single-orientation crystallographic texture
|
|
393
|
+
P : Create a P single-orientation crystallographic texture
|
|
394
|
+
S : Create an S single-orientation crystallographic texture
|
|
395
|
+
"""
|
|
396
|
+
o = Orientation.from_euler([90, ANGLE_74, 45], degrees=True)
|
|
397
|
+
return DiscreteTexture(o)
|
|
398
|
+
|
|
399
|
+
@classmethod
|
|
400
|
+
def S(cls):
|
|
401
|
+
"""
|
|
402
|
+
Create an "S" crystallographic texture: {123}<634>
|
|
403
|
+
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
DiscreteTexture
|
|
407
|
+
|
|
408
|
+
See Also
|
|
409
|
+
--------
|
|
410
|
+
A : Create an A single-orientation crystallographic texture
|
|
411
|
+
CuT : Create a CuT single-orientation crystallographic texture
|
|
412
|
+
P : Create a P single-orientation crystallographic texture
|
|
413
|
+
"""
|
|
414
|
+
o = Orientation.from_euler([ANGLE_59, ANGLE_37, ANGLE_63], degrees=True)
|
|
415
|
+
return DiscreteTexture(o)
|
|
416
|
+
|
|
417
|
+
def plot_as_pole_figure(self, miller, projection='lambert', fig=None, ax=None, **kwargs):
|
|
418
|
+
return _plot_as_pf(self.orientation, miller, fig, projection, ax=ax, plot_type='scatter', **kwargs)
|
|
419
|
+
|
|
420
|
+
def sample(self, num=50, seed=None):
|
|
421
|
+
return Orientation(np.repeat(self.orientation.data, num, axis=0))
|
|
422
|
+
|
|
423
|
+
class FibreTexture(CrystalTexture):
|
|
424
|
+
_title = 'Fibre texture'
|
|
425
|
+
|
|
426
|
+
def __init__(self, orientation, axis, point_group=None):
|
|
427
|
+
"""
|
|
428
|
+
Create a fibre-type crystallographic texture
|
|
429
|
+
|
|
430
|
+
Parameters
|
|
431
|
+
----------
|
|
432
|
+
orientation : orix.quaternion.orientation.Orientation
|
|
433
|
+
Reference orientation
|
|
434
|
+
axis : list or tuple or numpy.ndarray or orix.vector.Vector3D
|
|
435
|
+
Axis of rotation (in sample CS)
|
|
436
|
+
point_group : orix.phase.point_group.PointGroup, optional
|
|
437
|
+
Point group to use
|
|
438
|
+
"""
|
|
439
|
+
super().__init__()
|
|
440
|
+
self.orientation = orientation
|
|
441
|
+
self.axis = Vector3d(axis)
|
|
442
|
+
self.point_group = point_group
|
|
443
|
+
|
|
444
|
+
@classmethod
|
|
445
|
+
def from_Euler(cls, phi1=None, Phi=None, phi2=None, degrees=True):
|
|
446
|
+
"""
|
|
447
|
+
Create a fibre texture by providing two fixed Bunge-Euler values
|
|
448
|
+
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
phi1 : float
|
|
452
|
+
First Euler angle
|
|
453
|
+
Phi : float
|
|
454
|
+
Second Euler angle
|
|
455
|
+
phi2 : float
|
|
456
|
+
Third Euler angle
|
|
457
|
+
degrees : boolean, optional
|
|
458
|
+
If true (default), the angles must be passed in degrees (in radians otherwise)
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
FibreTexture
|
|
463
|
+
|
|
464
|
+
See Also
|
|
465
|
+
--------
|
|
466
|
+
from_Miller_axis : Define a fibre texture by aligning a miller direction with a given axis
|
|
467
|
+
|
|
468
|
+
Examples
|
|
469
|
+
--------
|
|
470
|
+
A fibre texture corresponding to constant (e.g. zero) values for phi1 and phi2, and uniform distribution of Phi
|
|
471
|
+
on [0,2π[, can be defined as follows:
|
|
472
|
+
|
|
473
|
+
>>> from elasticipy.crystal_texture import FibreTexture
|
|
474
|
+
>>> t1 = FibreTexture.from_Euler(phi1=0., phi2=0.)
|
|
475
|
+
>>> t1
|
|
476
|
+
Fibre texture
|
|
477
|
+
φ1= 0.0°, φ2= 0.0°
|
|
478
|
+
|
|
479
|
+
Similarly, the following returns a fibre texture for phi1=0 and Phi=0, and uniform distribution of phi2 on
|
|
480
|
+
[0,2π[:
|
|
481
|
+
|
|
482
|
+
>>> t2 = FibreTexture.from_Euler(phi1=0., Phi=0.)
|
|
483
|
+
>>> t2
|
|
484
|
+
Fibre texture
|
|
485
|
+
φ1= 0.0°, ϕ= 0.0°
|
|
486
|
+
"""
|
|
487
|
+
if phi1 is None:
|
|
488
|
+
orient1 = Orientation.from_euler([0., Phi, phi2] , degrees=degrees)
|
|
489
|
+
orient2 = Orientation.from_euler([1., Phi, phi2] , degrees=degrees)
|
|
490
|
+
angle_list = {'ϕ':Phi, 'φ2':phi2}
|
|
491
|
+
elif Phi is None:
|
|
492
|
+
orient1 = Orientation.from_euler([phi1, 0., phi2], degrees=degrees)
|
|
493
|
+
orient2 = Orientation.from_euler([phi1, 1., phi2], degrees=degrees)
|
|
494
|
+
angle_list = {'φ1':phi1, 'φ2':phi2}
|
|
495
|
+
elif phi2 is None:
|
|
496
|
+
orient1 = Orientation.from_euler([phi1, Phi, 0.] , degrees=degrees)
|
|
497
|
+
orient2 = Orientation.from_euler([phi1, Phi, 1.] , degrees=degrees)
|
|
498
|
+
angle_list = {'φ1':phi1, 'ϕ':Phi}
|
|
499
|
+
else:
|
|
500
|
+
raise ValueError("Exactly two Euler angles are required.")
|
|
501
|
+
axis = (~orient1 * orient2).axis
|
|
502
|
+
a = cls(orient2, axis)
|
|
503
|
+
(k1, v1), (k2, v2) = angle_list.items()
|
|
504
|
+
if not degrees:
|
|
505
|
+
v1 = v1 * 180 / np.pi
|
|
506
|
+
v2 = v2 * 180 / np.pi
|
|
507
|
+
a._details = f"{k1}= {v1}°, {k2}= {v2}°"
|
|
508
|
+
return a
|
|
509
|
+
|
|
510
|
+
@classmethod
|
|
511
|
+
def from_Miller_axis(cls, miller, axis):
|
|
512
|
+
"""
|
|
513
|
+
Create a perfect fibre crystallographic texture
|
|
514
|
+
|
|
515
|
+
Parameters
|
|
516
|
+
----------
|
|
517
|
+
miller : orix.vector.miller.Miller
|
|
518
|
+
Crystal plane or direction to align with the axis
|
|
519
|
+
axis : tuple or list
|
|
520
|
+
Axis (in sample CS) to align with
|
|
521
|
+
|
|
522
|
+
Returns
|
|
523
|
+
-------
|
|
524
|
+
FibreTexture
|
|
525
|
+
|
|
526
|
+
See Also
|
|
527
|
+
--------
|
|
528
|
+
from_Euler : define a fibre texture from two Euler angles
|
|
529
|
+
|
|
530
|
+
Examples
|
|
531
|
+
--------
|
|
532
|
+
Let's consider a cubic poly-crystal (point group: m-3m), whose orientations are defined by a perfect alignment
|
|
533
|
+
of direction <100> with the Z axis of a sample (therefore a uniform distribution around the Z axis). This
|
|
534
|
+
texture can be defined as:
|
|
535
|
+
|
|
536
|
+
>>> from orix.crystal_map import Phase
|
|
537
|
+
>>> from orix.vector.miller import Miller
|
|
538
|
+
>>> from elasticipy.crystal_texture import FibreTexture
|
|
539
|
+
>>> phase = Phase(point_group='m-3m')
|
|
540
|
+
>>> m = Miller(uvw=[1,0,0], phase=phase)
|
|
541
|
+
>>> t = FibreTexture.from_Miller_axis(m, [0,0,1])
|
|
542
|
+
>>> t
|
|
543
|
+
Fibre texture
|
|
544
|
+
<1. 0. 0.> || [0, 0, 1]
|
|
545
|
+
"""
|
|
546
|
+
ref_orient = Orientation.from_align_vectors(miller, Vector3d(axis))
|
|
547
|
+
a = cls(ref_orient, axis, point_group=miller.phase.point_group.name)
|
|
548
|
+
if miller.coordinate_format == 'uvw' or miller.coordinate_format == 'UVTW':
|
|
549
|
+
miller_str = str(miller.uvw[0])
|
|
550
|
+
miller_str = miller_str.replace('[', '<').replace(']', '>')
|
|
551
|
+
else:
|
|
552
|
+
miller_str = str(miller.hkl[0])
|
|
553
|
+
a.point_group = miller.phase.point_group.name
|
|
554
|
+
row_0 = "{miller} || {axis}".format(miller=miller_str, axis=axis)
|
|
555
|
+
a._details = row_0
|
|
556
|
+
return a
|
|
557
|
+
|
|
558
|
+
def mean_tensor(self, tensor):
|
|
559
|
+
tensor_ref_orient = tensor * ~self.orientation
|
|
560
|
+
def fun(theta):
|
|
561
|
+
rotation = ~Orientation.from_axes_angles(self.axis, theta)
|
|
562
|
+
tensor_rotated = tensor_ref_orient * rotation
|
|
563
|
+
return tensor_rotated.to_Kelvin()
|
|
564
|
+
circle = 2 * np.pi
|
|
565
|
+
res, *_ = quad_vec(fun, 0, circle)
|
|
566
|
+
return tensor.__class__.from_Kelvin(res / circle)
|
|
567
|
+
|
|
568
|
+
def plot_as_pole_figure(self, miller, n_orientations=100, fig=None, ax=None, projection='lambert', **kwargs):
|
|
569
|
+
theta = np.linspace(0, 2 * np.pi, n_orientations)
|
|
570
|
+
orientations = self.orientation * Orientation.from_axes_angles(self.axis, theta)
|
|
571
|
+
return _plot_as_pf(orientations, miller, fig, projection, ax=ax, **kwargs)
|
|
572
|
+
|
|
573
|
+
def sample(self, num=50, seed=None):
|
|
574
|
+
rng = np.random.default_rng(seed)
|
|
575
|
+
theta = rng.uniform(0.0, 2.0 * np.pi, size=num)
|
|
576
|
+
random_rot = Orientation.from_axes_angles(self.axis, theta)
|
|
577
|
+
return self.orientation * random_rot
|
|
578
|
+
|
|
579
|
+
@classmethod
|
|
580
|
+
def gamma(cls):
|
|
581
|
+
"""
|
|
582
|
+
Create a gamma fibre-texture: <111> || ND
|
|
583
|
+
|
|
584
|
+
Returns
|
|
585
|
+
-------
|
|
586
|
+
FibreTexture
|
|
587
|
+
|
|
588
|
+
See Also
|
|
589
|
+
--------
|
|
590
|
+
alpha : create an alpha fibre texture
|
|
591
|
+
epsilon : create an epsilon fibre texture
|
|
592
|
+
"""
|
|
593
|
+
phase = Phase(point_group='m3m')
|
|
594
|
+
m = Miller(uvw=[1,1,1], phase=phase)
|
|
595
|
+
return FibreTexture.from_Miller_axis(m, [0,0,1])
|
|
596
|
+
|
|
597
|
+
@classmethod
|
|
598
|
+
def alpha(cls):
|
|
599
|
+
"""
|
|
600
|
+
Create an alpha fibre-texture: <110> || RD
|
|
601
|
+
|
|
602
|
+
Returns
|
|
603
|
+
-------
|
|
604
|
+
FibreTexture
|
|
605
|
+
|
|
606
|
+
See Also
|
|
607
|
+
--------
|
|
608
|
+
gamma : create an gamma fibre texture
|
|
609
|
+
epsilon : create an epsilon fibre texture
|
|
610
|
+
"""
|
|
611
|
+
phase = Phase(point_group='m3m')
|
|
612
|
+
m = Miller(uvw=[1,1,0], phase=phase)
|
|
613
|
+
return FibreTexture.from_Miller_axis(m, [1,0,0])
|
|
614
|
+
|
|
615
|
+
@classmethod
|
|
616
|
+
def epsilon(cls):
|
|
617
|
+
"""
|
|
618
|
+
Create an epsilon fibre-texture: <110> || TD
|
|
619
|
+
|
|
620
|
+
Returns
|
|
621
|
+
-------
|
|
622
|
+
FibreTexture
|
|
623
|
+
|
|
624
|
+
See Also
|
|
625
|
+
--------
|
|
626
|
+
gamma : create an gamma fibre texture
|
|
627
|
+
alpha : create an alpha fibre texture
|
|
628
|
+
"""
|
|
629
|
+
phase = Phase(point_group='m3m')
|
|
630
|
+
m = Miller(uvw=[1,1,0], phase=phase)
|
|
631
|
+
return FibreTexture.from_Miller_axis(m, [0,1,0])
|
|
632
|
+
|
|
633
|
+
class CompositeTexture:
|
|
634
|
+
def __init__(self, texture_list):
|
|
635
|
+
"""
|
|
636
|
+
Create a mix of crystal textures
|
|
637
|
+
|
|
638
|
+
Parameters
|
|
639
|
+
----------
|
|
640
|
+
texture_list : list of CrystalTexture
|
|
641
|
+
List of crystal textures to mix
|
|
642
|
+
"""
|
|
643
|
+
self.texture_list = list(texture_list)
|
|
644
|
+
|
|
645
|
+
def __mul__(self, other):
|
|
646
|
+
# self * other
|
|
647
|
+
if isinstance(other, (float, int)):
|
|
648
|
+
tm = deepcopy(self)
|
|
649
|
+
for t in tm.texture_list:
|
|
650
|
+
t.weight *= other
|
|
651
|
+
return tm
|
|
652
|
+
elif isinstance(other, FourthOrderTensor):
|
|
653
|
+
return self.mean_tensor(other)
|
|
654
|
+
|
|
655
|
+
def __rmul__(self, other):
|
|
656
|
+
# other * self
|
|
657
|
+
return self * other
|
|
658
|
+
|
|
659
|
+
def __add__(self, other):
|
|
660
|
+
# self + other
|
|
661
|
+
if isinstance(other, CrystalTexture):
|
|
662
|
+
t = deepcopy(self)
|
|
663
|
+
t.texture_list.append(other)
|
|
664
|
+
return t
|
|
665
|
+
elif isinstance(other, CompositeTexture):
|
|
666
|
+
return CompositeTexture(self.texture_list + other.texture_list)
|
|
667
|
+
|
|
668
|
+
def __len__(self):
|
|
669
|
+
return len(self.texture_list)
|
|
670
|
+
|
|
671
|
+
def __repr__(self):
|
|
672
|
+
title = 'Mixture of crystallographic textures'
|
|
673
|
+
heading = ' Wgt. Type Component'
|
|
674
|
+
sep = ' ------------------------------------------------------------'
|
|
675
|
+
table = []
|
|
676
|
+
for t in self.texture_list:
|
|
677
|
+
if isinstance(t, DiscreteTexture):
|
|
678
|
+
kind = 'discrete'
|
|
679
|
+
elif isinstance(t, UniformTexture):
|
|
680
|
+
kind = 'uniform '
|
|
681
|
+
else:
|
|
682
|
+
kind = 'fibre '
|
|
683
|
+
table.append(' {:.2f} {} {}'.format(t.weight, kind, t._details))
|
|
684
|
+
return '\n'.join([title, heading, sep] + table)
|
|
685
|
+
|
|
686
|
+
def mean_tensor(self, tensor):
|
|
687
|
+
"""
|
|
688
|
+
Compute the weighted average of a tensor, considering each texture component separately.
|
|
689
|
+
|
|
690
|
+
Parameters
|
|
691
|
+
----------
|
|
692
|
+
tensor : FourthOrderTensor
|
|
693
|
+
Reference tensor (unrotated)
|
|
694
|
+
|
|
695
|
+
Returns
|
|
696
|
+
-------
|
|
697
|
+
FourthOrderTensor
|
|
698
|
+
|
|
699
|
+
Examples
|
|
700
|
+
--------
|
|
701
|
+
Let consider a mixture of Goss and fibre tensor (with phi1=0 and phi2=0):
|
|
702
|
+
|
|
703
|
+
>>> from elasticipy.crystal_texture import DiscreteTexture, FibreTexture
|
|
704
|
+
>>> from elasticipy.tensors.elasticity import StiffnessTensor
|
|
705
|
+
>>> t = DiscreteTexture.Goss() + FibreTexture.from_Euler(phi1=0.0, phi2=0.0)
|
|
706
|
+
>>> t
|
|
707
|
+
Mixture of crystallographic textures
|
|
708
|
+
Wgt. Type Component
|
|
709
|
+
------------------------------------------------------------
|
|
710
|
+
1.00 discrete φ1=0.00°, ϕ=45.00°, φ2=0.00°
|
|
711
|
+
1.00 fibre φ1= 0.0°, φ2= 0.0°
|
|
712
|
+
|
|
713
|
+
Then, assume that the stiffness tensor is defined as follows:
|
|
714
|
+
|
|
715
|
+
>>> C = StiffnessTensor.cubic(C11=186, C12=134, C44=77) # mp-30
|
|
716
|
+
|
|
717
|
+
The ODF-weighted Voigt average can be computed as follows:
|
|
718
|
+
|
|
719
|
+
>>> Cvoigt = t.mean_tensor(C)
|
|
720
|
+
>>> Cvoigt
|
|
721
|
+
Stiffness tensor (in Voigt mapping):
|
|
722
|
+
[[ 1.86000000e+02 1.34000000e+02 1.34000000e+02 0.00000000e+00
|
|
723
|
+
0.00000000e+00 0.00000000e+00]
|
|
724
|
+
[ 1.34000000e+02 2.24250000e+02 9.57500000e+01 6.96664948e-15
|
|
725
|
+
0.00000000e+00 0.00000000e+00]
|
|
726
|
+
[ 1.34000000e+02 9.57500000e+01 2.24250000e+02 -2.83236976e-15
|
|
727
|
+
0.00000000e+00 0.00000000e+00]
|
|
728
|
+
[ 0.00000000e+00 2.85362012e-16 8.15320034e-17 2.58750000e+01
|
|
729
|
+
0.00000000e+00 0.00000000e+00]
|
|
730
|
+
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
|
|
731
|
+
5.77500000e+01 -5.48414542e-17]
|
|
732
|
+
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
|
|
733
|
+
5.48414542e-17 5.77500000e+01]]
|
|
734
|
+
|
|
735
|
+
Alternatively, on can directly use the following syntax:
|
|
736
|
+
|
|
737
|
+
>>> Cvoigt = C * t
|
|
738
|
+
"""
|
|
739
|
+
n = len(self)
|
|
740
|
+
t = tensor.__class__.eye(shape=(n,))
|
|
741
|
+
wgt = []
|
|
742
|
+
for i in range(0, n):
|
|
743
|
+
ti = self.texture_list[i]
|
|
744
|
+
wgt.append(ti.weight)
|
|
745
|
+
t[i] = ti.mean_tensor(tensor)
|
|
746
|
+
return t.tensor_average(weights=wgt)
|
|
747
|
+
|
|
748
|
+
def plot_as_pole_figure(self, miller, fig=None, ax=None, projection='lambert'):
|
|
749
|
+
"""
|
|
750
|
+
Plot the pole figure of the composite texture, given a set of Miller indices
|
|
751
|
+
|
|
752
|
+
Parameters
|
|
753
|
+
----------
|
|
754
|
+
miller : orix.vector.miller.Miller
|
|
755
|
+
Miller indices of directions/planes to plot
|
|
756
|
+
projection : str, optional
|
|
757
|
+
Type of projection to use, it can be either stereographic or Lambert
|
|
758
|
+
fig : matplotlib.figure.Figure, optional
|
|
759
|
+
Handle to existing figure, if needed
|
|
760
|
+
ax : matplotlib.projections.polar.PolarAxes, optional
|
|
761
|
+
Axes to plot on
|
|
762
|
+
kwargs
|
|
763
|
+
Keyword arguments to pass to matplotlib's scatter/plot functions
|
|
764
|
+
|
|
765
|
+
Returns
|
|
766
|
+
-------
|
|
767
|
+
matplotlib.figure.Figure
|
|
768
|
+
Handle to figure
|
|
769
|
+
matplotlib.projections.polar.PolarAxes
|
|
770
|
+
Axes where the pole figure is plotted
|
|
771
|
+
|
|
772
|
+
Examples
|
|
773
|
+
--------
|
|
774
|
+
Create a mixture of uniform of gamma and alpha fibre texture, and plot the corresponding [1,0,0] pole figure:
|
|
775
|
+
|
|
776
|
+
.. plot::
|
|
777
|
+
|
|
778
|
+
from elasticipy.crystal_texture import FibreTexture
|
|
779
|
+
from orix.vector import Miller
|
|
780
|
+
from orix.crystal_map import Phase
|
|
781
|
+
|
|
782
|
+
texture = FibreTexture.alpha() + FibreTexture.gamma()
|
|
783
|
+
phase = Phase(point_group='m3m')
|
|
784
|
+
miller = Miller([1,1,0], phase=phase)
|
|
785
|
+
texture.plot_as_pole_figure(miller)
|
|
786
|
+
"""
|
|
787
|
+
if fig is None:
|
|
788
|
+
fig = plt.figure(tight_layout=True)
|
|
789
|
+
ax = add_polefigure(fig, projection=projection)
|
|
790
|
+
for t in self.texture_list:
|
|
791
|
+
t.plot_as_pole_figure(miller, fig=fig, projection=projection, ax=ax)
|
|
792
|
+
return fig, ax
|
|
793
|
+
|
|
794
|
+
def sample(self, num=50, seed=None):
|
|
795
|
+
"""
|
|
796
|
+
Generate a random sample of orientations from the composite texture.
|
|
797
|
+
|
|
798
|
+
Parameters
|
|
799
|
+
----------
|
|
800
|
+
num : int, optional
|
|
801
|
+
Number of orientations to generate
|
|
802
|
+
seed : int, optional
|
|
803
|
+
Seed for random number generator
|
|
804
|
+
|
|
805
|
+
Returns
|
|
806
|
+
-------
|
|
807
|
+
orix.quaternion.Orientation
|
|
808
|
+
Random orientations from the given texture
|
|
809
|
+
|
|
810
|
+
Examples
|
|
811
|
+
--------
|
|
812
|
+
Create a balanced mixture of Goss and Brass texture:
|
|
813
|
+
|
|
814
|
+
>>> from elasticipy.crystal_texture import DiscreteTexture
|
|
815
|
+
>>> goss = DiscreteTexture.Goss()
|
|
816
|
+
>>> brass = DiscreteTexture.brass()
|
|
817
|
+
>>> texture = goss + brass
|
|
818
|
+
>>> print(texture)
|
|
819
|
+
Mixture of crystallographic textures
|
|
820
|
+
Wgt. Type Component
|
|
821
|
+
------------------------------------------------------------
|
|
822
|
+
1.00 discrete φ1=0.00°, ϕ=45.00°, φ2=0.00°
|
|
823
|
+
1.00 discrete φ1=35.26°, ϕ=45.00°, φ2=0.00°
|
|
824
|
+
|
|
825
|
+
Now generate a set of 1000 orientations from this texture:
|
|
826
|
+
|
|
827
|
+
>>> o = texture.sample(num=1000, seed=123) # Use seed to ensure reproducibility
|
|
828
|
+
|
|
829
|
+
One can check that around 50% of these orientations correspond to that of Goss:
|
|
830
|
+
|
|
831
|
+
>>> np.count_nonzero((o * ~goss.orientation).angle == 0.)
|
|
832
|
+
530
|
|
833
|
+
|
|
834
|
+
whereas all the other orientations correspond to brass:
|
|
835
|
+
|
|
836
|
+
>>> np.count_nonzero((o * ~brass.orientation).angle == 0.)
|
|
837
|
+
470
|
|
838
|
+
"""
|
|
839
|
+
weights = np.array([w.weight for w in self.texture_list])
|
|
840
|
+
weights = weights / weights.sum()
|
|
841
|
+
rng = np.random.default_rng(seed)
|
|
842
|
+
counts = rng.multinomial(num, weights)
|
|
843
|
+
quat = np.zeros((num,4))
|
|
844
|
+
start_index = 0
|
|
845
|
+
for tex, ni in zip(self.texture_list, counts):
|
|
846
|
+
sub_sample = tex.sample(ni, seed=seed)
|
|
847
|
+
quat[start_index:start_index+ni] = sub_sample.data
|
|
848
|
+
start_index += ni
|
|
849
|
+
return Orientation(quat)
|