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.
@@ -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)