From a670ab269f7f33face0789cf71ee4f292c489f2d Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Thu, 20 Feb 2020 22:50:54 +0100 Subject: [PATCH 01/13] proper spacing --- python/damask/grid_filters.py | 70 ++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/python/damask/grid_filters.py b/python/damask/grid_filters.py index db9f243c9..1375b4408 100644 --- a/python/damask/grid_filters.py +++ b/python/damask/grid_filters.py @@ -1,14 +1,14 @@ from scipy import spatial import numpy as np -def __ks(size,grid,first_order=False): +def _ks(size,grid,first_order=False): """ Get wave numbers operator. Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. """ k_sk = np.where(np.arange(grid[0])>grid[0]//2,np.arange(grid[0])-grid[0],np.arange(grid[0]))/size[0] @@ -30,14 +30,14 @@ def curl(size,field): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. """ n = np.prod(field.shape[3:]) - k_s = __ks(size,field.shape[:3],True) + k_s = _ks(size,field.shape[:3],True) e = np.zeros((3, 3, 3)) - e[0, 1, 2] = e[1, 2, 0] = e[2, 0, 1] = +1.0 # Levi-Civita symbol + e[0, 1, 2] = e[1, 2, 0] = e[2, 0, 1] = +1.0 # Levi-Civita symbol e[0, 2, 1] = e[2, 1, 0] = e[1, 0, 2] = -1.0 field_fourier = np.fft.rfftn(field,axes=(0,1,2)) @@ -54,11 +54,11 @@ def divergence(size,field): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. """ n = np.prod(field.shape[3:]) - k_s = __ks(size,field.shape[:3],True) + k_s = _ks(size,field.shape[:3],True) field_fourier = np.fft.rfftn(field,axes=(0,1,2)) divergence = (np.einsum('ijkl,ijkl ->ijk', k_s,field_fourier)*2.0j*np.pi if n == 3 else # vector, 3 -> 1 @@ -74,11 +74,11 @@ def gradient(size,field): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. """ n = np.prod(field.shape[3:]) - k_s = __ks(size,field.shape[:3],True) + k_s = _ks(size,field.shape[:3],True) field_fourier = np.fft.rfftn(field,axes=(0,1,2)) gradient = (np.einsum('ijkl,ijkm->ijkm', field_fourier,k_s)*2.0j*np.pi if n == 1 else # scalar, 1 -> 3 @@ -96,7 +96,7 @@ def cell_coord0(grid,size,origin=np.zeros(3)): grid : numpy.ndarray number of grid points. size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. origin : numpy.ndarray, optional physical origin of the periodic field. Default is [0.0,0.0,0.0]. @@ -108,7 +108,8 @@ def cell_coord0(grid,size,origin=np.zeros(3)): np.linspace(start[0],end[0],grid[0]), indexing = 'ij') - return np.concatenate((z[:,:,:,None],y[:,:,:,None],x[:,:,:,None]),axis = 3) + return np.concatenate((z[:,:,:,None],y[:,:,:,None],x[:,:,:,None]),axis = 3) + def cell_displacement_fluct(size,F): """ @@ -117,14 +118,14 @@ def cell_displacement_fluct(size,F): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. F : numpy.ndarray deformation gradient field. """ integrator = 0.5j*size/np.pi - k_s = __ks(size,F.shape[:3],False) + k_s = _ks(size,F.shape[:3],False) k_s_squared = np.einsum('...l,...l',k_s,k_s) k_s_squared[0,0,0] = 1.0 @@ -136,6 +137,7 @@ def cell_displacement_fluct(size,F): return np.fft.irfftn(displacement,axes=(0,1,2),s=F.shape[:3]) + def cell_displacement_avg(size,F): """ Cell center displacement field from average part of the deformation gradient field. @@ -143,7 +145,7 @@ def cell_displacement_avg(size,F): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. F : numpy.ndarray deformation gradient field. @@ -151,6 +153,7 @@ def cell_displacement_avg(size,F): F_avg = np.average(F,axis=(0,1,2)) return np.einsum('ml,ijkl->ijkm',F_avg-np.eye(3),cell_coord0(F.shape[:3][::-1],size)) + def cell_displacement(size,F): """ Cell center displacement field from deformation gradient field. @@ -158,13 +161,14 @@ def cell_displacement(size,F): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. F : numpy.ndarray deformation gradient field. """ return cell_displacement_avg(size,F) + cell_displacement_fluct(size,F) + def cell_coord(size,F,origin=np.zeros(3)): """ Cell center positions. @@ -172,7 +176,7 @@ def cell_coord(size,F,origin=np.zeros(3)): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. F : numpy.ndarray deformation gradient field. origin : numpy.ndarray, optional @@ -181,6 +185,7 @@ def cell_coord(size,F,origin=np.zeros(3)): """ return cell_coord0(F.shape[:3][::-1],size,origin) + cell_displacement(size,F) + def cell_coord0_gridSizeOrigin(coord0,ordered=True): """ Return grid 'DNA', i.e. grid, size, and origin from array of cell positions. @@ -200,11 +205,11 @@ def cell_coord0_gridSizeOrigin(coord0,ordered=True): size = grid/np.maximum(grid-1,1) * (maxcorner-mincorner) delta = size/grid origin = mincorner - delta*.5 - + # 1D/2D: size/origin combination undefined, set origin to 0.0 size [np.where(grid==1)] = origin[np.where(grid==1)]*2. origin[np.where(grid==1)] = 0.0 - + if grid.prod() != len(coord0): raise ValueError('Data count {} does not match grid {}.'.format(len(coord0),grid)) @@ -221,6 +226,7 @@ def cell_coord0_gridSizeOrigin(coord0,ordered=True): return (grid,size,origin) + def coord0_check(coord0): """ Check whether coordinates lie on a regular grid. @@ -234,7 +240,6 @@ def coord0_check(coord0): cell_coord0_gridSizeOrigin(coord0,ordered=True) - def node_coord0(grid,size,origin=np.zeros(3)): """ Nodal positions (undeformed). @@ -244,7 +249,7 @@ def node_coord0(grid,size,origin=np.zeros(3)): grid : numpy.ndarray number of grid points. size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. origin : numpy.ndarray, optional physical origin of the periodic field. Default is [0.0,0.0,0.0]. @@ -253,8 +258,9 @@ def node_coord0(grid,size,origin=np.zeros(3)): np.linspace(origin[1],size[1]+origin[1],1+grid[1]), np.linspace(origin[0],size[0]+origin[0],1+grid[0]), indexing = 'ij') - - return np.concatenate((z[:,:,:,None],y[:,:,:,None],x[:,:,:,None]),axis = 3) + + return np.concatenate((z[:,:,:,None],y[:,:,:,None],x[:,:,:,None]),axis = 3) + def node_displacement_fluct(size,F): """ @@ -263,13 +269,14 @@ def node_displacement_fluct(size,F): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. F : numpy.ndarray deformation gradient field. """ return cell_2_node(cell_displacement_fluct(size,F)) + def node_displacement_avg(size,F): """ Nodal displacement field from average part of the deformation gradient field. @@ -277,7 +284,7 @@ def node_displacement_avg(size,F): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. F : numpy.ndarray deformation gradient field. @@ -285,6 +292,7 @@ def node_displacement_avg(size,F): F_avg = np.average(F,axis=(0,1,2)) return np.einsum('ml,ijkl->ijkm',F_avg-np.eye(3),node_coord0(F.shape[:3][::-1],size)) + def node_displacement(size,F): """ Nodal displacement field from deformation gradient field. @@ -292,13 +300,14 @@ def node_displacement(size,F): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. F : numpy.ndarray deformation gradient field. """ return node_displacement_avg(size,F) + node_displacement_fluct(size,F) + def node_coord(size,F,origin=np.zeros(3)): """ Nodal positions. @@ -306,7 +315,7 @@ def node_coord(size,F,origin=np.zeros(3)): Parameters ---------- size : numpy.ndarray - physical size of the periodic field. + physical size of the periodic field. F : numpy.ndarray deformation gradient field. origin : numpy.ndarray, optional @@ -315,22 +324,25 @@ def node_coord(size,F,origin=np.zeros(3)): """ return node_coord0(F.shape[:3][::-1],size,origin) + node_displacement(size,F) + def cell_2_node(cell_data): """Interpolate periodic cell data to nodal data.""" n = ( cell_data + np.roll(cell_data,1,(0,1,2)) + np.roll(cell_data,1,(0,)) + np.roll(cell_data,1,(1,)) + np.roll(cell_data,1,(2,)) + np.roll(cell_data,1,(0,1)) + np.roll(cell_data,1,(1,2)) + np.roll(cell_data,1,(2,0)))*0.125 - + return np.pad(n,((0,1),(0,1),(0,1))+((0,0),)*len(cell_data.shape[3:]),mode='wrap') + def node_2_cell(node_data): """Interpolate periodic nodal data to cell data.""" c = ( node_data + np.roll(node_data,1,(0,1,2)) + np.roll(node_data,1,(0,)) + np.roll(node_data,1,(1,)) + np.roll(node_data,1,(2,)) + np.roll(node_data,1,(0,1)) + np.roll(node_data,1,(1,2)) + np.roll(node_data,1,(2,0)))*0.125 - + return c[:-1,:-1,:-1] + def node_coord0_gridSizeOrigin(coord0,ordered=False): """ Return grid 'DNA', i.e. grid, size, and origin from array of nodal positions. @@ -349,7 +361,7 @@ def node_coord0_gridSizeOrigin(coord0,ordered=False): grid = np.array(list(map(len,coords)),'i') - 1 size = maxcorner-mincorner origin = mincorner - + if (grid+1).prod() != len(coord0): raise ValueError('Data count {} does not match grid {}.'.format(len(coord0),grid)) From 16ed0623af599be7dd9f309b574023d602e3d919 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Thu, 20 Feb 2020 23:03:37 +0100 Subject: [PATCH 02/13] 4 space indentation (as suggested by PEP) --- python/damask/orientation.py | 1276 +++++++++++++++++----------------- 1 file changed, 637 insertions(+), 639 deletions(-) diff --git a/python/damask/orientation.py b/python/damask/orientation.py index abdaba661..fad16c0e0 100644 --- a/python/damask/orientation.py +++ b/python/damask/orientation.py @@ -27,7 +27,7 @@ class Rotation: Convention 5: The rotation angle ω is limited to the interval [0, π]. Convention 6: the real part of a quaternion is positive, Re(q) > 0 Convention 7: P = -1 (as default). - + Usage ----- Vector "a" (defined in coordinate system "A") is passively rotated @@ -38,212 +38,214 @@ class Rotation: """ __slots__ = ['quaternion'] - - def __init__(self,quaternion = np.array([1.0,0.0,0.0,0.0])): - """ - Initializes to identity unless specified. - - Parameters - ---------- - quaternion : numpy.ndarray, optional - Unit quaternion that follows the conventions. Use .fromQuaternion to perform a sanity check. - """ - self.quaternion = quaternion.copy() - + def __init__(self,quaternion = np.array([1.0,0.0,0.0,0.0])): + """ + Initializes to identity unless specified. + + Parameters + ---------- + quaternion : numpy.ndarray, optional + Unit quaternion that follows the conventions. Use .fromQuaternion to perform a sanity check. + + """ + self.quaternion = quaternion.copy() + def __copy__(self): - """Copy.""" - return self.__class__(self.quaternion) - + """Copy.""" + return self.__class__(self.quaternion) + copy = __copy__ def __repr__(self): - """Orientation displayed as unit quaternion, rotation matrix, and Bunge-Euler angles.""" - return '\n'.join([ - 'Quaternion: (real={:.3f}, imag=<{:+.3f}, {:+.3f}, {:+.3f}>)'.format(*(self.quaternion)), - 'Matrix:\n{}'.format(self.asMatrix()), - 'Bunge Eulers / deg: ({:3.2f}, {:3.2f}, {:3.2f})'.format(*self.asEulers(degrees=True)), - ]) - + """Orientation displayed as unit quaternion, rotation matrix, and Bunge-Euler angles.""" + return '\n'.join([ + 'Quaternion: (real={:.3f}, imag=<{:+.3f}, {:+.3f}, {:+.3f}>)'.format(*(self.quaternion)), + 'Matrix:\n{}'.format(self.asMatrix()), + 'Bunge Eulers / deg: ({:3.2f}, {:3.2f}, {:3.2f})'.format(*self.asEulers(degrees=True)), + ]) + + def __mul__(self, other): - """ - Multiplication. - - Parameters - ---------- - other : numpy.ndarray or Rotation - Vector, second or fourth order tensor, or rotation object that is rotated. + """ + Multiplication. - Todo - ---- - Document details active/passive) - considere rotation of (3,3,3,3)-matrix + Parameters + ---------- + other : numpy.ndarray or Rotation + Vector, second or fourth order tensor, or rotation object that is rotated. - """ - if isinstance(other, Rotation): # rotate a rotation - self_q = self.quaternion[0] - self_p = self.quaternion[1:] - other_q = other.quaternion[0] - other_p = other.quaternion[1:] - R = self.__class__(np.append(self_q*other_q - np.dot(self_p,other_p), - self_q*other_p + other_q*self_p + P * np.cross(self_p,other_p))) - return R.standardize() - elif isinstance(other, (tuple,np.ndarray)): - if isinstance(other,tuple) or other.shape == (3,): # rotate a single (3)-vector or meshgrid - A = self.quaternion[0]**2.0 - np.dot(self.quaternion[1:],self.quaternion[1:]) - B = 2.0 * ( self.quaternion[1]*other[0] - + self.quaternion[2]*other[1] - + self.quaternion[3]*other[2]) - C = 2.0 * P*self.quaternion[0] + Todo + ---- + Document details active/passive) + considere rotation of (3,3,3,3)-matrix - return np.array([ - A*other[0] + B*self.quaternion[1] + C*(self.quaternion[2]*other[2] - self.quaternion[3]*other[1]), - A*other[1] + B*self.quaternion[2] + C*(self.quaternion[3]*other[0] - self.quaternion[1]*other[2]), - A*other[2] + B*self.quaternion[3] + C*(self.quaternion[1]*other[1] - self.quaternion[2]*other[0]), - ]) - elif other.shape == (3,3,): # rotate a single (3x3)-matrix - return np.dot(self.asMatrix(),np.dot(other,self.asMatrix().T)) - elif other.shape == (3,3,3,3,): - raise NotImplementedError + """ + if isinstance(other, Rotation): # rotate a rotation + self_q = self.quaternion[0] + self_p = self.quaternion[1:] + other_q = other.quaternion[0] + other_p = other.quaternion[1:] + R = self.__class__(np.append(self_q*other_q - np.dot(self_p,other_p), + self_q*other_p + other_q*self_p + P * np.cross(self_p,other_p))) + return R.standardize() + elif isinstance(other, (tuple,np.ndarray)): + if isinstance(other,tuple) or other.shape == (3,): # rotate a single (3)-vector or meshgrid + A = self.quaternion[0]**2.0 - np.dot(self.quaternion[1:],self.quaternion[1:]) + B = 2.0 * ( self.quaternion[1]*other[0] + + self.quaternion[2]*other[1] + + self.quaternion[3]*other[2]) + C = 2.0 * P*self.quaternion[0] + + return np.array([ + A*other[0] + B*self.quaternion[1] + C*(self.quaternion[2]*other[2] - self.quaternion[3]*other[1]), + A*other[1] + B*self.quaternion[2] + C*(self.quaternion[3]*other[0] - self.quaternion[1]*other[2]), + A*other[2] + B*self.quaternion[3] + C*(self.quaternion[1]*other[1] - self.quaternion[2]*other[0]), + ]) + elif other.shape == (3,3,): # rotate a single (3x3)-matrix + return np.dot(self.asMatrix(),np.dot(other,self.asMatrix().T)) + elif other.shape == (3,3,3,3,): + raise NotImplementedError + else: + return NotImplemented else: - return NotImplemented - else: - return NotImplemented - - + return NotImplemented + + def inverse(self): - """In-place inverse rotation/backward rotation.""" - self.quaternion[1:] *= -1 - return self - + """In-place inverse rotation/backward rotation.""" + self.quaternion[1:] *= -1 + return self + def inversed(self): - """Inverse rotation/backward rotation.""" - return self.copy().inverse() + """Inverse rotation/backward rotation.""" + return self.copy().inverse() def standardize(self): - """In-place quaternion representation with positive q.""" - if self.quaternion[0] < 0.0: self.quaternion*=-1 - return self + """In-place quaternion representation with positive q.""" + if self.quaternion[0] < 0.0: self.quaternion*=-1 + return self def standardized(self): - """Quaternion representation with positive q.""" - return self.copy().standardize() + """Quaternion representation with positive q.""" + return self.copy().standardize() def misorientation(self,other): - """ - Get Misorientation. + """ + Get Misorientation. - Parameters - ---------- - other : Rotation - Rotation to which the misorientation is computed. + Parameters + ---------- + other : Rotation + Rotation to which the misorientation is computed. - """ - return other*self.inversed() + """ + return other*self.inversed() def average(self,other): - """ - Calculate the average rotation. + """ + Calculate the average rotation. - Parameters - ---------- - other : Rotation - Rotation from which the average is rotated. + Parameters + ---------- + other : Rotation + Rotation from which the average is rotated. + + """ + return Rotation.fromAverage([self,other]) - """ - return Rotation.fromAverage([self,other]) - ################################################################################################ # convert to different orientation representations (numpy arrays) def asQuaternion(self): - """ - Unit quaternion [q, p_1, p_2, p_3] unless quaternion == True: damask.quaternion object. + """ + Unit quaternion [q, p_1, p_2, p_3] unless quaternion == True: damask.quaternion object. + + Parameters + ---------- + quaternion : bool, optional + return quaternion as DAMASK object. + + """ + return self.quaternion - Parameters - ---------- - quaternion : bool, optional - return quaternion as DAMASK object. - - """ - return self.quaternion - def asEulers(self, degrees = False): - """ - Bunge-Euler angles: (φ_1, ϕ, φ_2). + """ + Bunge-Euler angles: (φ_1, ϕ, φ_2). - Parameters - ---------- - degrees : bool, optional - return angles in degrees. + Parameters + ---------- + degrees : bool, optional + return angles in degrees. + + """ + eu = qu2eu(self.quaternion) + if degrees: eu = np.degrees(eu) + return eu - """ - eu = qu2eu(self.quaternion) - if degrees: eu = np.degrees(eu) - return eu - def asAxisAngle(self, degrees = False, pair = False): - """ - Axis angle representation [n_1, n_2, n_3, ω] unless pair == True: ([n_1, n_2, n_3], ω). + """ + Axis angle representation [n_1, n_2, n_3, ω] unless pair == True: ([n_1, n_2, n_3], ω). + + Parameters + ---------- + degrees : bool, optional + return rotation angle in degrees. + pair : bool, optional + return tuple of axis and angle. + + """ + ax = qu2ax(self.quaternion) + if degrees: ax[3] = np.degrees(ax[3]) + return (ax[:3],np.degrees(ax[3])) if pair else ax - Parameters - ---------- - degrees : bool, optional - return rotation angle in degrees. - pair : bool, optional - return tuple of axis and angle. - - """ - ax = qu2ax(self.quaternion) - if degrees: ax[3] = np.degrees(ax[3]) - return (ax[:3],np.degrees(ax[3])) if pair else ax - def asMatrix(self): - """Rotation matrix.""" - return qu2om(self.quaternion) + """Rotation matrix.""" + return qu2om(self.quaternion) def asRodrigues(self, vector = False): - """ - Rodrigues-Frank vector representation [n_1, n_2, n_3, tan(ω/2)] unless vector == True: [n_1, n_2, n_3] * tan(ω/2). + """ + Rodrigues-Frank vector representation [n_1, n_2, n_3, tan(ω/2)] unless vector == True: + [n_1, n_2, n_3] * tan(ω/2). - Parameters - ---------- - vector : bool, optional - return as actual Rodrigues--Frank vector, i.e. rotation axis scaled by tan(ω/2). + Parameters + ---------- + vector : bool, optional + return as actual Rodrigues--Frank vector, i.e. rotation axis scaled by tan(ω/2). + + """ + ro = qu2ro(self.quaternion) + return ro[:3]*ro[3] if vector else ro - """ - ro = qu2ro(self.quaternion) - return ro[:3]*ro[3] if vector else ro - def asHomochoric(self): - """Homochoric vector: (h_1, h_2, h_3).""" - return qu2ho(self.quaternion) - + """Homochoric vector: (h_1, h_2, h_3).""" + return qu2ho(self.quaternion) + def asCubochoric(self): - """Cubochoric vector: (c_1, c_2, c_3).""" - return qu2cu(self.quaternion) - + """Cubochoric vector: (c_1, c_2, c_3).""" + return qu2cu(self.quaternion) + def asM(self): - """ - Intermediate representation supporting quaternion averaging. + """ + Intermediate representation supporting quaternion averaging. - References - ---------- - F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 - https://doi.org/10.2514/1.28949 + References + ---------- + F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 + https://doi.org/10.2514/1.28949 + + """ + return np.outer(self.quaternion,self.quaternion) - """ - return np.outer(self.quaternion,self.quaternion) - ################################################################################################ # static constructors. The input data needs to follow the convention, options allow to @@ -253,466 +255,462 @@ class Rotation: acceptHomomorph = False, P = -1): - qu = quaternion if isinstance(quaternion,np.ndarray) and quaternion.dtype == np.dtype(float) \ - else np.array(quaternion,dtype=float) - if P > 0: qu[1:4] *= -1 # convert from P=1 to P=-1 - if qu[0] < 0.0: - if acceptHomomorph: - qu *= -1. - else: - raise ValueError('Quaternion has negative first component.\n{}'.format(qu[0])) - if not np.isclose(np.linalg.norm(qu), 1.0): - raise ValueError('Quaternion is not of unit length.\n{} {} {} {}'.format(*qu)) + qu = quaternion if isinstance(quaternion,np.ndarray) and quaternion.dtype == np.dtype(float) \ + else np.array(quaternion,dtype=float) + if P > 0: qu[1:4] *= -1 # convert from P=1 to P=-1 + if qu[0] < 0.0: + if acceptHomomorph: + qu *= -1. + else: + raise ValueError('Quaternion has negative first component.\n{}'.format(qu[0])) + if not np.isclose(np.linalg.norm(qu), 1.0): + raise ValueError('Quaternion is not of unit length.\n{} {} {} {}'.format(*qu)) + + return Rotation(qu) - return Rotation(qu) - @staticmethod def fromEulers(eulers, degrees = False): - eu = eulers if isinstance(eulers, np.ndarray) and eulers.dtype == np.dtype(float) \ - else np.array(eulers,dtype=float) - eu = np.radians(eu) if degrees else eu - if np.any(eu < 0.0) or np.any(eu > 2.0*np.pi) or eu[1] > np.pi: - raise ValueError('Euler angles outside of [0..2π],[0..π],[0..2π].\n{} {} {}.'.format(*eu)) - - return Rotation(eu2qu(eu)) - + eu = eulers if isinstance(eulers, np.ndarray) and eulers.dtype == np.dtype(float) \ + else np.array(eulers,dtype=float) + eu = np.radians(eu) if degrees else eu + if np.any(eu < 0.0) or np.any(eu > 2.0*np.pi) or eu[1] > np.pi: + raise ValueError('Euler angles outside of [0..2π],[0..π],[0..2π].\n{} {} {}.'.format(*eu)) + + return Rotation(eu2qu(eu)) + @staticmethod def fromAxisAngle(angleAxis, degrees = False, normalise = False, P = -1): - - ax = angleAxis if isinstance(angleAxis, np.ndarray) and angleAxis.dtype == np.dtype(float) \ - else np.array(angleAxis,dtype=float) - if P > 0: ax[0:3] *= -1 # convert from P=1 to P=-1 - if degrees: ax[ 3] = np.radians(ax[3]) - if normalise: ax[0:3] /= np.linalg.norm(ax[0:3]) - if ax[3] < 0.0 or ax[3] > np.pi: - raise ValueError('Axis angle rotation angle outside of [0..π].\n'.format(ax[3])) - if not np.isclose(np.linalg.norm(ax[0:3]), 1.0): - raise ValueError('Axis angle rotation axis is not of unit length.\n{} {} {}'.format(*ax[0:3])) - return Rotation(ax2qu(ax)) - + ax = angleAxis if isinstance(angleAxis, np.ndarray) and angleAxis.dtype == np.dtype(float) \ + else np.array(angleAxis,dtype=float) + if P > 0: ax[0:3] *= -1 # convert from P=1 to P=-1 + if degrees: ax[ 3] = np.radians(ax[3]) + if normalise: ax[0:3] /= np.linalg.norm(ax[0:3]) + if ax[3] < 0.0 or ax[3] > np.pi: + raise ValueError('Axis angle rotation angle outside of [0..π].\n'.format(ax[3])) + if not np.isclose(np.linalg.norm(ax[0:3]), 1.0): + raise ValueError('Axis angle rotation axis is not of unit length.\n{} {} {}'.format(*ax[0:3])) + + return Rotation(ax2qu(ax)) + @staticmethod def fromBasis(basis, orthonormal = True, reciprocal = False, ): - - om = basis if isinstance(basis, np.ndarray) else np.array(basis).reshape((3,3)) - if reciprocal: - om = np.linalg.inv(om.T/np.pi) # transform reciprocal basis set - orthonormal = False # contains stretch - if not orthonormal: - (U,S,Vh) = np.linalg.svd(om) # singular value decomposition - om = np.dot(U,Vh) - if not np.isclose(np.linalg.det(om),1.0): - raise ValueError('matrix is not a proper rotation.\n{}'.format(om)) - if not np.isclose(np.dot(om[0],om[1]), 0.0) \ - or not np.isclose(np.dot(om[1],om[2]), 0.0) \ - or not np.isclose(np.dot(om[2],om[0]), 0.0): - raise ValueError('matrix is not orthogonal.\n{}'.format(om)) - return Rotation(om2qu(om)) - + om = basis if isinstance(basis, np.ndarray) else np.array(basis).reshape((3,3)) + if reciprocal: + om = np.linalg.inv(om.T/np.pi) # transform reciprocal basis set + orthonormal = False # contains stretch + if not orthonormal: + (U,S,Vh) = np.linalg.svd(om) # singular value decomposition + om = np.dot(U,Vh) + if not np.isclose(np.linalg.det(om),1.0): + raise ValueError('matrix is not a proper rotation.\n{}'.format(om)) + if not np.isclose(np.dot(om[0],om[1]), 0.0) \ + or not np.isclose(np.dot(om[1],om[2]), 0.0) \ + or not np.isclose(np.dot(om[2],om[0]), 0.0): + raise ValueError('matrix is not orthogonal.\n{}'.format(om)) + + return Rotation(om2qu(om)) + @staticmethod def fromMatrix(om, ): - - return Rotation.fromBasis(om) - + + return Rotation.fromBasis(om) + @staticmethod def fromRodrigues(rodrigues, normalise = False, P = -1): - - ro = rodrigues if isinstance(rodrigues, np.ndarray) and rodrigues.dtype == np.dtype(float) \ - else np.array(rodrigues,dtype=float) - if P > 0: ro[0:3] *= -1 # convert from P=1 to P=-1 - if normalise: ro[0:3] /= np.linalg.norm(ro[0:3]) - if not np.isclose(np.linalg.norm(ro[0:3]), 1.0): - raise ValueError('Rodrigues rotation axis is not of unit length.\n{} {} {}'.format(*ro[0:3])) - if ro[3] < 0.0: - raise ValueError('Rodriques rotation angle not positive.\n'.format(ro[3])) - - return Rotation(ro2qu(ro)) - + + ro = rodrigues if isinstance(rodrigues, np.ndarray) and rodrigues.dtype == np.dtype(float) \ + else np.array(rodrigues,dtype=float) + if P > 0: ro[0:3] *= -1 # convert from P=1 to P=-1 + if normalise: ro[0:3] /= np.linalg.norm(ro[0:3]) + if not np.isclose(np.linalg.norm(ro[0:3]), 1.0): + raise ValueError('Rodrigues rotation axis is not of unit length.\n{} {} {}'.format(*ro[0:3])) + if ro[3] < 0.0: + raise ValueError('Rodriques rotation angle not positive.\n'.format(ro[3])) + + return Rotation(ro2qu(ro)) + @staticmethod def fromHomochoric(homochoric, P = -1): - - ho = homochoric if isinstance(homochoric, np.ndarray) and homochoric.dtype == np.dtype(float) \ - else np.array(homochoric,dtype=float) - if P > 0: ho *= -1 # convert from P=1 to P=-1 - return Rotation(ho2qu(ho)) - + ho = homochoric if isinstance(homochoric, np.ndarray) and homochoric.dtype == np.dtype(float) \ + else np.array(homochoric,dtype=float) + if P > 0: ho *= -1 # convert from P=1 to P=-1 + + return Rotation(ho2qu(ho)) + @staticmethod def fromCubochoric(cubochoric, P = -1): - - cu = cubochoric if isinstance(cubochoric, np.ndarray) and cubochoric.dtype == np.dtype(float) \ - else np.array(cubochoric,dtype=float) - ho = cu2ho(cu) - if P > 0: ho *= -1 # convert from P=1 to P=-1 - - return Rotation(ho2qu(ho)) + + cu = cubochoric if isinstance(cubochoric, np.ndarray) and cubochoric.dtype == np.dtype(float) \ + else np.array(cubochoric,dtype=float) + ho = cu2ho(cu) + if P > 0: ho *= -1 # convert from P=1 to P=-1 + + return Rotation(ho2qu(ho)) @staticmethod def fromAverage(rotations, weights = []): - """ - Average rotation. - - References - ---------- - F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 - https://doi.org/10.2514/1.28949 - - Parameters - ---------- - rotations : list of Rotations - Rotations to average from - weights : list of floats, optional - Weights for each rotation used for averaging + """ + Average rotation. - """ - if not all(isinstance(item, Rotation) for item in rotations): - raise TypeError("Only instances of Rotation can be averaged.") + References + ---------- + F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 + https://doi.org/10.2514/1.28949 - N = len(rotations) - if weights == [] or not weights: - weights = np.ones(N,dtype='i') + Parameters + ---------- + rotations : list of Rotations + Rotations to average from + weights : list of floats, optional + Weights for each rotation used for averaging - for i,(r,n) in enumerate(zip(rotations,weights)): - M = r.asM() * n if i == 0 \ - else M + r.asM() * n # noqa add (multiples) of this rotation to average noqa - eig, vec = np.linalg.eig(M/N) + """ + if not all(isinstance(item, Rotation) for item in rotations): + raise TypeError("Only instances of Rotation can be averaged.") - return Rotation.fromQuaternion(np.real(vec.T[eig.argmax()]),acceptHomomorph = True) + N = len(rotations) + if weights == [] or not weights: + weights = np.ones(N,dtype='i') + + for i,(r,n) in enumerate(zip(rotations,weights)): + M = r.asM() * n if i == 0 \ + else M + r.asM() * n # noqa add (multiples) of this rotation to average noqa + eig, vec = np.linalg.eig(M/N) + + return Rotation.fromQuaternion(np.real(vec.T[eig.argmax()]),acceptHomomorph = True) @staticmethod def fromRandom(): - r = np.random.random(3) - A = np.sqrt(r[2]) - B = np.sqrt(1.0-r[2]) - return Rotation(np.array([np.cos(2.0*np.pi*r[0])*A, - np.sin(2.0*np.pi*r[1])*B, - np.cos(2.0*np.pi*r[1])*B, - np.sin(2.0*np.pi*r[0])*A])).standardize() + r = np.random.random(3) + A = np.sqrt(r[2]) + B = np.sqrt(1.0-r[2]) + return Rotation(np.array([np.cos(2.0*np.pi*r[0])*A, + np.sin(2.0*np.pi*r[1])*B, + np.cos(2.0*np.pi*r[1])*B, + np.sin(2.0*np.pi*r[0])*A])).standardize() # ****************************************************************************************** class Symmetry: - """ - Symmetry operations for lattice systems. - - References - ---------- - https://en.wikipedia.org/wiki/Crystal_system - - """ - - lattices = [None,'orthorhombic','tetragonal','hexagonal','cubic',] - - def __init__(self, symmetry = None): """ - Symmetry Definition. - - Parameters - ---------- - symmetry : str, optional - label of the crystal system - - """ - if symmetry is not None and symmetry.lower() not in Symmetry.lattices: - raise KeyError('Symmetry/crystal system "{}" is unknown'.format(symmetry)) - - self.lattice = symmetry.lower() if isinstance(symmetry,str) else symmetry - - - def __copy__(self): - """Copy.""" - return self.__class__(self.lattice) - - copy = __copy__ - - - def __repr__(self): - """Readable string.""" - return '{}'.format(self.lattice) - - - def __eq__(self, other): - """ - Equal to other. - - Parameters - ---------- - other : Symmetry - Symmetry to check for equality. - - """ - return self.lattice == other.lattice - - def __neq__(self, other): - """ - Not Equal to other. - - Parameters - ---------- - other : Symmetry - Symmetry to check for inequality. - - """ - return not self.__eq__(other) - - def __cmp__(self,other): - """ - Linear ordering. - - Parameters - ---------- - other : Symmetry - Symmetry to check for for order. - - """ - myOrder = Symmetry.lattices.index(self.lattice) - otherOrder = Symmetry.lattices.index(other.lattice) - return (myOrder > otherOrder) - (myOrder < otherOrder) - - def symmetryOperations(self,members=[]): - """List (or single element) of symmetry operations as rotations.""" - if self.lattice == 'cubic': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, 0.0, 0.0, 1.0 ], - [ 0.0, 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2) ], - [ 0.0, 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], - [ 0.0, -0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], - [ 0.5, 0.5, 0.5, 0.5 ], - [-0.5, 0.5, 0.5, 0.5 ], - [-0.5, 0.5, 0.5, -0.5 ], - [-0.5, 0.5, -0.5, 0.5 ], - [-0.5, -0.5, 0.5, 0.5 ], - [-0.5, -0.5, 0.5, -0.5 ], - [-0.5, -0.5, -0.5, 0.5 ], - [-0.5, 0.5, -0.5, -0.5 ], - [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [-0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2), 0.0 ], - [-0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2), 0.0 ], - [-0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0, 0.0 ], - [-0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0, 0.0 ], - ] - elif self.lattice == 'hexagonal': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [-0.5*np.sqrt(3), 0.0, 0.0, -0.5 ], - [ 0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], - [ 0.0, 0.0, 0.0, 1.0 ], - [-0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], - [-0.5*np.sqrt(3), 0.0, 0.0, 0.5 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, -0.5*np.sqrt(3), 0.5, 0.0 ], - [ 0.0, 0.5, -0.5*np.sqrt(3), 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, -0.5, -0.5*np.sqrt(3), 0.0 ], - [ 0.0, 0.5*np.sqrt(3), 0.5, 0.0 ], - ] - elif self.lattice == 'tetragonal': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, 0.0, 0.0, 1.0 ], - [ 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], - [ 0.0, -0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], - [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - ] - elif self.lattice == 'orthorhombic': - symQuats = [ - [ 1.0,0.0,0.0,0.0 ], - [ 0.0,1.0,0.0,0.0 ], - [ 0.0,0.0,1.0,0.0 ], - [ 0.0,0.0,0.0,1.0 ], - ] - else: - symQuats = [ - [ 1.0,0.0,0.0,0.0 ], - ] - - symOps = list(map(Rotation, - np.array(symQuats)[np.atleast_1d(members) if members != [] else range(len(symQuats))])) - try: - iter(members) # asking for (even empty) list of members? - except TypeError: - return symOps[0] # no, return rotation object - else: - return symOps # yes, return list of rotations - - - def inFZ(self,rodrigues): - """ - Check whether given Rodriques-Frank vector falls into fundamental zone of own symmetry. - - Fundamental zone in Rodrigues space is point symmetric around origin. - """ - if (len(rodrigues) != 3): - raise ValueError('Input is not a Rodriques-Frank vector.\n') - - if np.any(rodrigues == np.inf): return False - - Rabs = abs(rodrigues) - - if self.lattice == 'cubic': - return math.sqrt(2.0)-1.0 >= Rabs[0] \ - and math.sqrt(2.0)-1.0 >= Rabs[1] \ - and math.sqrt(2.0)-1.0 >= Rabs[2] \ - and 1.0 >= Rabs[0] + Rabs[1] + Rabs[2] - elif self.lattice == 'hexagonal': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] \ - and 2.0 >= math.sqrt(3)*Rabs[0] + Rabs[1] \ - and 2.0 >= math.sqrt(3)*Rabs[1] + Rabs[0] \ - and 2.0 >= math.sqrt(3) + Rabs[2] - elif self.lattice == 'tetragonal': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] \ - and math.sqrt(2.0) >= Rabs[0] + Rabs[1] \ - and math.sqrt(2.0) >= Rabs[2] + 1.0 - elif self.lattice == 'orthorhombic': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] - else: - return True - - - def inDisorientationSST(self,rodrigues): - """ - Check whether given Rodriques-Frank vector (of misorientation) falls into standard stereographic triangle of own symmetry. + Symmetry operations for lattice systems. References ---------- - A. Heinz and P. Neumann, Acta Crystallographica Section A 47:780-789, 1991 - https://doi.org/10.1107/S0108767391006864 + https://en.wikipedia.org/wiki/Crystal_system """ - if (len(rodrigues) != 3): - raise ValueError('Input is not a Rodriques-Frank vector.\n') - R = rodrigues - - epsilon = 0.0 - if self.lattice == 'cubic': - return R[0] >= R[1]+epsilon and R[1] >= R[2]+epsilon and R[2] >= epsilon - elif self.lattice == 'hexagonal': - return R[0] >= math.sqrt(3)*(R[1]-epsilon) and R[1] >= epsilon and R[2] >= epsilon + lattices = [None,'orthorhombic','tetragonal','hexagonal','cubic',] - elif self.lattice == 'tetragonal': - return R[0] >= R[1]-epsilon and R[1] >= epsilon and R[2] >= epsilon + def __init__(self, symmetry = None): + """ + Symmetry Definition. - elif self.lattice == 'orthorhombic': - return R[0] >= epsilon and R[1] >= epsilon and R[2] >= epsilon + Parameters + ---------- + symmetry : str, optional + label of the crystal system - else: - return True + """ + if symmetry is not None and symmetry.lower() not in Symmetry.lattices: + raise KeyError('Symmetry/crystal system "{}" is unknown'.format(symmetry)) + + self.lattice = symmetry.lower() if isinstance(symmetry,str) else symmetry - def inSST(self, - vector, - proper = False, - color = False): - """ - Check whether given vector falls into standard stereographic triangle of own symmetry. + def __copy__(self): + """Copy.""" + return self.__class__(self.lattice) - proper considers only vectors with z >= 0, hence uses two neighboring SSTs. - Return inverse pole figure color if requested. - Bases are computed from + copy = __copy__ - basis = {'cubic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,1.]/np.sqrt(2.), # direction of green - [1.,1.,1.]/np.sqrt(3.)]).T), # direction of blue - 'hexagonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [np.sqrt(3.),1.,0.]/np.sqrt(4.)]).T), # direction of blue - 'tetragonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [1.,1.,0.]/np.sqrt(2.)]).T), # direction of blue - 'orthorhombic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [0.,1.,0.]]).T), # direction of blue - } - """ - if self.lattice == 'cubic': - basis = {'improper':np.array([ [-1. , 0. , 1. ], - [ np.sqrt(2.) , -np.sqrt(2.) , 0. ], - [ 0. , np.sqrt(3.) , 0. ] ]), - 'proper':np.array([ [ 0. , -1. , 1. ], - [-np.sqrt(2.) , np.sqrt(2.) , 0. ], - [ np.sqrt(3.) , 0. , 0. ] ]), - } - elif self.lattice == 'hexagonal': - basis = {'improper':np.array([ [ 0. , 0. , 1. ], - [ 1. , -np.sqrt(3.) , 0. ], - [ 0. , 2. , 0. ] ]), - 'proper':np.array([ [ 0. , 0. , 1. ], - [-1. , np.sqrt(3.) , 0. ], - [ np.sqrt(3.) , -1. , 0. ] ]), - } - elif self.lattice == 'tetragonal': - basis = {'improper':np.array([ [ 0. , 0. , 1. ], - [ 1. , -1. , 0. ], - [ 0. , np.sqrt(2.) , 0. ] ]), - 'proper':np.array([ [ 0. , 0. , 1. ], - [-1. , 1. , 0. ], - [ np.sqrt(2.) , 0. , 0. ] ]), - } - elif self.lattice == 'orthorhombic': - basis = {'improper':np.array([ [ 0., 0., 1.], - [ 1., 0., 0.], - [ 0., 1., 0.] ]), - 'proper':np.array([ [ 0., 0., 1.], - [-1., 0., 0.], - [ 0., 1., 0.] ]), - } - else: # direct exit for unspecified symmetry - if color: - return (True,np.zeros(3,'d')) - else: - return True - v = np.array(vector,dtype=float) - if proper: # check both improper ... - theComponents = np.around(np.dot(basis['improper'],v),12) - inSST = np.all(theComponents >= 0.0) - if not inSST: # ... and proper SST - theComponents = np.around(np.dot(basis['proper'],v),12) - inSST = np.all(theComponents >= 0.0) - else: - v[2] = abs(v[2]) # z component projects identical - theComponents = np.around(np.dot(basis['improper'],v),12) # for positive and negative values - inSST = np.all(theComponents >= 0.0) + def __repr__(self): + """Readable string.""" + return '{}'.format(self.lattice) - if color: # have to return color array - if inSST: - rgb = np.power(theComponents/np.linalg.norm(theComponents),0.5) # smoothen color ramps - rgb = np.minimum(np.ones(3,dtype=float),rgb) # limit to maximum intensity - rgb /= max(rgb) # normalize to (HS)V = 1 - else: - rgb = np.zeros(3,dtype=float) - return (inSST,rgb) - else: - return inSST + + def __eq__(self, other): + """ + Equal to other. + + Parameters + ---------- + other : Symmetry + Symmetry to check for equality. + + """ + return self.lattice == other.lattice + + def __neq__(self, other): + """ + Not Equal to other. + + Parameters + ---------- + other : Symmetry + Symmetry to check for inequality. + + """ + return not self.__eq__(other) + + def __cmp__(self,other): + """ + Linear ordering. + + Parameters + ---------- + other : Symmetry + Symmetry to check for for order. + + """ + myOrder = Symmetry.lattices.index(self.lattice) + otherOrder = Symmetry.lattices.index(other.lattice) + return (myOrder > otherOrder) - (myOrder < otherOrder) + + def symmetryOperations(self,members=[]): + """List (or single element) of symmetry operations as rotations.""" + if self.lattice == 'cubic': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, 0.0, 0.0, 1.0 ], + [ 0.0, 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2) ], + [ 0.0, 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], + [ 0.0, -0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], + [ 0.5, 0.5, 0.5, 0.5 ], + [-0.5, 0.5, 0.5, 0.5 ], + [-0.5, 0.5, 0.5, -0.5 ], + [-0.5, 0.5, -0.5, 0.5 ], + [-0.5, -0.5, 0.5, 0.5 ], + [-0.5, -0.5, 0.5, -0.5 ], + [-0.5, -0.5, -0.5, 0.5 ], + [-0.5, 0.5, -0.5, -0.5 ], + [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [-0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2), 0.0 ], + [-0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2), 0.0 ], + [-0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0, 0.0 ], + [-0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0, 0.0 ], + ] + elif self.lattice == 'hexagonal': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [-0.5*np.sqrt(3), 0.0, 0.0, -0.5 ], + [ 0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], + [ 0.0, 0.0, 0.0, 1.0 ], + [-0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], + [-0.5*np.sqrt(3), 0.0, 0.0, 0.5 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, -0.5*np.sqrt(3), 0.5, 0.0 ], + [ 0.0, 0.5, -0.5*np.sqrt(3), 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, -0.5, -0.5*np.sqrt(3), 0.0 ], + [ 0.0, 0.5*np.sqrt(3), 0.5, 0.0 ], + ] + elif self.lattice == 'tetragonal': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, 0.0, 0.0, 1.0 ], + [ 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], + [ 0.0, -0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], + [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + ] + elif self.lattice == 'orthorhombic': + symQuats = [ + [ 1.0,0.0,0.0,0.0 ], + [ 0.0,1.0,0.0,0.0 ], + [ 0.0,0.0,1.0,0.0 ], + [ 0.0,0.0,0.0,1.0 ], + ] + else: + symQuats = [ + [ 1.0,0.0,0.0,0.0 ], + ] + + symOps = list(map(Rotation, + np.array(symQuats)[np.atleast_1d(members) if members != [] else range(len(symQuats))])) + try: + iter(members) # asking for (even empty) list of members? + except TypeError: + return symOps[0] # no, return rotation object + else: + return symOps # yes, return list of rotations + + + def inFZ(self,rodrigues): + """ + Check whether given Rodriques-Frank vector falls into fundamental zone of own symmetry. + + Fundamental zone in Rodrigues space is point symmetric around origin. + """ + if (len(rodrigues) != 3): + raise ValueError('Input is not a Rodriques-Frank vector.\n') + + if np.any(rodrigues == np.inf): return False + + Rabs = abs(rodrigues) + + if self.lattice == 'cubic': + return math.sqrt(2.0)-1.0 >= Rabs[0] \ + and math.sqrt(2.0)-1.0 >= Rabs[1] \ + and math.sqrt(2.0)-1.0 >= Rabs[2] \ + and 1.0 >= Rabs[0] + Rabs[1] + Rabs[2] + elif self.lattice == 'hexagonal': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] \ + and 2.0 >= math.sqrt(3)*Rabs[0] + Rabs[1] \ + and 2.0 >= math.sqrt(3)*Rabs[1] + Rabs[0] \ + and 2.0 >= math.sqrt(3) + Rabs[2] + elif self.lattice == 'tetragonal': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] \ + and math.sqrt(2.0) >= Rabs[0] + Rabs[1] \ + and math.sqrt(2.0) >= Rabs[2] + 1.0 + elif self.lattice == 'orthorhombic': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] + else: + return True + + + def inDisorientationSST(self,rodrigues): + """ + Check whether given Rodriques-Frank vector (of misorientation) falls into standard stereographic triangle of own symmetry. + + References + ---------- + A. Heinz and P. Neumann, Acta Crystallographica Section A 47:780-789, 1991 + https://doi.org/10.1107/S0108767391006864 + + """ + if (len(rodrigues) != 3): + raise ValueError('Input is not a Rodriques-Frank vector.\n') + R = rodrigues + + epsilon = 0.0 + if self.lattice == 'cubic': + return R[0] >= R[1]+epsilon and R[1] >= R[2]+epsilon and R[2] >= epsilon + elif self.lattice == 'hexagonal': + return R[0] >= math.sqrt(3)*(R[1]-epsilon) and R[1] >= epsilon and R[2] >= epsilon + elif self.lattice == 'tetragonal': + return R[0] >= R[1]-epsilon and R[1] >= epsilon and R[2] >= epsilon + elif self.lattice == 'orthorhombic': + return R[0] >= epsilon and R[1] >= epsilon and R[2] >= epsilon + else: + return True + + + def inSST(self, + vector, + proper = False, + color = False): + """ + Check whether given vector falls into standard stereographic triangle of own symmetry. + + proper considers only vectors with z >= 0, hence uses two neighboring SSTs. + Return inverse pole figure color if requested. + Bases are computed from + + basis = {'cubic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,1.]/np.sqrt(2.), # direction of green + [1.,1.,1.]/np.sqrt(3.)]).T), # direction of blue + 'hexagonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [np.sqrt(3.),1.,0.]/np.sqrt(4.)]).T), # direction of blue + 'tetragonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [1.,1.,0.]/np.sqrt(2.)]).T), # direction of blue + 'orthorhombic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [0.,1.,0.]]).T), # direction of blue + } + """ + if self.lattice == 'cubic': + basis = {'improper':np.array([ [-1. , 0. , 1. ], + [ np.sqrt(2.) , -np.sqrt(2.) , 0. ], + [ 0. , np.sqrt(3.) , 0. ] ]), + 'proper':np.array([ [ 0. , -1. , 1. ], + [-np.sqrt(2.) , np.sqrt(2.) , 0. ], + [ np.sqrt(3.) , 0. , 0. ] ]), + } + elif self.lattice == 'hexagonal': + basis = {'improper':np.array([ [ 0. , 0. , 1. ], + [ 1. , -np.sqrt(3.) , 0. ], + [ 0. , 2. , 0. ] ]), + 'proper':np.array([ [ 0. , 0. , 1. ], + [-1. , np.sqrt(3.) , 0. ], + [ np.sqrt(3.) , -1. , 0. ] ]), + } + elif self.lattice == 'tetragonal': + basis = {'improper':np.array([ [ 0. , 0. , 1. ], + [ 1. , -1. , 0. ], + [ 0. , np.sqrt(2.) , 0. ] ]), + 'proper':np.array([ [ 0. , 0. , 1. ], + [-1. , 1. , 0. ], + [ np.sqrt(2.) , 0. , 0. ] ]), + } + elif self.lattice == 'orthorhombic': + basis = {'improper':np.array([ [ 0., 0., 1.], + [ 1., 0., 0.], + [ 0., 1., 0.] ]), + 'proper':np.array([ [ 0., 0., 1.], + [-1., 0., 0.], + [ 0., 1., 0.] ]), + } + else: # direct exit for unspecified symmetry + if color: + return (True,np.zeros(3,'d')) + else: + return True + + v = np.array(vector,dtype=float) + if proper: # check both improper ... + theComponents = np.around(np.dot(basis['improper'],v),12) + inSST = np.all(theComponents >= 0.0) + if not inSST: # ... and proper SST + theComponents = np.around(np.dot(basis['proper'],v),12) + inSST = np.all(theComponents >= 0.0) + else: + v[2] = abs(v[2]) # z component projects identical + theComponents = np.around(np.dot(basis['improper'],v),12) # for positive and negative values + inSST = np.all(theComponents >= 0.0) + + if color: # have to return color array + if inSST: + rgb = np.power(theComponents/np.linalg.norm(theComponents),0.5) # smoothen color ramps + rgb = np.minimum(np.ones(3,dtype=float),rgb) # limit to maximum intensity + rgb /= max(rgb) # normalize to (HS)V = 1 + else: + rgb = np.zeros(3,dtype=float) + return (inSST,rgb) + else: + return inSST # code derived from https://github.com/ezag/pyeuclid # suggested reading: http://web.mit.edu/2.998/www/QuaternionReport1.pdf @@ -722,7 +720,7 @@ class Symmetry: class Lattice: """ Lattice system. - + Currently, this contains only a mapping from Bravais lattice to symmetry and orientation relationships. It could include twin and slip systems. @@ -744,7 +742,7 @@ class Lattice: def __init__(self, lattice): """ New lattice of given type. - + Parameters ---------- lattice : str @@ -753,16 +751,16 @@ class Lattice: """ self.lattice = lattice self.symmetry = Symmetry(self.lattices[lattice]['symmetry']) - - + + def __repr__(self): """Report basic lattice information.""" return 'Bravais lattice {} ({} symmetry)'.format(self.lattice,self.symmetry) - - + + # Kurdjomov--Sachs orientation relationship for fcc <-> bcc transformation - # from S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 - # also see K. Kitahara et al., Acta Materialia 54:1279-1288, 2006 + # from S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 + # also see K. Kitahara et al., Acta Materialia 54:1279-1288, 2006 KS = {'mapping':{'fcc':0,'bcc':1}, 'planes': np.array([ [[ 1, 1, 1],[ 0, 1, 1]], @@ -789,7 +787,7 @@ class Lattice: [[ 1, 1, -1],[ 0, 1, 1]], [[ 1, 1, -1],[ 0, 1, 1]], [[ 1, 1, -1],[ 0, 1, 1]]],dtype='float'), - 'directions': np.array([ + 'directions': np.array([ [[ -1, 0, 1],[ -1, -1, 1]], [[ -1, 0, 1],[ -1, 1, -1]], [[ 0, 1, -1],[ -1, -1, 1]], @@ -814,9 +812,9 @@ class Lattice: [[ 0, -1, -1],[ -1, 1, -1]], [[ 1, 0, 1],[ -1, -1, 1]], [[ 1, 0, 1],[ -1, 1, -1]]],dtype='float')} - + # Greninger--Troiano orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 GT = {'mapping':{'fcc':0,'bcc':1}, 'planes': np.array([ [[ 1, 1, 1],[ 1, 0, 1]], @@ -868,9 +866,9 @@ class Lattice: [[ 5, 17, 12],[ 17, 17, 7]], [[ 12, -5,-17],[ 7,-17,-17]], [[-17,-12, 5],[-17,-7, 17]]],dtype='float')} - + # Greninger--Troiano' orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 GTprime = {'mapping':{'fcc':0,'bcc':1}, 'planes': np.array([ [[ 7, 17, 17],[ 12, 5, 17]], @@ -922,7 +920,7 @@ class Lattice: [[ 0, 1, 1],[ 1, 1, 1]], [[ 1, 0, -1],[ 1, -1, -1]], [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} - + # Nishiyama--Wassermann orientation relationship for fcc <-> bcc transformation # from H. Kitahara et al., Materials Characterization 54:378-386, 2005 NW = {'mapping':{'fcc':0,'bcc':1}, @@ -952,9 +950,9 @@ class Lattice: [[ 2, -1, 1],[ 0, -1, 1]], #It is wrong in the paper, but matrix is correct [[ -1, 2, 1],[ 0, -1, 1]], [[ -1, -1, -2],[ 0, -1, 1]]],dtype='float')} - - # Pitsch orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Acta Materialia 53:1179-1190, 2005 + + # Pitsch orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Acta Materialia 53:1179-1190, 2005 Pitsch = {'mapping':{'fcc':0,'bcc':1}, 'planes': np.array([ [[ 0, 1, 0],[ -1, 0, 1]], @@ -982,8 +980,8 @@ class Lattice: [[ 0, 1, 1],[ -1, 1, 1]], [[ 1, 0, 1],[ 1, -1, 1]], [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} - - # Bain orientation relationship for fcc <-> bcc transformation + + # Bain orientation relationship for fcc <-> bcc transformation # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 Bain = {'mapping':{'fcc':0,'bcc':1}, 'planes': np.array([ @@ -994,30 +992,30 @@ class Lattice: [[ 0, 1, 0],[ 0, 1, 1]], [[ 0, 0, 1],[ 1, 0, 1]], [[ 1, 0, 0],[ 1, 1, 0]]],dtype='float')} - + def relationOperations(self,model): """ Crystallographic orientation relationships for phase transformations. - + References ---------- S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 https://doi.org/10.1016/j.jallcom.2012.02.004 - + K. Kitahara et al., Acta Materialia 54(5):1279-1288, 2006 https://doi.org/10.1016/j.actamat.2005.11.001 - + Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 https://doi.org/10.1107/S0021889805038276 - + H. Kitahara et al., Materials Characterization 54(4-5):378-386, 2005 https://doi.org/10.1016/j.matchar.2004.12.015 - - Y. He et al., Acta Materialia 53(4):1179-1190, 2005 + + Y. He et al., Acta Materialia 53(4):1179-1190, 2005 https://doi.org/10.1016/j.actamat.2004.11.021 """ - models={'KS':self.KS, 'GT':self.GT, 'GT_prime':self.GTprime, + models={'KS':self.KS, 'GT':self.GT, 'GT_prime':self.GTprime, 'NW':self.NW, 'Pitsch': self.Pitsch, 'Bain':self.Bain} try: relationship = models[model] @@ -1026,7 +1024,7 @@ class Lattice: if self.lattice not in relationship['mapping']: raise ValueError('Relationship "{}" not supported for lattice "{}"'.format(model,self.lattice)) - + r = {'lattice':Lattice((set(relationship['mapping'])-{self.lattice}).pop()), # target lattice 'rotations':[] } @@ -1053,12 +1051,12 @@ class Lattice: class Orientation: """ Crystallographic orientation. - + A crystallographic orientation contains a rotation and a lattice. """ __slots__ = ['rotation','lattice'] - + def __repr__(self): """Report lattice type and orientation.""" return self.lattice.__repr__()+'\n'+self.rotation.__repr__() @@ -1079,12 +1077,12 @@ class Orientation: self.lattice = lattice else: self.lattice = Lattice(lattice) # assume string - + if isinstance(rotation, Rotation): self.rotation = rotation else: self.rotation = Rotation.fromQuaternion(rotation) # assume quaternion - + def disorientation(self, other, SST = True, @@ -1101,7 +1099,7 @@ class Orientation: mySymEqs = self.equivalentOrientations() if SST else self.equivalentOrientations([0]) # take all or only first sym operation otherSymEqs = other.equivalentOrientations() - + for i,sA in enumerate(mySymEqs): aInv = sA.rotation.inversed() for j,sB in enumerate(otherSymEqs): @@ -1121,7 +1119,7 @@ class Orientation: def inFZ(self): return self.lattice.symmetry.inFZ(self.rotation.asRodrigues(vector=True)) - + def equivalentOrientations(self,members=[]): """List of orientations which are symmetrically equivalent.""" try: @@ -1131,21 +1129,21 @@ class Orientation: else: return [self.__class__(q*self.rotation,self.lattice) \ for q in self.lattice.symmetry.symmetryOperations(members)] # yes, return list of rotations - + def relatedOrientations(self,model): """List of orientations related by the given orientation relationship.""" r = self.lattice.relationOperations(model) return [self.__class__(o*self.rotation,r['lattice']) for o in r['rotations']] - + def reduced(self): """Transform orientation to fall into fundamental zone according to symmetry.""" for me in self.equivalentOrientations(): if self.lattice.symmetry.inFZ(me.rotation.asRodrigues(vector=True)): break return self.__class__(me.rotation,self.lattice) - - + + def inversePole(self, axis, proper = False, @@ -1159,8 +1157,8 @@ class Orientation: pole = self.rotation*axis # align crystal direction to axis return (pole,i if SST else 0) - - + + def IPFcolor(self,axis): """TSL color of inverse pole figure for given axis.""" color = np.zeros(3,'d') @@ -1170,7 +1168,7 @@ class Orientation: inSST,color = self.lattice.symmetry.inSST(pole,color=True) if inSST: break - return color + return color @staticmethod @@ -1203,36 +1201,36 @@ class Orientation: # Copyright (c) 2013-2014, Marc De Graef/Carnegie Mellon University # All rights reserved. # -# Redistribution and use in source and binary forms, with or without modification, are +# Redistribution and use in source and binary forms, with or without modification, are # permitted provided that the following conditions are met: # -# - Redistributions of source code must retain the above copyright notice, this list +# - Redistributions of source code must retain the above copyright notice, this list # of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or +# - Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or # other materials provided with the distribution. -# - Neither the names of Marc De Graef, Carnegie Mellon University nor the names -# of its contributors may be used to endorse or promote products derived from +# - Neither the names of Marc De Graef, Carnegie Mellon University nor the names +# of its contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #################################################################################################### def isone(a): return np.isclose(a,1.0,atol=1.0e-7,rtol=0.0) - + def iszero(a): return np.isclose(a,0.0,atol=1.0e-12,rtol=0.0) - + #---------- Quaternion ---------- def qu2om(qu): @@ -1247,22 +1245,22 @@ def qu2om(qu): om[0,2] = 2.0*(qu[1]*qu[3]+qu[0]*qu[2]) om[2,0] = 2.0*(qu[3]*qu[1]-qu[0]*qu[2]) return om if P > 0.0 else om.T - - + + def qu2eu(qu): - """Quaternion to Bunge-Euler angles.""" + """Quaternion to Bunge-Euler angles.""" q03 = qu[0]**2+qu[3]**2 q12 = qu[1]**2+qu[2]**2 chi = np.sqrt(q03*q12) - + if iszero(chi): eu = np.array([np.arctan2(-P*2.0*qu[0]*qu[3],qu[0]**2-qu[3]**2), 0.0, 0.0]) if iszero(q12) else \ np.array([np.arctan2(2.0*qu[1]*qu[2],qu[1]**2-qu[2]**2), np.pi, 0.0]) else: eu = np.array([np.arctan2((-P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]-qu[2]*qu[3])*chi ), - np.arctan2( 2.0*chi, q03-q12 ), + np.arctan2( 2.0*chi, q03-q12 ), np.arctan2(( P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]+qu[2]*qu[3])*chi )]) - + # reduce Euler angles to definition range, i.e a lower limit of 0.0 eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) return eu @@ -1271,7 +1269,7 @@ def qu2eu(qu): def qu2ax(qu): """ Quaternion to axis angle pair. - + Modified version of the original formulation, should be numerically more stable """ if iszero(qu[1]**2+qu[2]**2+qu[3]**2): # set axis to [001] if the angle is 0/360 @@ -1294,14 +1292,14 @@ def qu2ro(qu): s = np.linalg.norm([qu[1],qu[2],qu[3]]) ro = [0.0,0.0,P,0.0] if iszero(s) else \ [ qu[1]/s, qu[2]/s, qu[3]/s, np.tan(np.arccos(np.clip(qu[0],-1.0,1.0)))] # avoid numerical difficulties - + return np.array(ro) def qu2ho(qu): """Quaternion to homochoric vector.""" omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) # avoid numerical difficulties - + if iszero(omega): ho = np.array([ 0.0, 0.0, 0.0 ]) else: @@ -1322,7 +1320,7 @@ def qu2cu(qu): def om2qu(om): """ Rotation matrix to quaternion. - + The original formulation (direct conversion) had (numerical?) issues """ return eu2qu(om2eu(om)) @@ -1337,7 +1335,7 @@ def om2eu(om): np.arctan2(om[0,2]*zeta, om[1,2]*zeta)]) else: eu = np.array([np.arctan2( om[0,1],om[0,0]), np.pi*0.5*(1-om[2,2]),0.0]) # following the paper, not the reference implementation - + # reduce Euler angles to definition range, i.e a lower limit of 0.0 eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) return eu @@ -1350,7 +1348,7 @@ def om2ax(om): # first get the rotation angle t = 0.5*(om.trace() -1.0) ax[3] = np.arccos(np.clip(t,-1.0,1.0)) - + if iszero(ax[3]): ax = [ 0.0, 0.0, 1.0, 0.0] else: @@ -1360,14 +1358,14 @@ def om2ax(om): ax[0:3] = np.real(vr[0:3,i]) diagDelta = np.array([om[1,2]-om[2,1],om[2,0]-om[0,2],om[0,1]-om[1,0]]) ax[0:3] = np.where(iszero(diagDelta), ax[0:3],np.abs(ax[0:3])*np.sign(-P*diagDelta)) - + return np.array(ax) - + def om2ro(om): """Rotation matrix to Rodriques-Frank vector.""" return eu2ro(om2eu(om)) - + def om2ho(om): """Rotation matrix to homochoric vector.""" @@ -1377,8 +1375,8 @@ def om2ho(om): def om2cu(om): """Rotation matrix to cubochoric vector.""" return ho2cu(om2ho(om)) - - + + #---------- Bunge-Euler angles ---------- def eu2qu(eu): @@ -1408,21 +1406,21 @@ def eu2om(eu): def eu2ax(eu): - """Bunge-Euler angles to axis angle pair.""" + """Bunge-Euler angles to axis angle pair.""" t = np.tan(eu[1]*0.5) sigma = 0.5*(eu[0]+eu[2]) delta = 0.5*(eu[0]-eu[2]) tau = np.linalg.norm([t,np.sin(sigma)]) alpha = np.pi if iszero(np.cos(sigma)) else \ 2.0*np.arctan(tau/np.cos(sigma)) - + if iszero(alpha): ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) else: ax = -P/tau * np.array([ t*np.cos(delta), t*np.sin(delta), np.sin(sigma) ]) # passive axis angle pair so a minus sign in front - ax = np.append(ax,alpha) + ax = np.append(ax,alpha) if alpha < 0.0: ax *= -1.0 # ensure alpha is positive - + return ax @@ -1435,7 +1433,7 @@ def eu2ro(eu): ro = np.array([ 0.0, 0.0, P, 0.0 ]) else: ro[3] = np.tan(ro[3]*0.5) - + return ro @@ -1459,7 +1457,7 @@ def ax2qu(ax): c = np.cos(ax[3]*0.5) s = np.sin(ax[3]*0.5) qu = np.array([ c, ax[0]*s, ax[1]*s, ax[2]*s ]) - + return qu @@ -1477,14 +1475,14 @@ def ax2om(ax): return om if P < 0.0 else om.T - + def ax2eu(ax): """Rotation matrix to Bunge Euler angles.""" return om2eu(ax2om(ax)) def ax2ro(ax): - """Axis angle pair to Rodriques-Frank vector.""" + """Axis angle pair to Rodriques-Frank vector.""" if iszero(ax[3]): ro = [ 0.0, 0.0, P, 0.0 ] else: @@ -1497,7 +1495,7 @@ def ax2ro(ax): def ax2ho(ax): - """Axis angle pair to homochoric vector.""" + """Axis angle pair to homochoric vector.""" f = (0.75 * ( ax[3] - np.sin(ax[3]) ))**(1.0/3.0) ho = ax[0:3] * f return ho @@ -1523,12 +1521,12 @@ def ro2om(ro): def ro2eu(ro): """Rodriques-Frank vector to Bunge-Euler angles.""" return om2eu(ro2om(ro)) - - + + def ro2ax(ro): """Rodriques-Frank vector to axis angle pair.""" ta = ro[3] - + if iszero(ta): ax = [ 0.0, 0.0, 1.0, 0.0 ] elif not np.isfinite(ta): @@ -1542,7 +1540,7 @@ def ro2ax(ro): def ro2ho(ro): - """Rodriques-Frank vector to homochoric vector.""" + """Rodriques-Frank vector to homochoric vector.""" if iszero(np.sum(ro[0:3]**2.0)): ho = [ 0.0, 0.0, 0.0 ] else: @@ -1572,7 +1570,7 @@ def ho2om(ho): def ho2eu(ho): """Homochoric vector to Bunge-Euler angles.""" return ax2eu(ho2ax(ho)) - + def ho2ax(ho): """Homochoric vector to axis angle pair.""" @@ -1590,7 +1588,7 @@ def ho2ax(ho): ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) else: hm = hmag_squared - + # convert the magnitude to the rotation angle s = tfit[0] + tfit[1] * hmag_squared for i in range(2,16): From 898c696ef77405de0c34c68553cfbfa7d0bb4d68 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Thu, 20 Feb 2020 23:16:35 +0100 Subject: [PATCH 03/13] avoid long modules --- python/damask/__init__.py | 3 +- python/damask/orientation.py | 1495 +--------------------------------- python/damask/rotation.py | 1492 +++++++++++++++++++++++++++++++++ 3 files changed, 1496 insertions(+), 1494 deletions(-) create mode 100644 python/damask/rotation.py diff --git a/python/damask/__init__.py b/python/damask/__init__.py index 30fb37ced..b0f9b73df 100644 --- a/python/damask/__init__.py +++ b/python/damask/__init__.py @@ -13,7 +13,8 @@ from .asciitable import ASCIItable # noqa from .config import Material # noqa from .colormaps import Colormap, Color # noqa -from .orientation import Symmetry, Lattice, Rotation, Orientation # noqa +from .rotation import Symmetry, Lattice, Rotation # noqa +from .orientation import Orientation # noqa from .dadf5 import DADF5 # noqa from .geom import Geom # noqa diff --git a/python/damask/orientation.py b/python/damask/orientation.py index fad16c0e0..7f617e8df 100644 --- a/python/damask/orientation.py +++ b/python/damask/orientation.py @@ -1,1052 +1,7 @@ -import math - import numpy as np -from . import Lambert - -P = -1 - -#################################################################################################### -class Rotation: - u""" - Orientation stored with functionality for conversion to different representations. - - References - ---------- - D. Rowenhorst et al., Modelling and Simulation in Materials Science and Engineering 23:083501, 2015 - https://doi.org/10.1088/0965-0393/23/8/083501 - - Conventions - ----------- - Convention 1: Coordinate frames are right-handed. - Convention 2: A rotation angle ω is taken to be positive for a counterclockwise rotation - when viewing from the end point of the rotation axis towards the origin. - Convention 3: Rotations will be interpreted in the passive sense. - Convention 4: Euler angle triplets are implemented using the Bunge convention, - with the angular ranges as [0, 2π],[0, π],[0, 2π]. - Convention 5: The rotation angle ω is limited to the interval [0, π]. - Convention 6: the real part of a quaternion is positive, Re(q) > 0 - Convention 7: P = -1 (as default). - - Usage - ----- - Vector "a" (defined in coordinate system "A") is passively rotated - resulting in new coordinates "b" when expressed in system "B". - b = Q * a - b = np.dot(Q.asMatrix(),a) - - """ - - __slots__ = ['quaternion'] - - def __init__(self,quaternion = np.array([1.0,0.0,0.0,0.0])): - """ - Initializes to identity unless specified. - - Parameters - ---------- - quaternion : numpy.ndarray, optional - Unit quaternion that follows the conventions. Use .fromQuaternion to perform a sanity check. - - """ - self.quaternion = quaternion.copy() - - def __copy__(self): - """Copy.""" - return self.__class__(self.quaternion) - - copy = __copy__ - - - def __repr__(self): - """Orientation displayed as unit quaternion, rotation matrix, and Bunge-Euler angles.""" - return '\n'.join([ - 'Quaternion: (real={:.3f}, imag=<{:+.3f}, {:+.3f}, {:+.3f}>)'.format(*(self.quaternion)), - 'Matrix:\n{}'.format(self.asMatrix()), - 'Bunge Eulers / deg: ({:3.2f}, {:3.2f}, {:3.2f})'.format(*self.asEulers(degrees=True)), - ]) - - - def __mul__(self, other): - """ - Multiplication. - - Parameters - ---------- - other : numpy.ndarray or Rotation - Vector, second or fourth order tensor, or rotation object that is rotated. - - Todo - ---- - Document details active/passive) - considere rotation of (3,3,3,3)-matrix - - """ - if isinstance(other, Rotation): # rotate a rotation - self_q = self.quaternion[0] - self_p = self.quaternion[1:] - other_q = other.quaternion[0] - other_p = other.quaternion[1:] - R = self.__class__(np.append(self_q*other_q - np.dot(self_p,other_p), - self_q*other_p + other_q*self_p + P * np.cross(self_p,other_p))) - return R.standardize() - elif isinstance(other, (tuple,np.ndarray)): - if isinstance(other,tuple) or other.shape == (3,): # rotate a single (3)-vector or meshgrid - A = self.quaternion[0]**2.0 - np.dot(self.quaternion[1:],self.quaternion[1:]) - B = 2.0 * ( self.quaternion[1]*other[0] - + self.quaternion[2]*other[1] - + self.quaternion[3]*other[2]) - C = 2.0 * P*self.quaternion[0] - - return np.array([ - A*other[0] + B*self.quaternion[1] + C*(self.quaternion[2]*other[2] - self.quaternion[3]*other[1]), - A*other[1] + B*self.quaternion[2] + C*(self.quaternion[3]*other[0] - self.quaternion[1]*other[2]), - A*other[2] + B*self.quaternion[3] + C*(self.quaternion[1]*other[1] - self.quaternion[2]*other[0]), - ]) - elif other.shape == (3,3,): # rotate a single (3x3)-matrix - return np.dot(self.asMatrix(),np.dot(other,self.asMatrix().T)) - elif other.shape == (3,3,3,3,): - raise NotImplementedError - else: - return NotImplemented - else: - return NotImplemented - - - def inverse(self): - """In-place inverse rotation/backward rotation.""" - self.quaternion[1:] *= -1 - return self - - def inversed(self): - """Inverse rotation/backward rotation.""" - return self.copy().inverse() - - - def standardize(self): - """In-place quaternion representation with positive q.""" - if self.quaternion[0] < 0.0: self.quaternion*=-1 - return self - - def standardized(self): - """Quaternion representation with positive q.""" - return self.copy().standardize() - - - def misorientation(self,other): - """ - Get Misorientation. - - Parameters - ---------- - other : Rotation - Rotation to which the misorientation is computed. - - """ - return other*self.inversed() - - - def average(self,other): - """ - Calculate the average rotation. - - Parameters - ---------- - other : Rotation - Rotation from which the average is rotated. - - """ - return Rotation.fromAverage([self,other]) - - - ################################################################################################ - # convert to different orientation representations (numpy arrays) - - def asQuaternion(self): - """ - Unit quaternion [q, p_1, p_2, p_3] unless quaternion == True: damask.quaternion object. - - Parameters - ---------- - quaternion : bool, optional - return quaternion as DAMASK object. - - """ - return self.quaternion - - def asEulers(self, - degrees = False): - """ - Bunge-Euler angles: (φ_1, ϕ, φ_2). - - Parameters - ---------- - degrees : bool, optional - return angles in degrees. - - """ - eu = qu2eu(self.quaternion) - if degrees: eu = np.degrees(eu) - return eu - - def asAxisAngle(self, - degrees = False, - pair = False): - """ - Axis angle representation [n_1, n_2, n_3, ω] unless pair == True: ([n_1, n_2, n_3], ω). - - Parameters - ---------- - degrees : bool, optional - return rotation angle in degrees. - pair : bool, optional - return tuple of axis and angle. - - """ - ax = qu2ax(self.quaternion) - if degrees: ax[3] = np.degrees(ax[3]) - return (ax[:3],np.degrees(ax[3])) if pair else ax - - def asMatrix(self): - """Rotation matrix.""" - return qu2om(self.quaternion) - - def asRodrigues(self, - vector = False): - """ - Rodrigues-Frank vector representation [n_1, n_2, n_3, tan(ω/2)] unless vector == True: - [n_1, n_2, n_3] * tan(ω/2). - - Parameters - ---------- - vector : bool, optional - return as actual Rodrigues--Frank vector, i.e. rotation axis scaled by tan(ω/2). - - """ - ro = qu2ro(self.quaternion) - return ro[:3]*ro[3] if vector else ro - - def asHomochoric(self): - """Homochoric vector: (h_1, h_2, h_3).""" - return qu2ho(self.quaternion) - - def asCubochoric(self): - """Cubochoric vector: (c_1, c_2, c_3).""" - return qu2cu(self.quaternion) - - def asM(self): - """ - Intermediate representation supporting quaternion averaging. - - References - ---------- - F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 - https://doi.org/10.2514/1.28949 - - """ - return np.outer(self.quaternion,self.quaternion) - - - ################################################################################################ - # static constructors. The input data needs to follow the convention, options allow to - # relax these convections - @staticmethod - def fromQuaternion(quaternion, - acceptHomomorph = False, - P = -1): - - qu = quaternion if isinstance(quaternion,np.ndarray) and quaternion.dtype == np.dtype(float) \ - else np.array(quaternion,dtype=float) - if P > 0: qu[1:4] *= -1 # convert from P=1 to P=-1 - if qu[0] < 0.0: - if acceptHomomorph: - qu *= -1. - else: - raise ValueError('Quaternion has negative first component.\n{}'.format(qu[0])) - if not np.isclose(np.linalg.norm(qu), 1.0): - raise ValueError('Quaternion is not of unit length.\n{} {} {} {}'.format(*qu)) - - return Rotation(qu) - - @staticmethod - def fromEulers(eulers, - degrees = False): - - eu = eulers if isinstance(eulers, np.ndarray) and eulers.dtype == np.dtype(float) \ - else np.array(eulers,dtype=float) - eu = np.radians(eu) if degrees else eu - if np.any(eu < 0.0) or np.any(eu > 2.0*np.pi) or eu[1] > np.pi: - raise ValueError('Euler angles outside of [0..2π],[0..π],[0..2π].\n{} {} {}.'.format(*eu)) - - return Rotation(eu2qu(eu)) - - @staticmethod - def fromAxisAngle(angleAxis, - degrees = False, - normalise = False, - P = -1): - - ax = angleAxis if isinstance(angleAxis, np.ndarray) and angleAxis.dtype == np.dtype(float) \ - else np.array(angleAxis,dtype=float) - if P > 0: ax[0:3] *= -1 # convert from P=1 to P=-1 - if degrees: ax[ 3] = np.radians(ax[3]) - if normalise: ax[0:3] /= np.linalg.norm(ax[0:3]) - if ax[3] < 0.0 or ax[3] > np.pi: - raise ValueError('Axis angle rotation angle outside of [0..π].\n'.format(ax[3])) - if not np.isclose(np.linalg.norm(ax[0:3]), 1.0): - raise ValueError('Axis angle rotation axis is not of unit length.\n{} {} {}'.format(*ax[0:3])) - - return Rotation(ax2qu(ax)) - - @staticmethod - def fromBasis(basis, - orthonormal = True, - reciprocal = False, - ): - - om = basis if isinstance(basis, np.ndarray) else np.array(basis).reshape((3,3)) - if reciprocal: - om = np.linalg.inv(om.T/np.pi) # transform reciprocal basis set - orthonormal = False # contains stretch - if not orthonormal: - (U,S,Vh) = np.linalg.svd(om) # singular value decomposition - om = np.dot(U,Vh) - if not np.isclose(np.linalg.det(om),1.0): - raise ValueError('matrix is not a proper rotation.\n{}'.format(om)) - if not np.isclose(np.dot(om[0],om[1]), 0.0) \ - or not np.isclose(np.dot(om[1],om[2]), 0.0) \ - or not np.isclose(np.dot(om[2],om[0]), 0.0): - raise ValueError('matrix is not orthogonal.\n{}'.format(om)) - - return Rotation(om2qu(om)) - - @staticmethod - def fromMatrix(om, - ): - - return Rotation.fromBasis(om) - - @staticmethod - def fromRodrigues(rodrigues, - normalise = False, - P = -1): - - ro = rodrigues if isinstance(rodrigues, np.ndarray) and rodrigues.dtype == np.dtype(float) \ - else np.array(rodrigues,dtype=float) - if P > 0: ro[0:3] *= -1 # convert from P=1 to P=-1 - if normalise: ro[0:3] /= np.linalg.norm(ro[0:3]) - if not np.isclose(np.linalg.norm(ro[0:3]), 1.0): - raise ValueError('Rodrigues rotation axis is not of unit length.\n{} {} {}'.format(*ro[0:3])) - if ro[3] < 0.0: - raise ValueError('Rodriques rotation angle not positive.\n'.format(ro[3])) - - return Rotation(ro2qu(ro)) - - @staticmethod - def fromHomochoric(homochoric, - P = -1): - - ho = homochoric if isinstance(homochoric, np.ndarray) and homochoric.dtype == np.dtype(float) \ - else np.array(homochoric,dtype=float) - if P > 0: ho *= -1 # convert from P=1 to P=-1 - - return Rotation(ho2qu(ho)) - - @staticmethod - def fromCubochoric(cubochoric, - P = -1): - - cu = cubochoric if isinstance(cubochoric, np.ndarray) and cubochoric.dtype == np.dtype(float) \ - else np.array(cubochoric,dtype=float) - ho = cu2ho(cu) - if P > 0: ho *= -1 # convert from P=1 to P=-1 - - return Rotation(ho2qu(ho)) - - - @staticmethod - def fromAverage(rotations, - weights = []): - """ - Average rotation. - - References - ---------- - F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 - https://doi.org/10.2514/1.28949 - - Parameters - ---------- - rotations : list of Rotations - Rotations to average from - weights : list of floats, optional - Weights for each rotation used for averaging - - """ - if not all(isinstance(item, Rotation) for item in rotations): - raise TypeError("Only instances of Rotation can be averaged.") - - N = len(rotations) - if weights == [] or not weights: - weights = np.ones(N,dtype='i') - - for i,(r,n) in enumerate(zip(rotations,weights)): - M = r.asM() * n if i == 0 \ - else M + r.asM() * n # noqa add (multiples) of this rotation to average noqa - eig, vec = np.linalg.eig(M/N) - - return Rotation.fromQuaternion(np.real(vec.T[eig.argmax()]),acceptHomomorph = True) - - - @staticmethod - def fromRandom(): - r = np.random.random(3) - A = np.sqrt(r[2]) - B = np.sqrt(1.0-r[2]) - return Rotation(np.array([np.cos(2.0*np.pi*r[0])*A, - np.sin(2.0*np.pi*r[1])*B, - np.cos(2.0*np.pi*r[1])*B, - np.sin(2.0*np.pi*r[0])*A])).standardize() - - - -# ****************************************************************************************** -class Symmetry: - """ - Symmetry operations for lattice systems. - - References - ---------- - https://en.wikipedia.org/wiki/Crystal_system - - """ - - lattices = [None,'orthorhombic','tetragonal','hexagonal','cubic',] - - def __init__(self, symmetry = None): - """ - Symmetry Definition. - - Parameters - ---------- - symmetry : str, optional - label of the crystal system - - """ - if symmetry is not None and symmetry.lower() not in Symmetry.lattices: - raise KeyError('Symmetry/crystal system "{}" is unknown'.format(symmetry)) - - self.lattice = symmetry.lower() if isinstance(symmetry,str) else symmetry - - - def __copy__(self): - """Copy.""" - return self.__class__(self.lattice) - - copy = __copy__ - - - def __repr__(self): - """Readable string.""" - return '{}'.format(self.lattice) - - - def __eq__(self, other): - """ - Equal to other. - - Parameters - ---------- - other : Symmetry - Symmetry to check for equality. - - """ - return self.lattice == other.lattice - - def __neq__(self, other): - """ - Not Equal to other. - - Parameters - ---------- - other : Symmetry - Symmetry to check for inequality. - - """ - return not self.__eq__(other) - - def __cmp__(self,other): - """ - Linear ordering. - - Parameters - ---------- - other : Symmetry - Symmetry to check for for order. - - """ - myOrder = Symmetry.lattices.index(self.lattice) - otherOrder = Symmetry.lattices.index(other.lattice) - return (myOrder > otherOrder) - (myOrder < otherOrder) - - def symmetryOperations(self,members=[]): - """List (or single element) of symmetry operations as rotations.""" - if self.lattice == 'cubic': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, 0.0, 0.0, 1.0 ], - [ 0.0, 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2) ], - [ 0.0, 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], - [ 0.0, -0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], - [ 0.5, 0.5, 0.5, 0.5 ], - [-0.5, 0.5, 0.5, 0.5 ], - [-0.5, 0.5, 0.5, -0.5 ], - [-0.5, 0.5, -0.5, 0.5 ], - [-0.5, -0.5, 0.5, 0.5 ], - [-0.5, -0.5, 0.5, -0.5 ], - [-0.5, -0.5, -0.5, 0.5 ], - [-0.5, 0.5, -0.5, -0.5 ], - [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [-0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2), 0.0 ], - [-0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2), 0.0 ], - [-0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0, 0.0 ], - [-0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0, 0.0 ], - ] - elif self.lattice == 'hexagonal': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [-0.5*np.sqrt(3), 0.0, 0.0, -0.5 ], - [ 0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], - [ 0.0, 0.0, 0.0, 1.0 ], - [-0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], - [-0.5*np.sqrt(3), 0.0, 0.0, 0.5 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, -0.5*np.sqrt(3), 0.5, 0.0 ], - [ 0.0, 0.5, -0.5*np.sqrt(3), 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, -0.5, -0.5*np.sqrt(3), 0.0 ], - [ 0.0, 0.5*np.sqrt(3), 0.5, 0.0 ], - ] - elif self.lattice == 'tetragonal': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, 0.0, 0.0, 1.0 ], - [ 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], - [ 0.0, -0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], - [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - ] - elif self.lattice == 'orthorhombic': - symQuats = [ - [ 1.0,0.0,0.0,0.0 ], - [ 0.0,1.0,0.0,0.0 ], - [ 0.0,0.0,1.0,0.0 ], - [ 0.0,0.0,0.0,1.0 ], - ] - else: - symQuats = [ - [ 1.0,0.0,0.0,0.0 ], - ] - - symOps = list(map(Rotation, - np.array(symQuats)[np.atleast_1d(members) if members != [] else range(len(symQuats))])) - try: - iter(members) # asking for (even empty) list of members? - except TypeError: - return symOps[0] # no, return rotation object - else: - return symOps # yes, return list of rotations - - - def inFZ(self,rodrigues): - """ - Check whether given Rodriques-Frank vector falls into fundamental zone of own symmetry. - - Fundamental zone in Rodrigues space is point symmetric around origin. - """ - if (len(rodrigues) != 3): - raise ValueError('Input is not a Rodriques-Frank vector.\n') - - if np.any(rodrigues == np.inf): return False - - Rabs = abs(rodrigues) - - if self.lattice == 'cubic': - return math.sqrt(2.0)-1.0 >= Rabs[0] \ - and math.sqrt(2.0)-1.0 >= Rabs[1] \ - and math.sqrt(2.0)-1.0 >= Rabs[2] \ - and 1.0 >= Rabs[0] + Rabs[1] + Rabs[2] - elif self.lattice == 'hexagonal': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] \ - and 2.0 >= math.sqrt(3)*Rabs[0] + Rabs[1] \ - and 2.0 >= math.sqrt(3)*Rabs[1] + Rabs[0] \ - and 2.0 >= math.sqrt(3) + Rabs[2] - elif self.lattice == 'tetragonal': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] \ - and math.sqrt(2.0) >= Rabs[0] + Rabs[1] \ - and math.sqrt(2.0) >= Rabs[2] + 1.0 - elif self.lattice == 'orthorhombic': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] - else: - return True - - - def inDisorientationSST(self,rodrigues): - """ - Check whether given Rodriques-Frank vector (of misorientation) falls into standard stereographic triangle of own symmetry. - - References - ---------- - A. Heinz and P. Neumann, Acta Crystallographica Section A 47:780-789, 1991 - https://doi.org/10.1107/S0108767391006864 - - """ - if (len(rodrigues) != 3): - raise ValueError('Input is not a Rodriques-Frank vector.\n') - R = rodrigues - - epsilon = 0.0 - if self.lattice == 'cubic': - return R[0] >= R[1]+epsilon and R[1] >= R[2]+epsilon and R[2] >= epsilon - elif self.lattice == 'hexagonal': - return R[0] >= math.sqrt(3)*(R[1]-epsilon) and R[1] >= epsilon and R[2] >= epsilon - elif self.lattice == 'tetragonal': - return R[0] >= R[1]-epsilon and R[1] >= epsilon and R[2] >= epsilon - elif self.lattice == 'orthorhombic': - return R[0] >= epsilon and R[1] >= epsilon and R[2] >= epsilon - else: - return True - - - def inSST(self, - vector, - proper = False, - color = False): - """ - Check whether given vector falls into standard stereographic triangle of own symmetry. - - proper considers only vectors with z >= 0, hence uses two neighboring SSTs. - Return inverse pole figure color if requested. - Bases are computed from - - basis = {'cubic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,1.]/np.sqrt(2.), # direction of green - [1.,1.,1.]/np.sqrt(3.)]).T), # direction of blue - 'hexagonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [np.sqrt(3.),1.,0.]/np.sqrt(4.)]).T), # direction of blue - 'tetragonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [1.,1.,0.]/np.sqrt(2.)]).T), # direction of blue - 'orthorhombic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [0.,1.,0.]]).T), # direction of blue - } - """ - if self.lattice == 'cubic': - basis = {'improper':np.array([ [-1. , 0. , 1. ], - [ np.sqrt(2.) , -np.sqrt(2.) , 0. ], - [ 0. , np.sqrt(3.) , 0. ] ]), - 'proper':np.array([ [ 0. , -1. , 1. ], - [-np.sqrt(2.) , np.sqrt(2.) , 0. ], - [ np.sqrt(3.) , 0. , 0. ] ]), - } - elif self.lattice == 'hexagonal': - basis = {'improper':np.array([ [ 0. , 0. , 1. ], - [ 1. , -np.sqrt(3.) , 0. ], - [ 0. , 2. , 0. ] ]), - 'proper':np.array([ [ 0. , 0. , 1. ], - [-1. , np.sqrt(3.) , 0. ], - [ np.sqrt(3.) , -1. , 0. ] ]), - } - elif self.lattice == 'tetragonal': - basis = {'improper':np.array([ [ 0. , 0. , 1. ], - [ 1. , -1. , 0. ], - [ 0. , np.sqrt(2.) , 0. ] ]), - 'proper':np.array([ [ 0. , 0. , 1. ], - [-1. , 1. , 0. ], - [ np.sqrt(2.) , 0. , 0. ] ]), - } - elif self.lattice == 'orthorhombic': - basis = {'improper':np.array([ [ 0., 0., 1.], - [ 1., 0., 0.], - [ 0., 1., 0.] ]), - 'proper':np.array([ [ 0., 0., 1.], - [-1., 0., 0.], - [ 0., 1., 0.] ]), - } - else: # direct exit for unspecified symmetry - if color: - return (True,np.zeros(3,'d')) - else: - return True - - v = np.array(vector,dtype=float) - if proper: # check both improper ... - theComponents = np.around(np.dot(basis['improper'],v),12) - inSST = np.all(theComponents >= 0.0) - if not inSST: # ... and proper SST - theComponents = np.around(np.dot(basis['proper'],v),12) - inSST = np.all(theComponents >= 0.0) - else: - v[2] = abs(v[2]) # z component projects identical - theComponents = np.around(np.dot(basis['improper'],v),12) # for positive and negative values - inSST = np.all(theComponents >= 0.0) - - if color: # have to return color array - if inSST: - rgb = np.power(theComponents/np.linalg.norm(theComponents),0.5) # smoothen color ramps - rgb = np.minimum(np.ones(3,dtype=float),rgb) # limit to maximum intensity - rgb /= max(rgb) # normalize to (HS)V = 1 - else: - rgb = np.zeros(3,dtype=float) - return (inSST,rgb) - else: - return inSST - -# code derived from https://github.com/ezag/pyeuclid -# suggested reading: http://web.mit.edu/2.998/www/QuaternionReport1.pdf - - -# ****************************************************************************************** -class Lattice: - """ - Lattice system. - - Currently, this contains only a mapping from Bravais lattice to symmetry - and orientation relationships. It could include twin and slip systems. - - References - ---------- - https://en.wikipedia.org/wiki/Bravais_lattice - - """ - - lattices = { - 'triclinic':{'symmetry':None}, - 'bct':{'symmetry':'tetragonal'}, - 'hex':{'symmetry':'hexagonal'}, - 'fcc':{'symmetry':'cubic','c/a':1.0}, - 'bcc':{'symmetry':'cubic','c/a':1.0}, - } - - - def __init__(self, lattice): - """ - New lattice of given type. - - Parameters - ---------- - lattice : str - Bravais lattice. - - """ - self.lattice = lattice - self.symmetry = Symmetry(self.lattices[lattice]['symmetry']) - - - def __repr__(self): - """Report basic lattice information.""" - return 'Bravais lattice {} ({} symmetry)'.format(self.lattice,self.symmetry) - - - # Kurdjomov--Sachs orientation relationship for fcc <-> bcc transformation - # from S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 - # also see K. Kitahara et al., Acta Materialia 54:1279-1288, 2006 - KS = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]]],dtype='float'), - 'directions': np.array([ - [[ -1, 0, 1],[ -1, -1, 1]], - [[ -1, 0, 1],[ -1, 1, -1]], - [[ 0, 1, -1],[ -1, -1, 1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ 1, -1, 0],[ -1, -1, 1]], - [[ 1, -1, 0],[ -1, 1, -1]], - [[ 1, 0, -1],[ -1, -1, 1]], - [[ 1, 0, -1],[ -1, 1, -1]], - [[ -1, -1, 0],[ -1, -1, 1]], - [[ -1, -1, 0],[ -1, 1, -1]], - [[ 0, 1, 1],[ -1, -1, 1]], - [[ 0, 1, 1],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ 0, -1, 1],[ -1, 1, -1]], - [[ -1, 0, -1],[ -1, -1, 1]], - [[ -1, 0, -1],[ -1, 1, -1]], - [[ 1, 1, 0],[ -1, -1, 1]], - [[ 1, 1, 0],[ -1, 1, -1]], - [[ -1, 1, 0],[ -1, -1, 1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, -1],[ -1, -1, 1]], - [[ 0, -1, -1],[ -1, 1, -1]], - [[ 1, 0, 1],[ -1, -1, 1]], - [[ 1, 0, 1],[ -1, 1, -1]]],dtype='float')} - - # Greninger--Troiano orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - GT = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 1, 0, 1]], - [[ 1, 1, 1],[ 1, 1, 0]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ -1, 0, 1]], - [[ -1, -1, 1],[ -1, -1, 0]], - [[ -1, -1, 1],[ 0, -1, 1]], - [[ -1, 1, 1],[ -1, 0, 1]], - [[ -1, 1, 1],[ -1, 1, 0]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 1, 0, 1]], - [[ 1, -1, 1],[ 1, -1, 0]], - [[ 1, -1, 1],[ 0, -1, 1]], - [[ 1, 1, 1],[ 1, 1, 0]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 1, 0, 1]], - [[ -1, -1, 1],[ -1, -1, 0]], - [[ -1, -1, 1],[ 0, -1, 1]], - [[ -1, -1, 1],[ -1, 0, 1]], - [[ -1, 1, 1],[ -1, 1, 0]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ -1, 0, 1]], - [[ 1, -1, 1],[ 1, -1, 0]], - [[ 1, -1, 1],[ 0, -1, 1]], - [[ 1, -1, 1],[ 1, 0, 1]]],dtype='float'), - 'directions': np.array([ - [[ -5,-12, 17],[-17, -7, 17]], - [[ 17, -5,-12],[ 17,-17, -7]], - [[-12, 17, -5],[ -7, 17,-17]], - [[ 5, 12, 17],[ 17, 7, 17]], - [[-17, 5,-12],[-17, 17, -7]], - [[ 12,-17, -5],[ 7,-17,-17]], - [[ -5, 12,-17],[-17, 7,-17]], - [[ 17, 5, 12],[ 17, 17, 7]], - [[-12,-17, 5],[ -7,-17, 17]], - [[ 5,-12,-17],[ 17, -7,-17]], - [[-17, -5, 12],[-17,-17, 7]], - [[ 12, 17, 5],[ 7, 17, 17]], - [[ -5, 17,-12],[-17, 17, -7]], - [[-12, -5, 17],[ -7,-17, 17]], - [[ 17,-12, -5],[ 17, -7,-17]], - [[ 5,-17,-12],[ 17,-17, -7]], - [[ 12, 5, 17],[ 7, 17, 17]], - [[-17, 12, -5],[-17, 7,-17]], - [[ -5,-17, 12],[-17,-17, 7]], - [[-12, 5,-17],[ -7, 17,-17]], - [[ 17, 12, 5],[ 17, 7, 17]], - [[ 5, 17, 12],[ 17, 17, 7]], - [[ 12, -5,-17],[ 7,-17,-17]], - [[-17,-12, 5],[-17,-7, 17]]],dtype='float')} - - # Greninger--Troiano' orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - GTprime = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 7, 17, 17],[ 12, 5, 17]], - [[ 17, 7, 17],[ 17, 12, 5]], - [[ 17, 17, 7],[ 5, 17, 12]], - [[ -7,-17, 17],[-12, -5, 17]], - [[-17, -7, 17],[-17,-12, 5]], - [[-17,-17, 7],[ -5,-17, 12]], - [[ 7,-17,-17],[ 12, -5,-17]], - [[ 17, -7,-17],[ 17,-12, -5]], - [[ 17,-17, -7],[ 5,-17,-12]], - [[ -7, 17,-17],[-12, 5,-17]], - [[-17, 7,-17],[-17, 12, -5]], - [[-17, 17, -7],[ -5, 17,-12]], - [[ 7, 17, 17],[ 12, 17, 5]], - [[ 17, 7, 17],[ 5, 12, 17]], - [[ 17, 17, 7],[ 17, 5, 12]], - [[ -7,-17, 17],[-12,-17, 5]], - [[-17, -7, 17],[ -5,-12, 17]], - [[-17,-17, 7],[-17, -5, 12]], - [[ 7,-17,-17],[ 12,-17, -5]], - [[ 17, -7,-17],[ 5, -12,-17]], - [[ 17,-17, -7],[ 17, -5,-12]], - [[ -7, 17,-17],[-12, 17, -5]], - [[-17, 7,-17],[ -5, 12,-17]], - [[-17, 17, -7],[-17, 5,-12]]],dtype='float'), - 'directions': np.array([ - [[ 0, 1, -1],[ 1, 1, -1]], - [[ -1, 0, 1],[ -1, 1, 1]], - [[ 1, -1, 0],[ 1, -1, 1]], - [[ 0, -1, -1],[ -1, -1, -1]], - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, -1, 0],[ 1, -1, -1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ 1, 0, 1],[ 1, 1, 1]], - [[ -1, -1, 0],[ -1, -1, 1]], - [[ 0, -1, -1],[ 1, -1, -1]], - [[ -1, 0, 1],[ -1, -1, 1]], - [[ -1, -1, 0],[ -1, -1, -1]], - [[ 0, -1, 1],[ 1, -1, 1]], - [[ 1, 0, -1],[ 1, 1, -1]], - [[ -1, 1, 0],[ -1, 1, 1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ -1, 0, -1],[ -1, -1, -1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ -1, 0, -1],[ -1, 1, -1]], - [[ 1, 1, 0],[ 1, 1, 1]], - [[ 0, 1, 1],[ 1, 1, 1]], - [[ 1, 0, -1],[ 1, -1, -1]], - [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} - - # Nishiyama--Wassermann orientation relationship for fcc <-> bcc transformation - # from H. Kitahara et al., Materials Characterization 54:378-386, 2005 - NW = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]]],dtype='float'), - 'directions': np.array([ - [[ 2, -1, -1],[ 0, -1, 1]], - [[ -1, 2, -1],[ 0, -1, 1]], - [[ -1, -1, 2],[ 0, -1, 1]], - [[ -2, -1, -1],[ 0, -1, 1]], - [[ 1, 2, -1],[ 0, -1, 1]], - [[ 1, -1, 2],[ 0, -1, 1]], - [[ 2, 1, -1],[ 0, -1, 1]], - [[ -1, -2, -1],[ 0, -1, 1]], - [[ -1, 1, 2],[ 0, -1, 1]], - [[ 2, -1, 1],[ 0, -1, 1]], #It is wrong in the paper, but matrix is correct - [[ -1, 2, 1],[ 0, -1, 1]], - [[ -1, -1, -2],[ 0, -1, 1]]],dtype='float')} - - # Pitsch orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Acta Materialia 53:1179-1190, 2005 - Pitsch = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 0, 1, 0],[ -1, 0, 1]], - [[ 0, 0, 1],[ 1, -1, 0]], - [[ 1, 0, 0],[ 0, 1, -1]], - [[ 1, 0, 0],[ 0, -1, -1]], - [[ 0, 1, 0],[ -1, 0, -1]], - [[ 0, 0, 1],[ -1, -1, 0]], - [[ 0, 1, 0],[ -1, 0, -1]], - [[ 0, 0, 1],[ -1, -1, 0]], - [[ 1, 0, 0],[ 0, -1, -1]], - [[ 1, 0, 0],[ 0, -1, 1]], - [[ 0, 1, 0],[ 1, 0, -1]], - [[ 0, 0, 1],[ -1, 1, 0]]],dtype='float'), - 'directions': np.array([ - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, 1, 0],[ 1, 1, -1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ -1, 0, 1],[ -1, -1, 1]], - [[ 1, -1, 0],[ 1, -1, -1]], - [[ 1, 0, -1],[ 1, -1, -1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} - - # Bain orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - Bain = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 0, 0],[ 1, 0, 0]], - [[ 0, 1, 0],[ 0, 1, 0]], - [[ 0, 0, 1],[ 0, 0, 1]]],dtype='float'), - 'directions': np.array([ - [[ 0, 1, 0],[ 0, 1, 1]], - [[ 0, 0, 1],[ 1, 0, 1]], - [[ 1, 0, 0],[ 1, 1, 0]]],dtype='float')} - - def relationOperations(self,model): - """ - Crystallographic orientation relationships for phase transformations. - - References - ---------- - S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 - https://doi.org/10.1016/j.jallcom.2012.02.004 - - K. Kitahara et al., Acta Materialia 54(5):1279-1288, 2006 - https://doi.org/10.1016/j.actamat.2005.11.001 - - Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - https://doi.org/10.1107/S0021889805038276 - - H. Kitahara et al., Materials Characterization 54(4-5):378-386, 2005 - https://doi.org/10.1016/j.matchar.2004.12.015 - - Y. He et al., Acta Materialia 53(4):1179-1190, 2005 - https://doi.org/10.1016/j.actamat.2004.11.021 - - """ - models={'KS':self.KS, 'GT':self.GT, 'GT_prime':self.GTprime, - 'NW':self.NW, 'Pitsch': self.Pitsch, 'Bain':self.Bain} - try: - relationship = models[model] - except KeyError : - raise KeyError('Orientation relationship "{}" is unknown'.format(model)) - - if self.lattice not in relationship['mapping']: - raise ValueError('Relationship "{}" not supported for lattice "{}"'.format(model,self.lattice)) - - r = {'lattice':Lattice((set(relationship['mapping'])-{self.lattice}).pop()), # target lattice - 'rotations':[] } - - myPlane_id = relationship['mapping'][self.lattice] - otherPlane_id = (myPlane_id+1)%2 - myDir_id = myPlane_id +2 - otherDir_id = otherPlane_id +2 - - for miller in np.hstack((relationship['planes'],relationship['directions'])): - myPlane = miller[myPlane_id]/ np.linalg.norm(miller[myPlane_id]) - myDir = miller[myDir_id]/ np.linalg.norm(miller[myDir_id]) - myMatrix = np.array([myDir,np.cross(myPlane,myDir),myPlane]) - - otherPlane = miller[otherPlane_id]/ np.linalg.norm(miller[otherPlane_id]) - otherDir = miller[otherDir_id]/ np.linalg.norm(miller[otherDir_id]) - otherMatrix = np.array([otherDir,np.cross(otherPlane,otherDir),otherPlane]) - - r['rotations'].append(Rotation.fromMatrix(np.dot(otherMatrix.T,myMatrix))) - - return r - - +from .rotation import Lattice +from .rotation import Rotation class Orientation: """ @@ -1192,449 +147,3 @@ class Orientation: def average(self,other): """Calculate the average rotation.""" return Orientation.fromAverage([self,other]) - - -#################################################################################################### -# Code below available according to the following conditions on https://github.com/MarDiehl/3Drotations -#################################################################################################### -# Copyright (c) 2017-2019, Martin Diehl/Max-Planck-Institut für Eisenforschung GmbH -# Copyright (c) 2013-2014, Marc De Graef/Carnegie Mellon University -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, are -# permitted provided that the following conditions are met: -# -# - Redistributions of source code must retain the above copyright notice, this list -# of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# - Neither the names of Marc De Graef, Carnegie Mellon University nor the names -# of its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#################################################################################################### - -def isone(a): - return np.isclose(a,1.0,atol=1.0e-7,rtol=0.0) - -def iszero(a): - return np.isclose(a,0.0,atol=1.0e-12,rtol=0.0) - -#---------- Quaternion ---------- - -def qu2om(qu): - """Quaternion to rotation matrix.""" - qq = qu[0]**2-(qu[1]**2 + qu[2]**2 + qu[3]**2) - om = np.diag(qq + 2.0*np.array([qu[1],qu[2],qu[3]])**2) - - om[1,0] = 2.0*(qu[2]*qu[1]+qu[0]*qu[3]) - om[0,1] = 2.0*(qu[1]*qu[2]-qu[0]*qu[3]) - om[2,1] = 2.0*(qu[3]*qu[2]+qu[0]*qu[1]) - om[1,2] = 2.0*(qu[2]*qu[3]-qu[0]*qu[1]) - om[0,2] = 2.0*(qu[1]*qu[3]+qu[0]*qu[2]) - om[2,0] = 2.0*(qu[3]*qu[1]-qu[0]*qu[2]) - return om if P > 0.0 else om.T - - -def qu2eu(qu): - """Quaternion to Bunge-Euler angles.""" - q03 = qu[0]**2+qu[3]**2 - q12 = qu[1]**2+qu[2]**2 - chi = np.sqrt(q03*q12) - - if iszero(chi): - eu = np.array([np.arctan2(-P*2.0*qu[0]*qu[3],qu[0]**2-qu[3]**2), 0.0, 0.0]) if iszero(q12) else \ - np.array([np.arctan2(2.0*qu[1]*qu[2],qu[1]**2-qu[2]**2), np.pi, 0.0]) - else: - eu = np.array([np.arctan2((-P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]-qu[2]*qu[3])*chi ), - np.arctan2( 2.0*chi, q03-q12 ), - np.arctan2(( P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]+qu[2]*qu[3])*chi )]) - - # reduce Euler angles to definition range, i.e a lower limit of 0.0 - eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) - return eu - - -def qu2ax(qu): - """ - Quaternion to axis angle pair. - - Modified version of the original formulation, should be numerically more stable - """ - if iszero(qu[1]**2+qu[2]**2+qu[3]**2): # set axis to [001] if the angle is 0/360 - ax = [ 0.0, 0.0, 1.0, 0.0 ] - elif not iszero(qu[0]): - s = np.sign(qu[0])/np.sqrt(qu[1]**2+qu[2]**2+qu[3]**2) - omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) - ax = [ qu[1]*s, qu[2]*s, qu[3]*s, omega ] - else: - ax = [ qu[1], qu[2], qu[3], np.pi] - - return np.array(ax) - - -def qu2ro(qu): - """Quaternion to Rodriques-Frank vector.""" - if iszero(qu[0]): - ro = [qu[1], qu[2], qu[3], np.inf] - else: - s = np.linalg.norm([qu[1],qu[2],qu[3]]) - ro = [0.0,0.0,P,0.0] if iszero(s) else \ - [ qu[1]/s, qu[2]/s, qu[3]/s, np.tan(np.arccos(np.clip(qu[0],-1.0,1.0)))] # avoid numerical difficulties - - return np.array(ro) - - -def qu2ho(qu): - """Quaternion to homochoric vector.""" - omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) # avoid numerical difficulties - - if iszero(omega): - ho = np.array([ 0.0, 0.0, 0.0 ]) - else: - ho = np.array([qu[1], qu[2], qu[3]]) - f = 0.75 * ( omega - np.sin(omega) ) - ho = ho/np.linalg.norm(ho) * f**(1./3.) - - return ho - - -def qu2cu(qu): - """Quaternion to cubochoric vector.""" - return ho2cu(qu2ho(qu)) - - -#---------- Rotation matrix ---------- - -def om2qu(om): - """ - Rotation matrix to quaternion. - - The original formulation (direct conversion) had (numerical?) issues - """ - return eu2qu(om2eu(om)) - - -def om2eu(om): - """Rotation matrix to Bunge-Euler angles.""" - if abs(om[2,2]) < 1.0: - zeta = 1.0/np.sqrt(1.0-om[2,2]**2) - eu = np.array([np.arctan2(om[2,0]*zeta,-om[2,1]*zeta), - np.arccos(om[2,2]), - np.arctan2(om[0,2]*zeta, om[1,2]*zeta)]) - else: - eu = np.array([np.arctan2( om[0,1],om[0,0]), np.pi*0.5*(1-om[2,2]),0.0]) # following the paper, not the reference implementation - - # reduce Euler angles to definition range, i.e a lower limit of 0.0 - eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) - return eu - - -def om2ax(om): - """Rotation matrix to axis angle pair.""" - ax=np.empty(4) - - # first get the rotation angle - t = 0.5*(om.trace() -1.0) - ax[3] = np.arccos(np.clip(t,-1.0,1.0)) - - if iszero(ax[3]): - ax = [ 0.0, 0.0, 1.0, 0.0] - else: - w,vr = np.linalg.eig(om) - # next, find the eigenvalue (1,0j) - i = np.where(np.isclose(w,1.0+0.0j))[0][0] - ax[0:3] = np.real(vr[0:3,i]) - diagDelta = np.array([om[1,2]-om[2,1],om[2,0]-om[0,2],om[0,1]-om[1,0]]) - ax[0:3] = np.where(iszero(diagDelta), ax[0:3],np.abs(ax[0:3])*np.sign(-P*diagDelta)) - - return np.array(ax) - - -def om2ro(om): - """Rotation matrix to Rodriques-Frank vector.""" - return eu2ro(om2eu(om)) - - -def om2ho(om): - """Rotation matrix to homochoric vector.""" - return ax2ho(om2ax(om)) - - -def om2cu(om): - """Rotation matrix to cubochoric vector.""" - return ho2cu(om2ho(om)) - - -#---------- Bunge-Euler angles ---------- - -def eu2qu(eu): - """Bunge-Euler angles to quaternion.""" - ee = 0.5*eu - cPhi = np.cos(ee[1]) - sPhi = np.sin(ee[1]) - qu = np.array([ cPhi*np.cos(ee[0]+ee[2]), - -P*sPhi*np.cos(ee[0]-ee[2]), - -P*sPhi*np.sin(ee[0]-ee[2]), - -P*cPhi*np.sin(ee[0]+ee[2]) ]) - if qu[0] < 0.0: qu*=-1 - return qu - - -def eu2om(eu): - """Bunge-Euler angles to rotation matrix.""" - c = np.cos(eu) - s = np.sin(eu) - - om = np.array([[+c[0]*c[2]-s[0]*s[2]*c[1], +s[0]*c[2]+c[0]*s[2]*c[1], +s[2]*s[1]], - [-c[0]*s[2]-s[0]*c[2]*c[1], -s[0]*s[2]+c[0]*c[2]*c[1], +c[2]*s[1]], - [+s[0]*s[1], -c[0]*s[1], +c[1] ]]) - - om[np.where(iszero(om))] = 0.0 - return om - - -def eu2ax(eu): - """Bunge-Euler angles to axis angle pair.""" - t = np.tan(eu[1]*0.5) - sigma = 0.5*(eu[0]+eu[2]) - delta = 0.5*(eu[0]-eu[2]) - tau = np.linalg.norm([t,np.sin(sigma)]) - alpha = np.pi if iszero(np.cos(sigma)) else \ - 2.0*np.arctan(tau/np.cos(sigma)) - - if iszero(alpha): - ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) - else: - ax = -P/tau * np.array([ t*np.cos(delta), t*np.sin(delta), np.sin(sigma) ]) # passive axis angle pair so a minus sign in front - ax = np.append(ax,alpha) - if alpha < 0.0: ax *= -1.0 # ensure alpha is positive - - return ax - - -def eu2ro(eu): - """Bunge-Euler angles to Rodriques-Frank vector.""" - ro = eu2ax(eu) # convert to axis angle pair representation - if ro[3] >= np.pi: # Differs from original implementation. check convention 5 - ro[3] = np.inf - elif iszero(ro[3]): - ro = np.array([ 0.0, 0.0, P, 0.0 ]) - else: - ro[3] = np.tan(ro[3]*0.5) - - return ro - - -def eu2ho(eu): - """Bunge-Euler angles to homochoric vector.""" - return ax2ho(eu2ax(eu)) - - -def eu2cu(eu): - """Bunge-Euler angles to cubochoric vector.""" - return ho2cu(eu2ho(eu)) - - -#---------- Axis angle pair ---------- - -def ax2qu(ax): - """Axis angle pair to quaternion.""" - if iszero(ax[3]): - qu = np.array([ 1.0, 0.0, 0.0, 0.0 ]) - else: - c = np.cos(ax[3]*0.5) - s = np.sin(ax[3]*0.5) - qu = np.array([ c, ax[0]*s, ax[1]*s, ax[2]*s ]) - - return qu - - -def ax2om(ax): - """Axis angle pair to rotation matrix.""" - c = np.cos(ax[3]) - s = np.sin(ax[3]) - omc = 1.0-c - om=np.diag(ax[0:3]**2*omc + c) - - for idx in [[0,1,2],[1,2,0],[2,0,1]]: - q = omc*ax[idx[0]] * ax[idx[1]] - om[idx[0],idx[1]] = q + s*ax[idx[2]] - om[idx[1],idx[0]] = q - s*ax[idx[2]] - - return om if P < 0.0 else om.T - - -def ax2eu(ax): - """Rotation matrix to Bunge Euler angles.""" - return om2eu(ax2om(ax)) - - -def ax2ro(ax): - """Axis angle pair to Rodriques-Frank vector.""" - if iszero(ax[3]): - ro = [ 0.0, 0.0, P, 0.0 ] - else: - ro = [ax[0], ax[1], ax[2]] - # 180 degree case - ro += [np.inf] if np.isclose(ax[3],np.pi,atol=1.0e-15,rtol=0.0) else \ - [np.tan(ax[3]*0.5)] - - return np.array(ro) - - -def ax2ho(ax): - """Axis angle pair to homochoric vector.""" - f = (0.75 * ( ax[3] - np.sin(ax[3]) ))**(1.0/3.0) - ho = ax[0:3] * f - return ho - - -def ax2cu(ax): - """Axis angle pair to cubochoric vector.""" - return ho2cu(ax2ho(ax)) - - -#---------- Rodrigues-Frank vector ---------- - -def ro2qu(ro): - """Rodriques-Frank vector to quaternion.""" - return ax2qu(ro2ax(ro)) - - -def ro2om(ro): - """Rodgrigues-Frank vector to rotation matrix.""" - return ax2om(ro2ax(ro)) - - -def ro2eu(ro): - """Rodriques-Frank vector to Bunge-Euler angles.""" - return om2eu(ro2om(ro)) - - -def ro2ax(ro): - """Rodriques-Frank vector to axis angle pair.""" - ta = ro[3] - - if iszero(ta): - ax = [ 0.0, 0.0, 1.0, 0.0 ] - elif not np.isfinite(ta): - ax = [ ro[0], ro[1], ro[2], np.pi ] - else: - angle = 2.0*np.arctan(ta) - ta = 1.0/np.linalg.norm(ro[0:3]) - ax = [ ro[0]/ta, ro[1]/ta, ro[2]/ta, angle ] - - return np.array(ax) - - -def ro2ho(ro): - """Rodriques-Frank vector to homochoric vector.""" - if iszero(np.sum(ro[0:3]**2.0)): - ho = [ 0.0, 0.0, 0.0 ] - else: - f = 2.0*np.arctan(ro[3]) -np.sin(2.0*np.arctan(ro[3])) if np.isfinite(ro[3]) else np.pi - ho = ro[0:3] * (0.75*f)**(1.0/3.0) - - return np.array(ho) - - -def ro2cu(ro): - """Rodriques-Frank vector to cubochoric vector.""" - return ho2cu(ro2ho(ro)) - - -#---------- Homochoric vector---------- - -def ho2qu(ho): - """Homochoric vector to quaternion.""" - return ax2qu(ho2ax(ho)) - - -def ho2om(ho): - """Homochoric vector to rotation matrix.""" - return ax2om(ho2ax(ho)) - - -def ho2eu(ho): - """Homochoric vector to Bunge-Euler angles.""" - return ax2eu(ho2ax(ho)) - - -def ho2ax(ho): - """Homochoric vector to axis angle pair.""" - tfit = np.array([+1.0000000000018852, -0.5000000002194847, - -0.024999992127593126, -0.003928701544781374, - -0.0008152701535450438, -0.0002009500426119712, - -0.00002397986776071756, -0.00008202868926605841, - +0.00012448715042090092, -0.0001749114214822577, - +0.0001703481934140054, -0.00012062065004116828, - +0.000059719705868660826, -0.00001980756723965647, - +0.000003953714684212874, -0.00000036555001439719544]) - # normalize h and store the magnitude - hmag_squared = np.sum(ho**2.) - if iszero(hmag_squared): - ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) - else: - hm = hmag_squared - - # convert the magnitude to the rotation angle - s = tfit[0] + tfit[1] * hmag_squared - for i in range(2,16): - hm *= hmag_squared - s += tfit[i] * hm - ax = np.append(ho/np.sqrt(hmag_squared),2.0*np.arccos(np.clip(s,-1.0,1.0))) - return ax - - -def ho2ro(ho): - """Axis angle pair to Rodriques-Frank vector.""" - return ax2ro(ho2ax(ho)) - - -def ho2cu(ho): - """Homochoric vector to cubochoric vector.""" - return Lambert.BallToCube(ho) - - -#---------- Cubochoric ---------- - -def cu2qu(cu): - """Cubochoric vector to quaternion.""" - return ho2qu(cu2ho(cu)) - - -def cu2om(cu): - """Cubochoric vector to rotation matrix.""" - return ho2om(cu2ho(cu)) - - -def cu2eu(cu): - """Cubochoric vector to Bunge-Euler angles.""" - return ho2eu(cu2ho(cu)) - - -def cu2ax(cu): - """Cubochoric vector to axis angle pair.""" - return ho2ax(cu2ho(cu)) - - -def cu2ro(cu): - """Cubochoric vector to Rodriques-Frank vector.""" - return ho2ro(cu2ho(cu)) - - -def cu2ho(cu): - """Cubochoric vector to homochoric vector.""" - return Lambert.CubeToBall(cu) diff --git a/python/damask/rotation.py b/python/damask/rotation.py new file mode 100644 index 000000000..8f2aaa8c9 --- /dev/null +++ b/python/damask/rotation.py @@ -0,0 +1,1492 @@ +import math + +import numpy as np + +from . import Lambert + +P = -1 + +#################################################################################################### +class Rotation: + u""" + Orientation stored with functionality for conversion to different representations. + + References + ---------- + D. Rowenhorst et al., Modelling and Simulation in Materials Science and Engineering 23:083501, 2015 + https://doi.org/10.1088/0965-0393/23/8/083501 + + Conventions + ----------- + Convention 1: Coordinate frames are right-handed. + Convention 2: A rotation angle ω is taken to be positive for a counterclockwise rotation + when viewing from the end point of the rotation axis towards the origin. + Convention 3: Rotations will be interpreted in the passive sense. + Convention 4: Euler angle triplets are implemented using the Bunge convention, + with the angular ranges as [0, 2π],[0, π],[0, 2π]. + Convention 5: The rotation angle ω is limited to the interval [0, π]. + Convention 6: the real part of a quaternion is positive, Re(q) > 0 + Convention 7: P = -1 (as default). + + Usage + ----- + Vector "a" (defined in coordinate system "A") is passively rotated + resulting in new coordinates "b" when expressed in system "B". + b = Q * a + b = np.dot(Q.asMatrix(),a) + + """ + + __slots__ = ['quaternion'] + + def __init__(self,quaternion = np.array([1.0,0.0,0.0,0.0])): + """ + Initializes to identity unless specified. + + Parameters + ---------- + quaternion : numpy.ndarray, optional + Unit quaternion that follows the conventions. Use .fromQuaternion to perform a sanity check. + + """ + self.quaternion = quaternion.copy() + + def __copy__(self): + """Copy.""" + return self.__class__(self.quaternion) + + copy = __copy__ + + + def __repr__(self): + """Orientation displayed as unit quaternion, rotation matrix, and Bunge-Euler angles.""" + return '\n'.join([ + 'Quaternion: (real={:.3f}, imag=<{:+.3f}, {:+.3f}, {:+.3f}>)'.format(*(self.quaternion)), + 'Matrix:\n{}'.format(self.asMatrix()), + 'Bunge Eulers / deg: ({:3.2f}, {:3.2f}, {:3.2f})'.format(*self.asEulers(degrees=True)), + ]) + + + def __mul__(self, other): + """ + Multiplication. + + Parameters + ---------- + other : numpy.ndarray or Rotation + Vector, second or fourth order tensor, or rotation object that is rotated. + + Todo + ---- + Document details active/passive) + considere rotation of (3,3,3,3)-matrix + + """ + if isinstance(other, Rotation): # rotate a rotation + self_q = self.quaternion[0] + self_p = self.quaternion[1:] + other_q = other.quaternion[0] + other_p = other.quaternion[1:] + R = self.__class__(np.append(self_q*other_q - np.dot(self_p,other_p), + self_q*other_p + other_q*self_p + P * np.cross(self_p,other_p))) + return R.standardize() + elif isinstance(other, (tuple,np.ndarray)): + if isinstance(other,tuple) or other.shape == (3,): # rotate a single (3)-vector or meshgrid + A = self.quaternion[0]**2.0 - np.dot(self.quaternion[1:],self.quaternion[1:]) + B = 2.0 * ( self.quaternion[1]*other[0] + + self.quaternion[2]*other[1] + + self.quaternion[3]*other[2]) + C = 2.0 * P*self.quaternion[0] + + return np.array([ + A*other[0] + B*self.quaternion[1] + C*(self.quaternion[2]*other[2] - self.quaternion[3]*other[1]), + A*other[1] + B*self.quaternion[2] + C*(self.quaternion[3]*other[0] - self.quaternion[1]*other[2]), + A*other[2] + B*self.quaternion[3] + C*(self.quaternion[1]*other[1] - self.quaternion[2]*other[0]), + ]) + elif other.shape == (3,3,): # rotate a single (3x3)-matrix + return np.dot(self.asMatrix(),np.dot(other,self.asMatrix().T)) + elif other.shape == (3,3,3,3,): + raise NotImplementedError + else: + return NotImplemented + else: + return NotImplemented + + + def inverse(self): + """In-place inverse rotation/backward rotation.""" + self.quaternion[1:] *= -1 + return self + + def inversed(self): + """Inverse rotation/backward rotation.""" + return self.copy().inverse() + + + def standardize(self): + """In-place quaternion representation with positive q.""" + if self.quaternion[0] < 0.0: self.quaternion*=-1 + return self + + def standardized(self): + """Quaternion representation with positive q.""" + return self.copy().standardize() + + + def misorientation(self,other): + """ + Get Misorientation. + + Parameters + ---------- + other : Rotation + Rotation to which the misorientation is computed. + + """ + return other*self.inversed() + + + def average(self,other): + """ + Calculate the average rotation. + + Parameters + ---------- + other : Rotation + Rotation from which the average is rotated. + + """ + return Rotation.fromAverage([self,other]) + + + ################################################################################################ + # convert to different orientation representations (numpy arrays) + + def asQuaternion(self): + """ + Unit quaternion [q, p_1, p_2, p_3] unless quaternion == True: damask.quaternion object. + + Parameters + ---------- + quaternion : bool, optional + return quaternion as DAMASK object. + + """ + return self.quaternion + + def asEulers(self, + degrees = False): + """ + Bunge-Euler angles: (φ_1, ϕ, φ_2). + + Parameters + ---------- + degrees : bool, optional + return angles in degrees. + + """ + eu = qu2eu(self.quaternion) + if degrees: eu = np.degrees(eu) + return eu + + def asAxisAngle(self, + degrees = False, + pair = False): + """ + Axis angle representation [n_1, n_2, n_3, ω] unless pair == True: ([n_1, n_2, n_3], ω). + + Parameters + ---------- + degrees : bool, optional + return rotation angle in degrees. + pair : bool, optional + return tuple of axis and angle. + + """ + ax = qu2ax(self.quaternion) + if degrees: ax[3] = np.degrees(ax[3]) + return (ax[:3],np.degrees(ax[3])) if pair else ax + + def asMatrix(self): + """Rotation matrix.""" + return qu2om(self.quaternion) + + def asRodrigues(self, + vector = False): + """ + Rodrigues-Frank vector representation [n_1, n_2, n_3, tan(ω/2)] unless vector == True: + [n_1, n_2, n_3] * tan(ω/2). + + Parameters + ---------- + vector : bool, optional + return as actual Rodrigues--Frank vector, i.e. rotation axis scaled by tan(ω/2). + + """ + ro = qu2ro(self.quaternion) + return ro[:3]*ro[3] if vector else ro + + def asHomochoric(self): + """Homochoric vector: (h_1, h_2, h_3).""" + return qu2ho(self.quaternion) + + def asCubochoric(self): + """Cubochoric vector: (c_1, c_2, c_3).""" + return qu2cu(self.quaternion) + + def asM(self): + """ + Intermediate representation supporting quaternion averaging. + + References + ---------- + F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 + https://doi.org/10.2514/1.28949 + + """ + return np.outer(self.quaternion,self.quaternion) + + + ################################################################################################ + # static constructors. The input data needs to follow the convention, options allow to + # relax these convections + @staticmethod + def fromQuaternion(quaternion, + acceptHomomorph = False, + P = -1): + + qu = quaternion if isinstance(quaternion,np.ndarray) and quaternion.dtype == np.dtype(float) \ + else np.array(quaternion,dtype=float) + if P > 0: qu[1:4] *= -1 # convert from P=1 to P=-1 + if qu[0] < 0.0: + if acceptHomomorph: + qu *= -1. + else: + raise ValueError('Quaternion has negative first component.\n{}'.format(qu[0])) + if not np.isclose(np.linalg.norm(qu), 1.0): + raise ValueError('Quaternion is not of unit length.\n{} {} {} {}'.format(*qu)) + + return Rotation(qu) + + @staticmethod + def fromEulers(eulers, + degrees = False): + + eu = eulers if isinstance(eulers, np.ndarray) and eulers.dtype == np.dtype(float) \ + else np.array(eulers,dtype=float) + eu = np.radians(eu) if degrees else eu + if np.any(eu < 0.0) or np.any(eu > 2.0*np.pi) or eu[1] > np.pi: + raise ValueError('Euler angles outside of [0..2π],[0..π],[0..2π].\n{} {} {}.'.format(*eu)) + + return Rotation(eu2qu(eu)) + + @staticmethod + def fromAxisAngle(angleAxis, + degrees = False, + normalise = False, + P = -1): + + ax = angleAxis if isinstance(angleAxis, np.ndarray) and angleAxis.dtype == np.dtype(float) \ + else np.array(angleAxis,dtype=float) + if P > 0: ax[0:3] *= -1 # convert from P=1 to P=-1 + if degrees: ax[ 3] = np.radians(ax[3]) + if normalise: ax[0:3] /= np.linalg.norm(ax[0:3]) + if ax[3] < 0.0 or ax[3] > np.pi: + raise ValueError('Axis angle rotation angle outside of [0..π].\n'.format(ax[3])) + if not np.isclose(np.linalg.norm(ax[0:3]), 1.0): + raise ValueError('Axis angle rotation axis is not of unit length.\n{} {} {}'.format(*ax[0:3])) + + return Rotation(ax2qu(ax)) + + @staticmethod + def fromBasis(basis, + orthonormal = True, + reciprocal = False, + ): + + om = basis if isinstance(basis, np.ndarray) else np.array(basis).reshape((3,3)) + if reciprocal: + om = np.linalg.inv(om.T/np.pi) # transform reciprocal basis set + orthonormal = False # contains stretch + if not orthonormal: + (U,S,Vh) = np.linalg.svd(om) # singular value decomposition + om = np.dot(U,Vh) + if not np.isclose(np.linalg.det(om),1.0): + raise ValueError('matrix is not a proper rotation.\n{}'.format(om)) + if not np.isclose(np.dot(om[0],om[1]), 0.0) \ + or not np.isclose(np.dot(om[1],om[2]), 0.0) \ + or not np.isclose(np.dot(om[2],om[0]), 0.0): + raise ValueError('matrix is not orthogonal.\n{}'.format(om)) + + return Rotation(om2qu(om)) + + @staticmethod + def fromMatrix(om, + ): + + return Rotation.fromBasis(om) + + @staticmethod + def fromRodrigues(rodrigues, + normalise = False, + P = -1): + + ro = rodrigues if isinstance(rodrigues, np.ndarray) and rodrigues.dtype == np.dtype(float) \ + else np.array(rodrigues,dtype=float) + if P > 0: ro[0:3] *= -1 # convert from P=1 to P=-1 + if normalise: ro[0:3] /= np.linalg.norm(ro[0:3]) + if not np.isclose(np.linalg.norm(ro[0:3]), 1.0): + raise ValueError('Rodrigues rotation axis is not of unit length.\n{} {} {}'.format(*ro[0:3])) + if ro[3] < 0.0: + raise ValueError('Rodriques rotation angle not positive.\n'.format(ro[3])) + + return Rotation(ro2qu(ro)) + + @staticmethod + def fromHomochoric(homochoric, + P = -1): + + ho = homochoric if isinstance(homochoric, np.ndarray) and homochoric.dtype == np.dtype(float) \ + else np.array(homochoric,dtype=float) + if P > 0: ho *= -1 # convert from P=1 to P=-1 + + return Rotation(ho2qu(ho)) + + @staticmethod + def fromCubochoric(cubochoric, + P = -1): + + cu = cubochoric if isinstance(cubochoric, np.ndarray) and cubochoric.dtype == np.dtype(float) \ + else np.array(cubochoric,dtype=float) + ho = cu2ho(cu) + if P > 0: ho *= -1 # convert from P=1 to P=-1 + + return Rotation(ho2qu(ho)) + + + @staticmethod + def fromAverage(rotations, + weights = []): + """ + Average rotation. + + References + ---------- + F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 + https://doi.org/10.2514/1.28949 + + Parameters + ---------- + rotations : list of Rotations + Rotations to average from + weights : list of floats, optional + Weights for each rotation used for averaging + + """ + if not all(isinstance(item, Rotation) for item in rotations): + raise TypeError("Only instances of Rotation can be averaged.") + + N = len(rotations) + if weights == [] or not weights: + weights = np.ones(N,dtype='i') + + for i,(r,n) in enumerate(zip(rotations,weights)): + M = r.asM() * n if i == 0 \ + else M + r.asM() * n # noqa add (multiples) of this rotation to average noqa + eig, vec = np.linalg.eig(M/N) + + return Rotation.fromQuaternion(np.real(vec.T[eig.argmax()]),acceptHomomorph = True) + + + @staticmethod + def fromRandom(): + r = np.random.random(3) + A = np.sqrt(r[2]) + B = np.sqrt(1.0-r[2]) + return Rotation(np.array([np.cos(2.0*np.pi*r[0])*A, + np.sin(2.0*np.pi*r[1])*B, + np.cos(2.0*np.pi*r[1])*B, + np.sin(2.0*np.pi*r[0])*A])).standardize() + + + +# ****************************************************************************************** +class Symmetry: + """ + Symmetry operations for lattice systems. + + References + ---------- + https://en.wikipedia.org/wiki/Crystal_system + + """ + + lattices = [None,'orthorhombic','tetragonal','hexagonal','cubic',] + + def __init__(self, symmetry = None): + """ + Symmetry Definition. + + Parameters + ---------- + symmetry : str, optional + label of the crystal system + + """ + if symmetry is not None and symmetry.lower() not in Symmetry.lattices: + raise KeyError('Symmetry/crystal system "{}" is unknown'.format(symmetry)) + + self.lattice = symmetry.lower() if isinstance(symmetry,str) else symmetry + + + def __copy__(self): + """Copy.""" + return self.__class__(self.lattice) + + copy = __copy__ + + + def __repr__(self): + """Readable string.""" + return '{}'.format(self.lattice) + + + def __eq__(self, other): + """ + Equal to other. + + Parameters + ---------- + other : Symmetry + Symmetry to check for equality. + + """ + return self.lattice == other.lattice + + def __neq__(self, other): + """ + Not Equal to other. + + Parameters + ---------- + other : Symmetry + Symmetry to check for inequality. + + """ + return not self.__eq__(other) + + def __cmp__(self,other): + """ + Linear ordering. + + Parameters + ---------- + other : Symmetry + Symmetry to check for for order. + + """ + myOrder = Symmetry.lattices.index(self.lattice) + otherOrder = Symmetry.lattices.index(other.lattice) + return (myOrder > otherOrder) - (myOrder < otherOrder) + + def symmetryOperations(self,members=[]): + """List (or single element) of symmetry operations as rotations.""" + if self.lattice == 'cubic': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, 0.0, 0.0, 1.0 ], + [ 0.0, 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2) ], + [ 0.0, 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], + [ 0.0, -0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], + [ 0.5, 0.5, 0.5, 0.5 ], + [-0.5, 0.5, 0.5, 0.5 ], + [-0.5, 0.5, 0.5, -0.5 ], + [-0.5, 0.5, -0.5, 0.5 ], + [-0.5, -0.5, 0.5, 0.5 ], + [-0.5, -0.5, 0.5, -0.5 ], + [-0.5, -0.5, -0.5, 0.5 ], + [-0.5, 0.5, -0.5, -0.5 ], + [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [-0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2), 0.0 ], + [-0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2), 0.0 ], + [-0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0, 0.0 ], + [-0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0, 0.0 ], + ] + elif self.lattice == 'hexagonal': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [-0.5*np.sqrt(3), 0.0, 0.0, -0.5 ], + [ 0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], + [ 0.0, 0.0, 0.0, 1.0 ], + [-0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], + [-0.5*np.sqrt(3), 0.0, 0.0, 0.5 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, -0.5*np.sqrt(3), 0.5, 0.0 ], + [ 0.0, 0.5, -0.5*np.sqrt(3), 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, -0.5, -0.5*np.sqrt(3), 0.0 ], + [ 0.0, 0.5*np.sqrt(3), 0.5, 0.0 ], + ] + elif self.lattice == 'tetragonal': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, 0.0, 0.0, 1.0 ], + [ 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], + [ 0.0, -0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], + [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + ] + elif self.lattice == 'orthorhombic': + symQuats = [ + [ 1.0,0.0,0.0,0.0 ], + [ 0.0,1.0,0.0,0.0 ], + [ 0.0,0.0,1.0,0.0 ], + [ 0.0,0.0,0.0,1.0 ], + ] + else: + symQuats = [ + [ 1.0,0.0,0.0,0.0 ], + ] + + symOps = list(map(Rotation, + np.array(symQuats)[np.atleast_1d(members) if members != [] else range(len(symQuats))])) + try: + iter(members) # asking for (even empty) list of members? + except TypeError: + return symOps[0] # no, return rotation object + else: + return symOps # yes, return list of rotations + + + def inFZ(self,rodrigues): + """ + Check whether given Rodriques-Frank vector falls into fundamental zone of own symmetry. + + Fundamental zone in Rodrigues space is point symmetric around origin. + """ + if (len(rodrigues) != 3): + raise ValueError('Input is not a Rodriques-Frank vector.\n') + + if np.any(rodrigues == np.inf): return False + + Rabs = abs(rodrigues) + + if self.lattice == 'cubic': + return math.sqrt(2.0)-1.0 >= Rabs[0] \ + and math.sqrt(2.0)-1.0 >= Rabs[1] \ + and math.sqrt(2.0)-1.0 >= Rabs[2] \ + and 1.0 >= Rabs[0] + Rabs[1] + Rabs[2] + elif self.lattice == 'hexagonal': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] \ + and 2.0 >= math.sqrt(3)*Rabs[0] + Rabs[1] \ + and 2.0 >= math.sqrt(3)*Rabs[1] + Rabs[0] \ + and 2.0 >= math.sqrt(3) + Rabs[2] + elif self.lattice == 'tetragonal': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] \ + and math.sqrt(2.0) >= Rabs[0] + Rabs[1] \ + and math.sqrt(2.0) >= Rabs[2] + 1.0 + elif self.lattice == 'orthorhombic': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] + else: + return True + + + def inDisorientationSST(self,rodrigues): + """ + Check whether given Rodriques-Frank vector (of misorientation) falls into standard stereographic triangle of own symmetry. + + References + ---------- + A. Heinz and P. Neumann, Acta Crystallographica Section A 47:780-789, 1991 + https://doi.org/10.1107/S0108767391006864 + + """ + if (len(rodrigues) != 3): + raise ValueError('Input is not a Rodriques-Frank vector.\n') + R = rodrigues + + epsilon = 0.0 + if self.lattice == 'cubic': + return R[0] >= R[1]+epsilon and R[1] >= R[2]+epsilon and R[2] >= epsilon + elif self.lattice == 'hexagonal': + return R[0] >= math.sqrt(3)*(R[1]-epsilon) and R[1] >= epsilon and R[2] >= epsilon + elif self.lattice == 'tetragonal': + return R[0] >= R[1]-epsilon and R[1] >= epsilon and R[2] >= epsilon + elif self.lattice == 'orthorhombic': + return R[0] >= epsilon and R[1] >= epsilon and R[2] >= epsilon + else: + return True + + + def inSST(self, + vector, + proper = False, + color = False): + """ + Check whether given vector falls into standard stereographic triangle of own symmetry. + + proper considers only vectors with z >= 0, hence uses two neighboring SSTs. + Return inverse pole figure color if requested. + Bases are computed from + + basis = {'cubic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,1.]/np.sqrt(2.), # direction of green + [1.,1.,1.]/np.sqrt(3.)]).T), # direction of blue + 'hexagonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [np.sqrt(3.),1.,0.]/np.sqrt(4.)]).T), # direction of blue + 'tetragonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [1.,1.,0.]/np.sqrt(2.)]).T), # direction of blue + 'orthorhombic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [0.,1.,0.]]).T), # direction of blue + } + """ + if self.lattice == 'cubic': + basis = {'improper':np.array([ [-1. , 0. , 1. ], + [ np.sqrt(2.) , -np.sqrt(2.) , 0. ], + [ 0. , np.sqrt(3.) , 0. ] ]), + 'proper':np.array([ [ 0. , -1. , 1. ], + [-np.sqrt(2.) , np.sqrt(2.) , 0. ], + [ np.sqrt(3.) , 0. , 0. ] ]), + } + elif self.lattice == 'hexagonal': + basis = {'improper':np.array([ [ 0. , 0. , 1. ], + [ 1. , -np.sqrt(3.) , 0. ], + [ 0. , 2. , 0. ] ]), + 'proper':np.array([ [ 0. , 0. , 1. ], + [-1. , np.sqrt(3.) , 0. ], + [ np.sqrt(3.) , -1. , 0. ] ]), + } + elif self.lattice == 'tetragonal': + basis = {'improper':np.array([ [ 0. , 0. , 1. ], + [ 1. , -1. , 0. ], + [ 0. , np.sqrt(2.) , 0. ] ]), + 'proper':np.array([ [ 0. , 0. , 1. ], + [-1. , 1. , 0. ], + [ np.sqrt(2.) , 0. , 0. ] ]), + } + elif self.lattice == 'orthorhombic': + basis = {'improper':np.array([ [ 0., 0., 1.], + [ 1., 0., 0.], + [ 0., 1., 0.] ]), + 'proper':np.array([ [ 0., 0., 1.], + [-1., 0., 0.], + [ 0., 1., 0.] ]), + } + else: # direct exit for unspecified symmetry + if color: + return (True,np.zeros(3,'d')) + else: + return True + + v = np.array(vector,dtype=float) + if proper: # check both improper ... + theComponents = np.around(np.dot(basis['improper'],v),12) + inSST = np.all(theComponents >= 0.0) + if not inSST: # ... and proper SST + theComponents = np.around(np.dot(basis['proper'],v),12) + inSST = np.all(theComponents >= 0.0) + else: + v[2] = abs(v[2]) # z component projects identical + theComponents = np.around(np.dot(basis['improper'],v),12) # for positive and negative values + inSST = np.all(theComponents >= 0.0) + + if color: # have to return color array + if inSST: + rgb = np.power(theComponents/np.linalg.norm(theComponents),0.5) # smoothen color ramps + rgb = np.minimum(np.ones(3,dtype=float),rgb) # limit to maximum intensity + rgb /= max(rgb) # normalize to (HS)V = 1 + else: + rgb = np.zeros(3,dtype=float) + return (inSST,rgb) + else: + return inSST + +# code derived from https://github.com/ezag/pyeuclid +# suggested reading: http://web.mit.edu/2.998/www/QuaternionReport1.pdf + + +# ****************************************************************************************** +class Lattice: + """ + Lattice system. + + Currently, this contains only a mapping from Bravais lattice to symmetry + and orientation relationships. It could include twin and slip systems. + + References + ---------- + https://en.wikipedia.org/wiki/Bravais_lattice + + """ + + lattices = { + 'triclinic':{'symmetry':None}, + 'bct':{'symmetry':'tetragonal'}, + 'hex':{'symmetry':'hexagonal'}, + 'fcc':{'symmetry':'cubic','c/a':1.0}, + 'bcc':{'symmetry':'cubic','c/a':1.0}, + } + + + def __init__(self, lattice): + """ + New lattice of given type. + + Parameters + ---------- + lattice : str + Bravais lattice. + + """ + self.lattice = lattice + self.symmetry = Symmetry(self.lattices[lattice]['symmetry']) + + + def __repr__(self): + """Report basic lattice information.""" + return 'Bravais lattice {} ({} symmetry)'.format(self.lattice,self.symmetry) + + + # Kurdjomov--Sachs orientation relationship for fcc <-> bcc transformation + # from S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 + # also see K. Kitahara et al., Acta Materialia 54:1279-1288, 2006 + KS = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]]],dtype='float'), + 'directions': np.array([ + [[ -1, 0, 1],[ -1, -1, 1]], + [[ -1, 0, 1],[ -1, 1, -1]], + [[ 0, 1, -1],[ -1, -1, 1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ 1, -1, 0],[ -1, -1, 1]], + [[ 1, -1, 0],[ -1, 1, -1]], + [[ 1, 0, -1],[ -1, -1, 1]], + [[ 1, 0, -1],[ -1, 1, -1]], + [[ -1, -1, 0],[ -1, -1, 1]], + [[ -1, -1, 0],[ -1, 1, -1]], + [[ 0, 1, 1],[ -1, -1, 1]], + [[ 0, 1, 1],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ 0, -1, 1],[ -1, 1, -1]], + [[ -1, 0, -1],[ -1, -1, 1]], + [[ -1, 0, -1],[ -1, 1, -1]], + [[ 1, 1, 0],[ -1, -1, 1]], + [[ 1, 1, 0],[ -1, 1, -1]], + [[ -1, 1, 0],[ -1, -1, 1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, -1],[ -1, -1, 1]], + [[ 0, -1, -1],[ -1, 1, -1]], + [[ 1, 0, 1],[ -1, -1, 1]], + [[ 1, 0, 1],[ -1, 1, -1]]],dtype='float')} + + # Greninger--Troiano orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + GT = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 1, 0, 1]], + [[ 1, 1, 1],[ 1, 1, 0]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ -1, 0, 1]], + [[ -1, -1, 1],[ -1, -1, 0]], + [[ -1, -1, 1],[ 0, -1, 1]], + [[ -1, 1, 1],[ -1, 0, 1]], + [[ -1, 1, 1],[ -1, 1, 0]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 1, 0, 1]], + [[ 1, -1, 1],[ 1, -1, 0]], + [[ 1, -1, 1],[ 0, -1, 1]], + [[ 1, 1, 1],[ 1, 1, 0]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 1, 0, 1]], + [[ -1, -1, 1],[ -1, -1, 0]], + [[ -1, -1, 1],[ 0, -1, 1]], + [[ -1, -1, 1],[ -1, 0, 1]], + [[ -1, 1, 1],[ -1, 1, 0]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ -1, 0, 1]], + [[ 1, -1, 1],[ 1, -1, 0]], + [[ 1, -1, 1],[ 0, -1, 1]], + [[ 1, -1, 1],[ 1, 0, 1]]],dtype='float'), + 'directions': np.array([ + [[ -5,-12, 17],[-17, -7, 17]], + [[ 17, -5,-12],[ 17,-17, -7]], + [[-12, 17, -5],[ -7, 17,-17]], + [[ 5, 12, 17],[ 17, 7, 17]], + [[-17, 5,-12],[-17, 17, -7]], + [[ 12,-17, -5],[ 7,-17,-17]], + [[ -5, 12,-17],[-17, 7,-17]], + [[ 17, 5, 12],[ 17, 17, 7]], + [[-12,-17, 5],[ -7,-17, 17]], + [[ 5,-12,-17],[ 17, -7,-17]], + [[-17, -5, 12],[-17,-17, 7]], + [[ 12, 17, 5],[ 7, 17, 17]], + [[ -5, 17,-12],[-17, 17, -7]], + [[-12, -5, 17],[ -7,-17, 17]], + [[ 17,-12, -5],[ 17, -7,-17]], + [[ 5,-17,-12],[ 17,-17, -7]], + [[ 12, 5, 17],[ 7, 17, 17]], + [[-17, 12, -5],[-17, 7,-17]], + [[ -5,-17, 12],[-17,-17, 7]], + [[-12, 5,-17],[ -7, 17,-17]], + [[ 17, 12, 5],[ 17, 7, 17]], + [[ 5, 17, 12],[ 17, 17, 7]], + [[ 12, -5,-17],[ 7,-17,-17]], + [[-17,-12, 5],[-17,-7, 17]]],dtype='float')} + + # Greninger--Troiano' orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + GTprime = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 7, 17, 17],[ 12, 5, 17]], + [[ 17, 7, 17],[ 17, 12, 5]], + [[ 17, 17, 7],[ 5, 17, 12]], + [[ -7,-17, 17],[-12, -5, 17]], + [[-17, -7, 17],[-17,-12, 5]], + [[-17,-17, 7],[ -5,-17, 12]], + [[ 7,-17,-17],[ 12, -5,-17]], + [[ 17, -7,-17],[ 17,-12, -5]], + [[ 17,-17, -7],[ 5,-17,-12]], + [[ -7, 17,-17],[-12, 5,-17]], + [[-17, 7,-17],[-17, 12, -5]], + [[-17, 17, -7],[ -5, 17,-12]], + [[ 7, 17, 17],[ 12, 17, 5]], + [[ 17, 7, 17],[ 5, 12, 17]], + [[ 17, 17, 7],[ 17, 5, 12]], + [[ -7,-17, 17],[-12,-17, 5]], + [[-17, -7, 17],[ -5,-12, 17]], + [[-17,-17, 7],[-17, -5, 12]], + [[ 7,-17,-17],[ 12,-17, -5]], + [[ 17, -7,-17],[ 5, -12,-17]], + [[ 17,-17, -7],[ 17, -5,-12]], + [[ -7, 17,-17],[-12, 17, -5]], + [[-17, 7,-17],[ -5, 12,-17]], + [[-17, 17, -7],[-17, 5,-12]]],dtype='float'), + 'directions': np.array([ + [[ 0, 1, -1],[ 1, 1, -1]], + [[ -1, 0, 1],[ -1, 1, 1]], + [[ 1, -1, 0],[ 1, -1, 1]], + [[ 0, -1, -1],[ -1, -1, -1]], + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, -1, 0],[ 1, -1, -1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ 1, 0, 1],[ 1, 1, 1]], + [[ -1, -1, 0],[ -1, -1, 1]], + [[ 0, -1, -1],[ 1, -1, -1]], + [[ -1, 0, 1],[ -1, -1, 1]], + [[ -1, -1, 0],[ -1, -1, -1]], + [[ 0, -1, 1],[ 1, -1, 1]], + [[ 1, 0, -1],[ 1, 1, -1]], + [[ -1, 1, 0],[ -1, 1, 1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ -1, 0, -1],[ -1, -1, -1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ -1, 0, -1],[ -1, 1, -1]], + [[ 1, 1, 0],[ 1, 1, 1]], + [[ 0, 1, 1],[ 1, 1, 1]], + [[ 1, 0, -1],[ 1, -1, -1]], + [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} + + # Nishiyama--Wassermann orientation relationship for fcc <-> bcc transformation + # from H. Kitahara et al., Materials Characterization 54:378-386, 2005 + NW = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]]],dtype='float'), + 'directions': np.array([ + [[ 2, -1, -1],[ 0, -1, 1]], + [[ -1, 2, -1],[ 0, -1, 1]], + [[ -1, -1, 2],[ 0, -1, 1]], + [[ -2, -1, -1],[ 0, -1, 1]], + [[ 1, 2, -1],[ 0, -1, 1]], + [[ 1, -1, 2],[ 0, -1, 1]], + [[ 2, 1, -1],[ 0, -1, 1]], + [[ -1, -2, -1],[ 0, -1, 1]], + [[ -1, 1, 2],[ 0, -1, 1]], + [[ 2, -1, 1],[ 0, -1, 1]], #It is wrong in the paper, but matrix is correct + [[ -1, 2, 1],[ 0, -1, 1]], + [[ -1, -1, -2],[ 0, -1, 1]]],dtype='float')} + + # Pitsch orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Acta Materialia 53:1179-1190, 2005 + Pitsch = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 0, 1, 0],[ -1, 0, 1]], + [[ 0, 0, 1],[ 1, -1, 0]], + [[ 1, 0, 0],[ 0, 1, -1]], + [[ 1, 0, 0],[ 0, -1, -1]], + [[ 0, 1, 0],[ -1, 0, -1]], + [[ 0, 0, 1],[ -1, -1, 0]], + [[ 0, 1, 0],[ -1, 0, -1]], + [[ 0, 0, 1],[ -1, -1, 0]], + [[ 1, 0, 0],[ 0, -1, -1]], + [[ 1, 0, 0],[ 0, -1, 1]], + [[ 0, 1, 0],[ 1, 0, -1]], + [[ 0, 0, 1],[ -1, 1, 0]]],dtype='float'), + 'directions': np.array([ + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, 1, 0],[ 1, 1, -1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ -1, 0, 1],[ -1, -1, 1]], + [[ 1, -1, 0],[ 1, -1, -1]], + [[ 1, 0, -1],[ 1, -1, -1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} + + # Bain orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + Bain = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 0, 0],[ 1, 0, 0]], + [[ 0, 1, 0],[ 0, 1, 0]], + [[ 0, 0, 1],[ 0, 0, 1]]],dtype='float'), + 'directions': np.array([ + [[ 0, 1, 0],[ 0, 1, 1]], + [[ 0, 0, 1],[ 1, 0, 1]], + [[ 1, 0, 0],[ 1, 1, 0]]],dtype='float')} + + def relationOperations(self,model): + """ + Crystallographic orientation relationships for phase transformations. + + References + ---------- + S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 + https://doi.org/10.1016/j.jallcom.2012.02.004 + + K. Kitahara et al., Acta Materialia 54(5):1279-1288, 2006 + https://doi.org/10.1016/j.actamat.2005.11.001 + + Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + https://doi.org/10.1107/S0021889805038276 + + H. Kitahara et al., Materials Characterization 54(4-5):378-386, 2005 + https://doi.org/10.1016/j.matchar.2004.12.015 + + Y. He et al., Acta Materialia 53(4):1179-1190, 2005 + https://doi.org/10.1016/j.actamat.2004.11.021 + + """ + models={'KS':self.KS, 'GT':self.GT, 'GT_prime':self.GTprime, + 'NW':self.NW, 'Pitsch': self.Pitsch, 'Bain':self.Bain} + try: + relationship = models[model] + except KeyError : + raise KeyError('Orientation relationship "{}" is unknown'.format(model)) + + if self.lattice not in relationship['mapping']: + raise ValueError('Relationship "{}" not supported for lattice "{}"'.format(model,self.lattice)) + + r = {'lattice':Lattice((set(relationship['mapping'])-{self.lattice}).pop()), # target lattice + 'rotations':[] } + + myPlane_id = relationship['mapping'][self.lattice] + otherPlane_id = (myPlane_id+1)%2 + myDir_id = myPlane_id +2 + otherDir_id = otherPlane_id +2 + + for miller in np.hstack((relationship['planes'],relationship['directions'])): + myPlane = miller[myPlane_id]/ np.linalg.norm(miller[myPlane_id]) + myDir = miller[myDir_id]/ np.linalg.norm(miller[myDir_id]) + myMatrix = np.array([myDir,np.cross(myPlane,myDir),myPlane]) + + otherPlane = miller[otherPlane_id]/ np.linalg.norm(miller[otherPlane_id]) + otherDir = miller[otherDir_id]/ np.linalg.norm(miller[otherDir_id]) + otherMatrix = np.array([otherDir,np.cross(otherPlane,otherDir),otherPlane]) + + r['rotations'].append(Rotation.fromMatrix(np.dot(otherMatrix.T,myMatrix))) + + return r + +#################################################################################################### +# Code below available according to the following conditions on https://github.com/MarDiehl/3Drotations +#################################################################################################### +# Copyright (c) 2017-2019, Martin Diehl/Max-Planck-Institut für Eisenforschung GmbH +# Copyright (c) 2013-2014, Marc De Graef/Carnegie Mellon University +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# - Redistributions of source code must retain the above copyright notice, this list +# of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# - Neither the names of Marc De Graef, Carnegie Mellon University nor the names +# of its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#################################################################################################### + +def isone(a): + return np.isclose(a,1.0,atol=1.0e-7,rtol=0.0) + +def iszero(a): + return np.isclose(a,0.0,atol=1.0e-12,rtol=0.0) + +#---------- Quaternion ---------- + +def qu2om(qu): + """Quaternion to rotation matrix.""" + qq = qu[0]**2-(qu[1]**2 + qu[2]**2 + qu[3]**2) + om = np.diag(qq + 2.0*np.array([qu[1],qu[2],qu[3]])**2) + + om[1,0] = 2.0*(qu[2]*qu[1]+qu[0]*qu[3]) + om[0,1] = 2.0*(qu[1]*qu[2]-qu[0]*qu[3]) + om[2,1] = 2.0*(qu[3]*qu[2]+qu[0]*qu[1]) + om[1,2] = 2.0*(qu[2]*qu[3]-qu[0]*qu[1]) + om[0,2] = 2.0*(qu[1]*qu[3]+qu[0]*qu[2]) + om[2,0] = 2.0*(qu[3]*qu[1]-qu[0]*qu[2]) + return om if P > 0.0 else om.T + + +def qu2eu(qu): + """Quaternion to Bunge-Euler angles.""" + q03 = qu[0]**2+qu[3]**2 + q12 = qu[1]**2+qu[2]**2 + chi = np.sqrt(q03*q12) + + if iszero(chi): + eu = np.array([np.arctan2(-P*2.0*qu[0]*qu[3],qu[0]**2-qu[3]**2), 0.0, 0.0]) if iszero(q12) else \ + np.array([np.arctan2(2.0*qu[1]*qu[2],qu[1]**2-qu[2]**2), np.pi, 0.0]) + else: + eu = np.array([np.arctan2((-P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]-qu[2]*qu[3])*chi ), + np.arctan2( 2.0*chi, q03-q12 ), + np.arctan2(( P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]+qu[2]*qu[3])*chi )]) + + # reduce Euler angles to definition range, i.e a lower limit of 0.0 + eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) + return eu + + +def qu2ax(qu): + """ + Quaternion to axis angle pair. + + Modified version of the original formulation, should be numerically more stable + """ + if iszero(qu[1]**2+qu[2]**2+qu[3]**2): # set axis to [001] if the angle is 0/360 + ax = [ 0.0, 0.0, 1.0, 0.0 ] + elif not iszero(qu[0]): + s = np.sign(qu[0])/np.sqrt(qu[1]**2+qu[2]**2+qu[3]**2) + omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) + ax = [ qu[1]*s, qu[2]*s, qu[3]*s, omega ] + else: + ax = [ qu[1], qu[2], qu[3], np.pi] + + return np.array(ax) + + +def qu2ro(qu): + """Quaternion to Rodriques-Frank vector.""" + if iszero(qu[0]): + ro = [qu[1], qu[2], qu[3], np.inf] + else: + s = np.linalg.norm([qu[1],qu[2],qu[3]]) + ro = [0.0,0.0,P,0.0] if iszero(s) else \ + [ qu[1]/s, qu[2]/s, qu[3]/s, np.tan(np.arccos(np.clip(qu[0],-1.0,1.0)))] # avoid numerical difficulties + + return np.array(ro) + + +def qu2ho(qu): + """Quaternion to homochoric vector.""" + omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) # avoid numerical difficulties + + if iszero(omega): + ho = np.array([ 0.0, 0.0, 0.0 ]) + else: + ho = np.array([qu[1], qu[2], qu[3]]) + f = 0.75 * ( omega - np.sin(omega) ) + ho = ho/np.linalg.norm(ho) * f**(1./3.) + + return ho + + +def qu2cu(qu): + """Quaternion to cubochoric vector.""" + return ho2cu(qu2ho(qu)) + + +#---------- Rotation matrix ---------- + +def om2qu(om): + """ + Rotation matrix to quaternion. + + The original formulation (direct conversion) had (numerical?) issues + """ + return eu2qu(om2eu(om)) + + +def om2eu(om): + """Rotation matrix to Bunge-Euler angles.""" + if abs(om[2,2]) < 1.0: + zeta = 1.0/np.sqrt(1.0-om[2,2]**2) + eu = np.array([np.arctan2(om[2,0]*zeta,-om[2,1]*zeta), + np.arccos(om[2,2]), + np.arctan2(om[0,2]*zeta, om[1,2]*zeta)]) + else: + eu = np.array([np.arctan2( om[0,1],om[0,0]), np.pi*0.5*(1-om[2,2]),0.0]) # following the paper, not the reference implementation + + # reduce Euler angles to definition range, i.e a lower limit of 0.0 + eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) + return eu + + +def om2ax(om): + """Rotation matrix to axis angle pair.""" + ax=np.empty(4) + + # first get the rotation angle + t = 0.5*(om.trace() -1.0) + ax[3] = np.arccos(np.clip(t,-1.0,1.0)) + + if iszero(ax[3]): + ax = [ 0.0, 0.0, 1.0, 0.0] + else: + w,vr = np.linalg.eig(om) + # next, find the eigenvalue (1,0j) + i = np.where(np.isclose(w,1.0+0.0j))[0][0] + ax[0:3] = np.real(vr[0:3,i]) + diagDelta = np.array([om[1,2]-om[2,1],om[2,0]-om[0,2],om[0,1]-om[1,0]]) + ax[0:3] = np.where(iszero(diagDelta), ax[0:3],np.abs(ax[0:3])*np.sign(-P*diagDelta)) + + return np.array(ax) + + +def om2ro(om): + """Rotation matrix to Rodriques-Frank vector.""" + return eu2ro(om2eu(om)) + + +def om2ho(om): + """Rotation matrix to homochoric vector.""" + return ax2ho(om2ax(om)) + + +def om2cu(om): + """Rotation matrix to cubochoric vector.""" + return ho2cu(om2ho(om)) + + +#---------- Bunge-Euler angles ---------- + +def eu2qu(eu): + """Bunge-Euler angles to quaternion.""" + ee = 0.5*eu + cPhi = np.cos(ee[1]) + sPhi = np.sin(ee[1]) + qu = np.array([ cPhi*np.cos(ee[0]+ee[2]), + -P*sPhi*np.cos(ee[0]-ee[2]), + -P*sPhi*np.sin(ee[0]-ee[2]), + -P*cPhi*np.sin(ee[0]+ee[2]) ]) + if qu[0] < 0.0: qu*=-1 + return qu + + +def eu2om(eu): + """Bunge-Euler angles to rotation matrix.""" + c = np.cos(eu) + s = np.sin(eu) + + om = np.array([[+c[0]*c[2]-s[0]*s[2]*c[1], +s[0]*c[2]+c[0]*s[2]*c[1], +s[2]*s[1]], + [-c[0]*s[2]-s[0]*c[2]*c[1], -s[0]*s[2]+c[0]*c[2]*c[1], +c[2]*s[1]], + [+s[0]*s[1], -c[0]*s[1], +c[1] ]]) + + om[np.where(iszero(om))] = 0.0 + return om + + +def eu2ax(eu): + """Bunge-Euler angles to axis angle pair.""" + t = np.tan(eu[1]*0.5) + sigma = 0.5*(eu[0]+eu[2]) + delta = 0.5*(eu[0]-eu[2]) + tau = np.linalg.norm([t,np.sin(sigma)]) + alpha = np.pi if iszero(np.cos(sigma)) else \ + 2.0*np.arctan(tau/np.cos(sigma)) + + if iszero(alpha): + ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) + else: + ax = -P/tau * np.array([ t*np.cos(delta), t*np.sin(delta), np.sin(sigma) ]) # passive axis angle pair so a minus sign in front + ax = np.append(ax,alpha) + if alpha < 0.0: ax *= -1.0 # ensure alpha is positive + + return ax + + +def eu2ro(eu): + """Bunge-Euler angles to Rodriques-Frank vector.""" + ro = eu2ax(eu) # convert to axis angle pair representation + if ro[3] >= np.pi: # Differs from original implementation. check convention 5 + ro[3] = np.inf + elif iszero(ro[3]): + ro = np.array([ 0.0, 0.0, P, 0.0 ]) + else: + ro[3] = np.tan(ro[3]*0.5) + + return ro + + +def eu2ho(eu): + """Bunge-Euler angles to homochoric vector.""" + return ax2ho(eu2ax(eu)) + + +def eu2cu(eu): + """Bunge-Euler angles to cubochoric vector.""" + return ho2cu(eu2ho(eu)) + + +#---------- Axis angle pair ---------- + +def ax2qu(ax): + """Axis angle pair to quaternion.""" + if iszero(ax[3]): + qu = np.array([ 1.0, 0.0, 0.0, 0.0 ]) + else: + c = np.cos(ax[3]*0.5) + s = np.sin(ax[3]*0.5) + qu = np.array([ c, ax[0]*s, ax[1]*s, ax[2]*s ]) + + return qu + + +def ax2om(ax): + """Axis angle pair to rotation matrix.""" + c = np.cos(ax[3]) + s = np.sin(ax[3]) + omc = 1.0-c + om=np.diag(ax[0:3]**2*omc + c) + + for idx in [[0,1,2],[1,2,0],[2,0,1]]: + q = omc*ax[idx[0]] * ax[idx[1]] + om[idx[0],idx[1]] = q + s*ax[idx[2]] + om[idx[1],idx[0]] = q - s*ax[idx[2]] + + return om if P < 0.0 else om.T + + +def ax2eu(ax): + """Rotation matrix to Bunge Euler angles.""" + return om2eu(ax2om(ax)) + + +def ax2ro(ax): + """Axis angle pair to Rodriques-Frank vector.""" + if iszero(ax[3]): + ro = [ 0.0, 0.0, P, 0.0 ] + else: + ro = [ax[0], ax[1], ax[2]] + # 180 degree case + ro += [np.inf] if np.isclose(ax[3],np.pi,atol=1.0e-15,rtol=0.0) else \ + [np.tan(ax[3]*0.5)] + + return np.array(ro) + + +def ax2ho(ax): + """Axis angle pair to homochoric vector.""" + f = (0.75 * ( ax[3] - np.sin(ax[3]) ))**(1.0/3.0) + ho = ax[0:3] * f + return ho + + +def ax2cu(ax): + """Axis angle pair to cubochoric vector.""" + return ho2cu(ax2ho(ax)) + + +#---------- Rodrigues-Frank vector ---------- + +def ro2qu(ro): + """Rodriques-Frank vector to quaternion.""" + return ax2qu(ro2ax(ro)) + + +def ro2om(ro): + """Rodgrigues-Frank vector to rotation matrix.""" + return ax2om(ro2ax(ro)) + + +def ro2eu(ro): + """Rodriques-Frank vector to Bunge-Euler angles.""" + return om2eu(ro2om(ro)) + + +def ro2ax(ro): + """Rodriques-Frank vector to axis angle pair.""" + ta = ro[3] + + if iszero(ta): + ax = [ 0.0, 0.0, 1.0, 0.0 ] + elif not np.isfinite(ta): + ax = [ ro[0], ro[1], ro[2], np.pi ] + else: + angle = 2.0*np.arctan(ta) + ta = 1.0/np.linalg.norm(ro[0:3]) + ax = [ ro[0]/ta, ro[1]/ta, ro[2]/ta, angle ] + + return np.array(ax) + + +def ro2ho(ro): + """Rodriques-Frank vector to homochoric vector.""" + if iszero(np.sum(ro[0:3]**2.0)): + ho = [ 0.0, 0.0, 0.0 ] + else: + f = 2.0*np.arctan(ro[3]) -np.sin(2.0*np.arctan(ro[3])) if np.isfinite(ro[3]) else np.pi + ho = ro[0:3] * (0.75*f)**(1.0/3.0) + + return np.array(ho) + + +def ro2cu(ro): + """Rodriques-Frank vector to cubochoric vector.""" + return ho2cu(ro2ho(ro)) + + +#---------- Homochoric vector---------- + +def ho2qu(ho): + """Homochoric vector to quaternion.""" + return ax2qu(ho2ax(ho)) + + +def ho2om(ho): + """Homochoric vector to rotation matrix.""" + return ax2om(ho2ax(ho)) + + +def ho2eu(ho): + """Homochoric vector to Bunge-Euler angles.""" + return ax2eu(ho2ax(ho)) + + +def ho2ax(ho): + """Homochoric vector to axis angle pair.""" + tfit = np.array([+1.0000000000018852, -0.5000000002194847, + -0.024999992127593126, -0.003928701544781374, + -0.0008152701535450438, -0.0002009500426119712, + -0.00002397986776071756, -0.00008202868926605841, + +0.00012448715042090092, -0.0001749114214822577, + +0.0001703481934140054, -0.00012062065004116828, + +0.000059719705868660826, -0.00001980756723965647, + +0.000003953714684212874, -0.00000036555001439719544]) + # normalize h and store the magnitude + hmag_squared = np.sum(ho**2.) + if iszero(hmag_squared): + ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) + else: + hm = hmag_squared + + # convert the magnitude to the rotation angle + s = tfit[0] + tfit[1] * hmag_squared + for i in range(2,16): + hm *= hmag_squared + s += tfit[i] * hm + ax = np.append(ho/np.sqrt(hmag_squared),2.0*np.arccos(np.clip(s,-1.0,1.0))) + return ax + + +def ho2ro(ho): + """Axis angle pair to Rodriques-Frank vector.""" + return ax2ro(ho2ax(ho)) + + +def ho2cu(ho): + """Homochoric vector to cubochoric vector.""" + return Lambert.BallToCube(ho) + + +#---------- Cubochoric ---------- + +def cu2qu(cu): + """Cubochoric vector to quaternion.""" + return ho2qu(cu2ho(cu)) + + +def cu2om(cu): + """Cubochoric vector to rotation matrix.""" + return ho2om(cu2ho(cu)) + + +def cu2eu(cu): + """Cubochoric vector to Bunge-Euler angles.""" + return ho2eu(cu2ho(cu)) + + +def cu2ax(cu): + """Cubochoric vector to axis angle pair.""" + return ho2ax(cu2ho(cu)) + + +def cu2ro(cu): + """Cubochoric vector to Rodriques-Frank vector.""" + return ho2ro(cu2ho(cu)) + + +def cu2ho(cu): + """Cubochoric vector to homochoric vector.""" + return Lambert.CubeToBall(cu) From 5ce143e36edffc3a354cb158c5bb07064fb0adcf Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Thu, 20 Feb 2020 23:17:54 +0100 Subject: [PATCH 04/13] numpy is already imported --- python/damask/rotation.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/python/damask/rotation.py b/python/damask/rotation.py index 8f2aaa8c9..aab2392bc 100644 --- a/python/damask/rotation.py +++ b/python/damask/rotation.py @@ -1,5 +1,3 @@ -import math - import numpy as np from . import Lambert @@ -580,19 +578,19 @@ class Symmetry: Rabs = abs(rodrigues) if self.lattice == 'cubic': - return math.sqrt(2.0)-1.0 >= Rabs[0] \ - and math.sqrt(2.0)-1.0 >= Rabs[1] \ - and math.sqrt(2.0)-1.0 >= Rabs[2] \ + return np.sqrt(2.0)-1.0 >= Rabs[0] \ + and np.sqrt(2.0)-1.0 >= Rabs[1] \ + and np.sqrt(2.0)-1.0 >= Rabs[2] \ and 1.0 >= Rabs[0] + Rabs[1] + Rabs[2] elif self.lattice == 'hexagonal': return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] \ - and 2.0 >= math.sqrt(3)*Rabs[0] + Rabs[1] \ - and 2.0 >= math.sqrt(3)*Rabs[1] + Rabs[0] \ - and 2.0 >= math.sqrt(3) + Rabs[2] + and 2.0 >= np.sqrt(3)*Rabs[0] + Rabs[1] \ + and 2.0 >= np.sqrt(3)*Rabs[1] + Rabs[0] \ + and 2.0 >= np.sqrt(3) + Rabs[2] elif self.lattice == 'tetragonal': return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] \ - and math.sqrt(2.0) >= Rabs[0] + Rabs[1] \ - and math.sqrt(2.0) >= Rabs[2] + 1.0 + and np.sqrt(2.0) >= Rabs[0] + Rabs[1] \ + and np.sqrt(2.0) >= Rabs[2] + 1.0 elif self.lattice == 'orthorhombic': return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] else: @@ -615,13 +613,13 @@ class Symmetry: epsilon = 0.0 if self.lattice == 'cubic': - return R[0] >= R[1]+epsilon and R[1] >= R[2]+epsilon and R[2] >= epsilon + return R[0] >= R[1]+epsilon and R[1] >= R[2]+epsilon and R[2] >= epsilon elif self.lattice == 'hexagonal': - return R[0] >= math.sqrt(3)*(R[1]-epsilon) and R[1] >= epsilon and R[2] >= epsilon + return R[0] >= np.sqrt(3)*(R[1]-epsilon) and R[1] >= epsilon and R[2] >= epsilon elif self.lattice == 'tetragonal': - return R[0] >= R[1]-epsilon and R[1] >= epsilon and R[2] >= epsilon + return R[0] >= R[1]-epsilon and R[1] >= epsilon and R[2] >= epsilon elif self.lattice == 'orthorhombic': - return R[0] >= epsilon and R[1] >= epsilon and R[2] >= epsilon + return R[0] >= epsilon and R[1] >= epsilon and R[2] >= epsilon else: return True From 1e1cb3f15170e5cd679ece9526c0d6da33351433 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Thu, 20 Feb 2020 23:29:12 +0100 Subject: [PATCH 05/13] avoid long modules --- python/damask/__init__.py | 5 +- python/damask/lattice.py | 642 +++++++++++++++++++++++++++++++++++ python/damask/orientation.py | 2 +- python/damask/rotation.py | 637 ---------------------------------- 4 files changed, 646 insertions(+), 640 deletions(-) create mode 100644 python/damask/lattice.py diff --git a/python/damask/__init__.py b/python/damask/__init__.py index b0f9b73df..fa0677d7b 100644 --- a/python/damask/__init__.py +++ b/python/damask/__init__.py @@ -13,8 +13,9 @@ from .asciitable import ASCIItable # noqa from .config import Material # noqa from .colormaps import Colormap, Color # noqa -from .rotation import Symmetry, Lattice, Rotation # noqa -from .orientation import Orientation # noqa +from .rotation import Rotation # noqa +from .lattice import Symmetry, Lattice # noqa +from .orientation import Orientation # noqa from .dadf5 import DADF5 # noqa from .geom import Geom # noqa diff --git a/python/damask/lattice.py b/python/damask/lattice.py new file mode 100644 index 000000000..fe3965547 --- /dev/null +++ b/python/damask/lattice.py @@ -0,0 +1,642 @@ +import numpy as np + +from .rotation import Rotation +from . import Lambert + +P = -1 + +# ****************************************************************************************** +class Symmetry: + """ + Symmetry operations for lattice systems. + + References + ---------- + https://en.wikipedia.org/wiki/Crystal_system + + """ + + lattices = [None,'orthorhombic','tetragonal','hexagonal','cubic',] + + def __init__(self, symmetry = None): + """ + Symmetry Definition. + + Parameters + ---------- + symmetry : str, optional + label of the crystal system + + """ + if symmetry is not None and symmetry.lower() not in Symmetry.lattices: + raise KeyError('Symmetry/crystal system "{}" is unknown'.format(symmetry)) + + self.lattice = symmetry.lower() if isinstance(symmetry,str) else symmetry + + + def __copy__(self): + """Copy.""" + return self.__class__(self.lattice) + + copy = __copy__ + + + def __repr__(self): + """Readable string.""" + return '{}'.format(self.lattice) + + + def __eq__(self, other): + """ + Equal to other. + + Parameters + ---------- + other : Symmetry + Symmetry to check for equality. + + """ + return self.lattice == other.lattice + + def __neq__(self, other): + """ + Not Equal to other. + + Parameters + ---------- + other : Symmetry + Symmetry to check for inequality. + + """ + return not self.__eq__(other) + + def __cmp__(self,other): + """ + Linear ordering. + + Parameters + ---------- + other : Symmetry + Symmetry to check for for order. + + """ + myOrder = Symmetry.lattices.index(self.lattice) + otherOrder = Symmetry.lattices.index(other.lattice) + return (myOrder > otherOrder) - (myOrder < otherOrder) + + def symmetryOperations(self,members=[]): + """List (or single element) of symmetry operations as rotations.""" + if self.lattice == 'cubic': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, 0.0, 0.0, 1.0 ], + [ 0.0, 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2) ], + [ 0.0, 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2) ], + [ 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], + [ 0.0, -0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], + [ 0.5, 0.5, 0.5, 0.5 ], + [-0.5, 0.5, 0.5, 0.5 ], + [-0.5, 0.5, 0.5, -0.5 ], + [-0.5, 0.5, -0.5, 0.5 ], + [-0.5, -0.5, 0.5, 0.5 ], + [-0.5, -0.5, 0.5, -0.5 ], + [-0.5, -0.5, -0.5, 0.5 ], + [-0.5, 0.5, -0.5, -0.5 ], + [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [-0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2), 0.0 ], + [-0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2), 0.0 ], + [-0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0, 0.0 ], + [-0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0, 0.0 ], + ] + elif self.lattice == 'hexagonal': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [-0.5*np.sqrt(3), 0.0, 0.0, -0.5 ], + [ 0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], + [ 0.0, 0.0, 0.0, 1.0 ], + [-0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], + [-0.5*np.sqrt(3), 0.0, 0.0, 0.5 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, -0.5*np.sqrt(3), 0.5, 0.0 ], + [ 0.0, 0.5, -0.5*np.sqrt(3), 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, -0.5, -0.5*np.sqrt(3), 0.0 ], + [ 0.0, 0.5*np.sqrt(3), 0.5, 0.0 ], + ] + elif self.lattice == 'tetragonal': + symQuats = [ + [ 1.0, 0.0, 0.0, 0.0 ], + [ 0.0, 1.0, 0.0, 0.0 ], + [ 0.0, 0.0, 1.0, 0.0 ], + [ 0.0, 0.0, 0.0, 1.0 ], + [ 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], + [ 0.0, -0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], + [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], + ] + elif self.lattice == 'orthorhombic': + symQuats = [ + [ 1.0,0.0,0.0,0.0 ], + [ 0.0,1.0,0.0,0.0 ], + [ 0.0,0.0,1.0,0.0 ], + [ 0.0,0.0,0.0,1.0 ], + ] + else: + symQuats = [ + [ 1.0,0.0,0.0,0.0 ], + ] + + symOps = list(map(Rotation, + np.array(symQuats)[np.atleast_1d(members) if members != [] else range(len(symQuats))])) + try: + iter(members) # asking for (even empty) list of members? + except TypeError: + return symOps[0] # no, return rotation object + else: + return symOps # yes, return list of rotations + + + def inFZ(self,rodrigues): + """ + Check whether given Rodriques-Frank vector falls into fundamental zone of own symmetry. + + Fundamental zone in Rodrigues space is point symmetric around origin. + """ + if (len(rodrigues) != 3): + raise ValueError('Input is not a Rodriques-Frank vector.\n') + + if np.any(rodrigues == np.inf): return False + + Rabs = abs(rodrigues) + + if self.lattice == 'cubic': + return np.sqrt(2.0)-1.0 >= Rabs[0] \ + and np.sqrt(2.0)-1.0 >= Rabs[1] \ + and np.sqrt(2.0)-1.0 >= Rabs[2] \ + and 1.0 >= Rabs[0] + Rabs[1] + Rabs[2] + elif self.lattice == 'hexagonal': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] \ + and 2.0 >= np.sqrt(3)*Rabs[0] + Rabs[1] \ + and 2.0 >= np.sqrt(3)*Rabs[1] + Rabs[0] \ + and 2.0 >= np.sqrt(3) + Rabs[2] + elif self.lattice == 'tetragonal': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] \ + and np.sqrt(2.0) >= Rabs[0] + Rabs[1] \ + and np.sqrt(2.0) >= Rabs[2] + 1.0 + elif self.lattice == 'orthorhombic': + return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] + else: + return True + + + def inDisorientationSST(self,rodrigues): + """ + Check whether given Rodriques-Frank vector (of misorientation) falls into standard stereographic triangle of own symmetry. + + References + ---------- + A. Heinz and P. Neumann, Acta Crystallographica Section A 47:780-789, 1991 + https://doi.org/10.1107/S0108767391006864 + + """ + if (len(rodrigues) != 3): + raise ValueError('Input is not a Rodriques-Frank vector.\n') + R = rodrigues + + epsilon = 0.0 + if self.lattice == 'cubic': + return R[0] >= R[1]+epsilon and R[1] >= R[2]+epsilon and R[2] >= epsilon + elif self.lattice == 'hexagonal': + return R[0] >= np.sqrt(3)*(R[1]-epsilon) and R[1] >= epsilon and R[2] >= epsilon + elif self.lattice == 'tetragonal': + return R[0] >= R[1]-epsilon and R[1] >= epsilon and R[2] >= epsilon + elif self.lattice == 'orthorhombic': + return R[0] >= epsilon and R[1] >= epsilon and R[2] >= epsilon + else: + return True + + + def inSST(self, + vector, + proper = False, + color = False): + """ + Check whether given vector falls into standard stereographic triangle of own symmetry. + + proper considers only vectors with z >= 0, hence uses two neighboring SSTs. + Return inverse pole figure color if requested. + Bases are computed from + + basis = {'cubic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,1.]/np.sqrt(2.), # direction of green + [1.,1.,1.]/np.sqrt(3.)]).T), # direction of blue + 'hexagonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [np.sqrt(3.),1.,0.]/np.sqrt(4.)]).T), # direction of blue + 'tetragonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [1.,1.,0.]/np.sqrt(2.)]).T), # direction of blue + 'orthorhombic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red + [1.,0.,0.], # direction of green + [0.,1.,0.]]).T), # direction of blue + } + """ + if self.lattice == 'cubic': + basis = {'improper':np.array([ [-1. , 0. , 1. ], + [ np.sqrt(2.) , -np.sqrt(2.) , 0. ], + [ 0. , np.sqrt(3.) , 0. ] ]), + 'proper':np.array([ [ 0. , -1. , 1. ], + [-np.sqrt(2.) , np.sqrt(2.) , 0. ], + [ np.sqrt(3.) , 0. , 0. ] ]), + } + elif self.lattice == 'hexagonal': + basis = {'improper':np.array([ [ 0. , 0. , 1. ], + [ 1. , -np.sqrt(3.) , 0. ], + [ 0. , 2. , 0. ] ]), + 'proper':np.array([ [ 0. , 0. , 1. ], + [-1. , np.sqrt(3.) , 0. ], + [ np.sqrt(3.) , -1. , 0. ] ]), + } + elif self.lattice == 'tetragonal': + basis = {'improper':np.array([ [ 0. , 0. , 1. ], + [ 1. , -1. , 0. ], + [ 0. , np.sqrt(2.) , 0. ] ]), + 'proper':np.array([ [ 0. , 0. , 1. ], + [-1. , 1. , 0. ], + [ np.sqrt(2.) , 0. , 0. ] ]), + } + elif self.lattice == 'orthorhombic': + basis = {'improper':np.array([ [ 0., 0., 1.], + [ 1., 0., 0.], + [ 0., 1., 0.] ]), + 'proper':np.array([ [ 0., 0., 1.], + [-1., 0., 0.], + [ 0., 1., 0.] ]), + } + else: # direct exit for unspecified symmetry + if color: + return (True,np.zeros(3,'d')) + else: + return True + + v = np.array(vector,dtype=float) + if proper: # check both improper ... + theComponents = np.around(np.dot(basis['improper'],v),12) + inSST = np.all(theComponents >= 0.0) + if not inSST: # ... and proper SST + theComponents = np.around(np.dot(basis['proper'],v),12) + inSST = np.all(theComponents >= 0.0) + else: + v[2] = abs(v[2]) # z component projects identical + theComponents = np.around(np.dot(basis['improper'],v),12) # for positive and negative values + inSST = np.all(theComponents >= 0.0) + + if color: # have to return color array + if inSST: + rgb = np.power(theComponents/np.linalg.norm(theComponents),0.5) # smoothen color ramps + rgb = np.minimum(np.ones(3,dtype=float),rgb) # limit to maximum intensity + rgb /= max(rgb) # normalize to (HS)V = 1 + else: + rgb = np.zeros(3,dtype=float) + return (inSST,rgb) + else: + return inSST + +# code derived from https://github.com/ezag/pyeuclid +# suggested reading: http://web.mit.edu/2.998/www/QuaternionReport1.pdf + + +# ****************************************************************************************** +class Lattice: + """ + Lattice system. + + Currently, this contains only a mapping from Bravais lattice to symmetry + and orientation relationships. It could include twin and slip systems. + + References + ---------- + https://en.wikipedia.org/wiki/Bravais_lattice + + """ + + lattices = { + 'triclinic':{'symmetry':None}, + 'bct':{'symmetry':'tetragonal'}, + 'hex':{'symmetry':'hexagonal'}, + 'fcc':{'symmetry':'cubic','c/a':1.0}, + 'bcc':{'symmetry':'cubic','c/a':1.0}, + } + + + def __init__(self, lattice): + """ + New lattice of given type. + + Parameters + ---------- + lattice : str + Bravais lattice. + + """ + self.lattice = lattice + self.symmetry = Symmetry(self.lattices[lattice]['symmetry']) + + + def __repr__(self): + """Report basic lattice information.""" + return 'Bravais lattice {} ({} symmetry)'.format(self.lattice,self.symmetry) + + + # Kurdjomov--Sachs orientation relationship for fcc <-> bcc transformation + # from S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 + # also see K. Kitahara et al., Acta Materialia 54:1279-1288, 2006 + KS = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]]],dtype='float'), + 'directions': np.array([ + [[ -1, 0, 1],[ -1, -1, 1]], + [[ -1, 0, 1],[ -1, 1, -1]], + [[ 0, 1, -1],[ -1, -1, 1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ 1, -1, 0],[ -1, -1, 1]], + [[ 1, -1, 0],[ -1, 1, -1]], + [[ 1, 0, -1],[ -1, -1, 1]], + [[ 1, 0, -1],[ -1, 1, -1]], + [[ -1, -1, 0],[ -1, -1, 1]], + [[ -1, -1, 0],[ -1, 1, -1]], + [[ 0, 1, 1],[ -1, -1, 1]], + [[ 0, 1, 1],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ 0, -1, 1],[ -1, 1, -1]], + [[ -1, 0, -1],[ -1, -1, 1]], + [[ -1, 0, -1],[ -1, 1, -1]], + [[ 1, 1, 0],[ -1, -1, 1]], + [[ 1, 1, 0],[ -1, 1, -1]], + [[ -1, 1, 0],[ -1, -1, 1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, -1],[ -1, -1, 1]], + [[ 0, -1, -1],[ -1, 1, -1]], + [[ 1, 0, 1],[ -1, -1, 1]], + [[ 1, 0, 1],[ -1, 1, -1]]],dtype='float')} + + # Greninger--Troiano orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + GT = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 1, 0, 1]], + [[ 1, 1, 1],[ 1, 1, 0]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ -1, 0, 1]], + [[ -1, -1, 1],[ -1, -1, 0]], + [[ -1, -1, 1],[ 0, -1, 1]], + [[ -1, 1, 1],[ -1, 0, 1]], + [[ -1, 1, 1],[ -1, 1, 0]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 1, 0, 1]], + [[ 1, -1, 1],[ 1, -1, 0]], + [[ 1, -1, 1],[ 0, -1, 1]], + [[ 1, 1, 1],[ 1, 1, 0]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 1, 0, 1]], + [[ -1, -1, 1],[ -1, -1, 0]], + [[ -1, -1, 1],[ 0, -1, 1]], + [[ -1, -1, 1],[ -1, 0, 1]], + [[ -1, 1, 1],[ -1, 1, 0]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ -1, 0, 1]], + [[ 1, -1, 1],[ 1, -1, 0]], + [[ 1, -1, 1],[ 0, -1, 1]], + [[ 1, -1, 1],[ 1, 0, 1]]],dtype='float'), + 'directions': np.array([ + [[ -5,-12, 17],[-17, -7, 17]], + [[ 17, -5,-12],[ 17,-17, -7]], + [[-12, 17, -5],[ -7, 17,-17]], + [[ 5, 12, 17],[ 17, 7, 17]], + [[-17, 5,-12],[-17, 17, -7]], + [[ 12,-17, -5],[ 7,-17,-17]], + [[ -5, 12,-17],[-17, 7,-17]], + [[ 17, 5, 12],[ 17, 17, 7]], + [[-12,-17, 5],[ -7,-17, 17]], + [[ 5,-12,-17],[ 17, -7,-17]], + [[-17, -5, 12],[-17,-17, 7]], + [[ 12, 17, 5],[ 7, 17, 17]], + [[ -5, 17,-12],[-17, 17, -7]], + [[-12, -5, 17],[ -7,-17, 17]], + [[ 17,-12, -5],[ 17, -7,-17]], + [[ 5,-17,-12],[ 17,-17, -7]], + [[ 12, 5, 17],[ 7, 17, 17]], + [[-17, 12, -5],[-17, 7,-17]], + [[ -5,-17, 12],[-17,-17, 7]], + [[-12, 5,-17],[ -7, 17,-17]], + [[ 17, 12, 5],[ 17, 7, 17]], + [[ 5, 17, 12],[ 17, 17, 7]], + [[ 12, -5,-17],[ 7,-17,-17]], + [[-17,-12, 5],[-17,-7, 17]]],dtype='float')} + + # Greninger--Troiano' orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + GTprime = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 7, 17, 17],[ 12, 5, 17]], + [[ 17, 7, 17],[ 17, 12, 5]], + [[ 17, 17, 7],[ 5, 17, 12]], + [[ -7,-17, 17],[-12, -5, 17]], + [[-17, -7, 17],[-17,-12, 5]], + [[-17,-17, 7],[ -5,-17, 12]], + [[ 7,-17,-17],[ 12, -5,-17]], + [[ 17, -7,-17],[ 17,-12, -5]], + [[ 17,-17, -7],[ 5,-17,-12]], + [[ -7, 17,-17],[-12, 5,-17]], + [[-17, 7,-17],[-17, 12, -5]], + [[-17, 17, -7],[ -5, 17,-12]], + [[ 7, 17, 17],[ 12, 17, 5]], + [[ 17, 7, 17],[ 5, 12, 17]], + [[ 17, 17, 7],[ 17, 5, 12]], + [[ -7,-17, 17],[-12,-17, 5]], + [[-17, -7, 17],[ -5,-12, 17]], + [[-17,-17, 7],[-17, -5, 12]], + [[ 7,-17,-17],[ 12,-17, -5]], + [[ 17, -7,-17],[ 5, -12,-17]], + [[ 17,-17, -7],[ 17, -5,-12]], + [[ -7, 17,-17],[-12, 17, -5]], + [[-17, 7,-17],[ -5, 12,-17]], + [[-17, 17, -7],[-17, 5,-12]]],dtype='float'), + 'directions': np.array([ + [[ 0, 1, -1],[ 1, 1, -1]], + [[ -1, 0, 1],[ -1, 1, 1]], + [[ 1, -1, 0],[ 1, -1, 1]], + [[ 0, -1, -1],[ -1, -1, -1]], + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, -1, 0],[ 1, -1, -1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ 1, 0, 1],[ 1, 1, 1]], + [[ -1, -1, 0],[ -1, -1, 1]], + [[ 0, -1, -1],[ 1, -1, -1]], + [[ -1, 0, 1],[ -1, -1, 1]], + [[ -1, -1, 0],[ -1, -1, -1]], + [[ 0, -1, 1],[ 1, -1, 1]], + [[ 1, 0, -1],[ 1, 1, -1]], + [[ -1, 1, 0],[ -1, 1, 1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ -1, 0, -1],[ -1, -1, -1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ -1, 0, -1],[ -1, 1, -1]], + [[ 1, 1, 0],[ 1, 1, 1]], + [[ 0, 1, 1],[ 1, 1, 1]], + [[ 1, 0, -1],[ 1, -1, -1]], + [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} + + # Nishiyama--Wassermann orientation relationship for fcc <-> bcc transformation + # from H. Kitahara et al., Materials Characterization 54:378-386, 2005 + NW = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]]],dtype='float'), + 'directions': np.array([ + [[ 2, -1, -1],[ 0, -1, 1]], + [[ -1, 2, -1],[ 0, -1, 1]], + [[ -1, -1, 2],[ 0, -1, 1]], + [[ -2, -1, -1],[ 0, -1, 1]], + [[ 1, 2, -1],[ 0, -1, 1]], + [[ 1, -1, 2],[ 0, -1, 1]], + [[ 2, 1, -1],[ 0, -1, 1]], + [[ -1, -2, -1],[ 0, -1, 1]], + [[ -1, 1, 2],[ 0, -1, 1]], + [[ 2, -1, 1],[ 0, -1, 1]], #It is wrong in the paper, but matrix is correct + [[ -1, 2, 1],[ 0, -1, 1]], + [[ -1, -1, -2],[ 0, -1, 1]]],dtype='float')} + + # Pitsch orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Acta Materialia 53:1179-1190, 2005 + Pitsch = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 0, 1, 0],[ -1, 0, 1]], + [[ 0, 0, 1],[ 1, -1, 0]], + [[ 1, 0, 0],[ 0, 1, -1]], + [[ 1, 0, 0],[ 0, -1, -1]], + [[ 0, 1, 0],[ -1, 0, -1]], + [[ 0, 0, 1],[ -1, -1, 0]], + [[ 0, 1, 0],[ -1, 0, -1]], + [[ 0, 0, 1],[ -1, -1, 0]], + [[ 1, 0, 0],[ 0, -1, -1]], + [[ 1, 0, 0],[ 0, -1, 1]], + [[ 0, 1, 0],[ 1, 0, -1]], + [[ 0, 0, 1],[ -1, 1, 0]]],dtype='float'), + 'directions': np.array([ + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, 1, 0],[ 1, 1, -1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ -1, 0, 1],[ -1, -1, 1]], + [[ 1, -1, 0],[ 1, -1, -1]], + [[ 1, 0, -1],[ 1, -1, -1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} + + # Bain orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + Bain = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 0, 0],[ 1, 0, 0]], + [[ 0, 1, 0],[ 0, 1, 0]], + [[ 0, 0, 1],[ 0, 0, 1]]],dtype='float'), + 'directions': np.array([ + [[ 0, 1, 0],[ 0, 1, 1]], + [[ 0, 0, 1],[ 1, 0, 1]], + [[ 1, 0, 0],[ 1, 1, 0]]],dtype='float')} + + def relationOperations(self,model): + """ + Crystallographic orientation relationships for phase transformations. + + References + ---------- + S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 + https://doi.org/10.1016/j.jallcom.2012.02.004 + + K. Kitahara et al., Acta Materialia 54(5):1279-1288, 2006 + https://doi.org/10.1016/j.actamat.2005.11.001 + + Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + https://doi.org/10.1107/S0021889805038276 + + H. Kitahara et al., Materials Characterization 54(4-5):378-386, 2005 + https://doi.org/10.1016/j.matchar.2004.12.015 + + Y. He et al., Acta Materialia 53(4):1179-1190, 2005 + https://doi.org/10.1016/j.actamat.2004.11.021 + + """ + models={'KS':self.KS, 'GT':self.GT, 'GT_prime':self.GTprime, + 'NW':self.NW, 'Pitsch': self.Pitsch, 'Bain':self.Bain} + try: + relationship = models[model] + except KeyError : + raise KeyError('Orientation relationship "{}" is unknown'.format(model)) + + if self.lattice not in relationship['mapping']: + raise ValueError('Relationship "{}" not supported for lattice "{}"'.format(model,self.lattice)) + + r = {'lattice':Lattice((set(relationship['mapping'])-{self.lattice}).pop()), # target lattice + 'rotations':[] } + + myPlane_id = relationship['mapping'][self.lattice] + otherPlane_id = (myPlane_id+1)%2 + myDir_id = myPlane_id +2 + otherDir_id = otherPlane_id +2 + + for miller in np.hstack((relationship['planes'],relationship['directions'])): + myPlane = miller[myPlane_id]/ np.linalg.norm(miller[myPlane_id]) + myDir = miller[myDir_id]/ np.linalg.norm(miller[myDir_id]) + myMatrix = np.array([myDir,np.cross(myPlane,myDir),myPlane]) + + otherPlane = miller[otherPlane_id]/ np.linalg.norm(miller[otherPlane_id]) + otherDir = miller[otherDir_id]/ np.linalg.norm(miller[otherDir_id]) + otherMatrix = np.array([otherDir,np.cross(otherPlane,otherDir),otherPlane]) + + r['rotations'].append(Rotation.fromMatrix(np.dot(otherMatrix.T,myMatrix))) + + return r diff --git a/python/damask/orientation.py b/python/damask/orientation.py index 7f617e8df..55a58959c 100644 --- a/python/damask/orientation.py +++ b/python/damask/orientation.py @@ -1,6 +1,6 @@ import numpy as np -from .rotation import Lattice +from .lattice import Lattice from .rotation import Rotation class Orientation: diff --git a/python/damask/rotation.py b/python/damask/rotation.py index aab2392bc..1f8962105 100644 --- a/python/damask/rotation.py +++ b/python/damask/rotation.py @@ -407,643 +407,6 @@ class Rotation: np.sin(2.0*np.pi*r[0])*A])).standardize() - -# ****************************************************************************************** -class Symmetry: - """ - Symmetry operations for lattice systems. - - References - ---------- - https://en.wikipedia.org/wiki/Crystal_system - - """ - - lattices = [None,'orthorhombic','tetragonal','hexagonal','cubic',] - - def __init__(self, symmetry = None): - """ - Symmetry Definition. - - Parameters - ---------- - symmetry : str, optional - label of the crystal system - - """ - if symmetry is not None and symmetry.lower() not in Symmetry.lattices: - raise KeyError('Symmetry/crystal system "{}" is unknown'.format(symmetry)) - - self.lattice = symmetry.lower() if isinstance(symmetry,str) else symmetry - - - def __copy__(self): - """Copy.""" - return self.__class__(self.lattice) - - copy = __copy__ - - - def __repr__(self): - """Readable string.""" - return '{}'.format(self.lattice) - - - def __eq__(self, other): - """ - Equal to other. - - Parameters - ---------- - other : Symmetry - Symmetry to check for equality. - - """ - return self.lattice == other.lattice - - def __neq__(self, other): - """ - Not Equal to other. - - Parameters - ---------- - other : Symmetry - Symmetry to check for inequality. - - """ - return not self.__eq__(other) - - def __cmp__(self,other): - """ - Linear ordering. - - Parameters - ---------- - other : Symmetry - Symmetry to check for for order. - - """ - myOrder = Symmetry.lattices.index(self.lattice) - otherOrder = Symmetry.lattices.index(other.lattice) - return (myOrder > otherOrder) - (myOrder < otherOrder) - - def symmetryOperations(self,members=[]): - """List (or single element) of symmetry operations as rotations.""" - if self.lattice == 'cubic': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, 0.0, 0.0, 1.0 ], - [ 0.0, 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2) ], - [ 0.0, 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2) ], - [ 0.0, 0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], - [ 0.0, -0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0 ], - [ 0.5, 0.5, 0.5, 0.5 ], - [-0.5, 0.5, 0.5, 0.5 ], - [-0.5, 0.5, 0.5, -0.5 ], - [-0.5, 0.5, -0.5, 0.5 ], - [-0.5, -0.5, 0.5, 0.5 ], - [-0.5, -0.5, 0.5, -0.5 ], - [-0.5, -0.5, -0.5, 0.5 ], - [-0.5, 0.5, -0.5, -0.5 ], - [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [-0.5*np.sqrt(2), 0.0, 0.5*np.sqrt(2), 0.0 ], - [-0.5*np.sqrt(2), 0.0, -0.5*np.sqrt(2), 0.0 ], - [-0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0, 0.0 ], - [-0.5*np.sqrt(2),-0.5*np.sqrt(2), 0.0, 0.0 ], - ] - elif self.lattice == 'hexagonal': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [-0.5*np.sqrt(3), 0.0, 0.0, -0.5 ], - [ 0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], - [ 0.0, 0.0, 0.0, 1.0 ], - [-0.5, 0.0, 0.0, 0.5*np.sqrt(3) ], - [-0.5*np.sqrt(3), 0.0, 0.0, 0.5 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, -0.5*np.sqrt(3), 0.5, 0.0 ], - [ 0.0, 0.5, -0.5*np.sqrt(3), 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, -0.5, -0.5*np.sqrt(3), 0.0 ], - [ 0.0, 0.5*np.sqrt(3), 0.5, 0.0 ], - ] - elif self.lattice == 'tetragonal': - symQuats = [ - [ 1.0, 0.0, 0.0, 0.0 ], - [ 0.0, 1.0, 0.0, 0.0 ], - [ 0.0, 0.0, 1.0, 0.0 ], - [ 0.0, 0.0, 0.0, 1.0 ], - [ 0.0, 0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], - [ 0.0, -0.5*np.sqrt(2), 0.5*np.sqrt(2), 0.0 ], - [ 0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - [-0.5*np.sqrt(2), 0.0, 0.0, 0.5*np.sqrt(2) ], - ] - elif self.lattice == 'orthorhombic': - symQuats = [ - [ 1.0,0.0,0.0,0.0 ], - [ 0.0,1.0,0.0,0.0 ], - [ 0.0,0.0,1.0,0.0 ], - [ 0.0,0.0,0.0,1.0 ], - ] - else: - symQuats = [ - [ 1.0,0.0,0.0,0.0 ], - ] - - symOps = list(map(Rotation, - np.array(symQuats)[np.atleast_1d(members) if members != [] else range(len(symQuats))])) - try: - iter(members) # asking for (even empty) list of members? - except TypeError: - return symOps[0] # no, return rotation object - else: - return symOps # yes, return list of rotations - - - def inFZ(self,rodrigues): - """ - Check whether given Rodriques-Frank vector falls into fundamental zone of own symmetry. - - Fundamental zone in Rodrigues space is point symmetric around origin. - """ - if (len(rodrigues) != 3): - raise ValueError('Input is not a Rodriques-Frank vector.\n') - - if np.any(rodrigues == np.inf): return False - - Rabs = abs(rodrigues) - - if self.lattice == 'cubic': - return np.sqrt(2.0)-1.0 >= Rabs[0] \ - and np.sqrt(2.0)-1.0 >= Rabs[1] \ - and np.sqrt(2.0)-1.0 >= Rabs[2] \ - and 1.0 >= Rabs[0] + Rabs[1] + Rabs[2] - elif self.lattice == 'hexagonal': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] \ - and 2.0 >= np.sqrt(3)*Rabs[0] + Rabs[1] \ - and 2.0 >= np.sqrt(3)*Rabs[1] + Rabs[0] \ - and 2.0 >= np.sqrt(3) + Rabs[2] - elif self.lattice == 'tetragonal': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] \ - and np.sqrt(2.0) >= Rabs[0] + Rabs[1] \ - and np.sqrt(2.0) >= Rabs[2] + 1.0 - elif self.lattice == 'orthorhombic': - return 1.0 >= Rabs[0] and 1.0 >= Rabs[1] and 1.0 >= Rabs[2] - else: - return True - - - def inDisorientationSST(self,rodrigues): - """ - Check whether given Rodriques-Frank vector (of misorientation) falls into standard stereographic triangle of own symmetry. - - References - ---------- - A. Heinz and P. Neumann, Acta Crystallographica Section A 47:780-789, 1991 - https://doi.org/10.1107/S0108767391006864 - - """ - if (len(rodrigues) != 3): - raise ValueError('Input is not a Rodriques-Frank vector.\n') - R = rodrigues - - epsilon = 0.0 - if self.lattice == 'cubic': - return R[0] >= R[1]+epsilon and R[1] >= R[2]+epsilon and R[2] >= epsilon - elif self.lattice == 'hexagonal': - return R[0] >= np.sqrt(3)*(R[1]-epsilon) and R[1] >= epsilon and R[2] >= epsilon - elif self.lattice == 'tetragonal': - return R[0] >= R[1]-epsilon and R[1] >= epsilon and R[2] >= epsilon - elif self.lattice == 'orthorhombic': - return R[0] >= epsilon and R[1] >= epsilon and R[2] >= epsilon - else: - return True - - - def inSST(self, - vector, - proper = False, - color = False): - """ - Check whether given vector falls into standard stereographic triangle of own symmetry. - - proper considers only vectors with z >= 0, hence uses two neighboring SSTs. - Return inverse pole figure color if requested. - Bases are computed from - - basis = {'cubic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,1.]/np.sqrt(2.), # direction of green - [1.,1.,1.]/np.sqrt(3.)]).T), # direction of blue - 'hexagonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [np.sqrt(3.),1.,0.]/np.sqrt(4.)]).T), # direction of blue - 'tetragonal' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [1.,1.,0.]/np.sqrt(2.)]).T), # direction of blue - 'orthorhombic' : np.linalg.inv(np.array([[0.,0.,1.], # direction of red - [1.,0.,0.], # direction of green - [0.,1.,0.]]).T), # direction of blue - } - """ - if self.lattice == 'cubic': - basis = {'improper':np.array([ [-1. , 0. , 1. ], - [ np.sqrt(2.) , -np.sqrt(2.) , 0. ], - [ 0. , np.sqrt(3.) , 0. ] ]), - 'proper':np.array([ [ 0. , -1. , 1. ], - [-np.sqrt(2.) , np.sqrt(2.) , 0. ], - [ np.sqrt(3.) , 0. , 0. ] ]), - } - elif self.lattice == 'hexagonal': - basis = {'improper':np.array([ [ 0. , 0. , 1. ], - [ 1. , -np.sqrt(3.) , 0. ], - [ 0. , 2. , 0. ] ]), - 'proper':np.array([ [ 0. , 0. , 1. ], - [-1. , np.sqrt(3.) , 0. ], - [ np.sqrt(3.) , -1. , 0. ] ]), - } - elif self.lattice == 'tetragonal': - basis = {'improper':np.array([ [ 0. , 0. , 1. ], - [ 1. , -1. , 0. ], - [ 0. , np.sqrt(2.) , 0. ] ]), - 'proper':np.array([ [ 0. , 0. , 1. ], - [-1. , 1. , 0. ], - [ np.sqrt(2.) , 0. , 0. ] ]), - } - elif self.lattice == 'orthorhombic': - basis = {'improper':np.array([ [ 0., 0., 1.], - [ 1., 0., 0.], - [ 0., 1., 0.] ]), - 'proper':np.array([ [ 0., 0., 1.], - [-1., 0., 0.], - [ 0., 1., 0.] ]), - } - else: # direct exit for unspecified symmetry - if color: - return (True,np.zeros(3,'d')) - else: - return True - - v = np.array(vector,dtype=float) - if proper: # check both improper ... - theComponents = np.around(np.dot(basis['improper'],v),12) - inSST = np.all(theComponents >= 0.0) - if not inSST: # ... and proper SST - theComponents = np.around(np.dot(basis['proper'],v),12) - inSST = np.all(theComponents >= 0.0) - else: - v[2] = abs(v[2]) # z component projects identical - theComponents = np.around(np.dot(basis['improper'],v),12) # for positive and negative values - inSST = np.all(theComponents >= 0.0) - - if color: # have to return color array - if inSST: - rgb = np.power(theComponents/np.linalg.norm(theComponents),0.5) # smoothen color ramps - rgb = np.minimum(np.ones(3,dtype=float),rgb) # limit to maximum intensity - rgb /= max(rgb) # normalize to (HS)V = 1 - else: - rgb = np.zeros(3,dtype=float) - return (inSST,rgb) - else: - return inSST - -# code derived from https://github.com/ezag/pyeuclid -# suggested reading: http://web.mit.edu/2.998/www/QuaternionReport1.pdf - - -# ****************************************************************************************** -class Lattice: - """ - Lattice system. - - Currently, this contains only a mapping from Bravais lattice to symmetry - and orientation relationships. It could include twin and slip systems. - - References - ---------- - https://en.wikipedia.org/wiki/Bravais_lattice - - """ - - lattices = { - 'triclinic':{'symmetry':None}, - 'bct':{'symmetry':'tetragonal'}, - 'hex':{'symmetry':'hexagonal'}, - 'fcc':{'symmetry':'cubic','c/a':1.0}, - 'bcc':{'symmetry':'cubic','c/a':1.0}, - } - - - def __init__(self, lattice): - """ - New lattice of given type. - - Parameters - ---------- - lattice : str - Bravais lattice. - - """ - self.lattice = lattice - self.symmetry = Symmetry(self.lattices[lattice]['symmetry']) - - - def __repr__(self): - """Report basic lattice information.""" - return 'Bravais lattice {} ({} symmetry)'.format(self.lattice,self.symmetry) - - - # Kurdjomov--Sachs orientation relationship for fcc <-> bcc transformation - # from S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 - # also see K. Kitahara et al., Acta Materialia 54:1279-1288, 2006 - KS = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]]],dtype='float'), - 'directions': np.array([ - [[ -1, 0, 1],[ -1, -1, 1]], - [[ -1, 0, 1],[ -1, 1, -1]], - [[ 0, 1, -1],[ -1, -1, 1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ 1, -1, 0],[ -1, -1, 1]], - [[ 1, -1, 0],[ -1, 1, -1]], - [[ 1, 0, -1],[ -1, -1, 1]], - [[ 1, 0, -1],[ -1, 1, -1]], - [[ -1, -1, 0],[ -1, -1, 1]], - [[ -1, -1, 0],[ -1, 1, -1]], - [[ 0, 1, 1],[ -1, -1, 1]], - [[ 0, 1, 1],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ 0, -1, 1],[ -1, 1, -1]], - [[ -1, 0, -1],[ -1, -1, 1]], - [[ -1, 0, -1],[ -1, 1, -1]], - [[ 1, 1, 0],[ -1, -1, 1]], - [[ 1, 1, 0],[ -1, 1, -1]], - [[ -1, 1, 0],[ -1, -1, 1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, -1],[ -1, -1, 1]], - [[ 0, -1, -1],[ -1, 1, -1]], - [[ 1, 0, 1],[ -1, -1, 1]], - [[ 1, 0, 1],[ -1, 1, -1]]],dtype='float')} - - # Greninger--Troiano orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - GT = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 1, 0, 1]], - [[ 1, 1, 1],[ 1, 1, 0]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ -1, 0, 1]], - [[ -1, -1, 1],[ -1, -1, 0]], - [[ -1, -1, 1],[ 0, -1, 1]], - [[ -1, 1, 1],[ -1, 0, 1]], - [[ -1, 1, 1],[ -1, 1, 0]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 1, 0, 1]], - [[ 1, -1, 1],[ 1, -1, 0]], - [[ 1, -1, 1],[ 0, -1, 1]], - [[ 1, 1, 1],[ 1, 1, 0]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 1, 0, 1]], - [[ -1, -1, 1],[ -1, -1, 0]], - [[ -1, -1, 1],[ 0, -1, 1]], - [[ -1, -1, 1],[ -1, 0, 1]], - [[ -1, 1, 1],[ -1, 1, 0]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ -1, 0, 1]], - [[ 1, -1, 1],[ 1, -1, 0]], - [[ 1, -1, 1],[ 0, -1, 1]], - [[ 1, -1, 1],[ 1, 0, 1]]],dtype='float'), - 'directions': np.array([ - [[ -5,-12, 17],[-17, -7, 17]], - [[ 17, -5,-12],[ 17,-17, -7]], - [[-12, 17, -5],[ -7, 17,-17]], - [[ 5, 12, 17],[ 17, 7, 17]], - [[-17, 5,-12],[-17, 17, -7]], - [[ 12,-17, -5],[ 7,-17,-17]], - [[ -5, 12,-17],[-17, 7,-17]], - [[ 17, 5, 12],[ 17, 17, 7]], - [[-12,-17, 5],[ -7,-17, 17]], - [[ 5,-12,-17],[ 17, -7,-17]], - [[-17, -5, 12],[-17,-17, 7]], - [[ 12, 17, 5],[ 7, 17, 17]], - [[ -5, 17,-12],[-17, 17, -7]], - [[-12, -5, 17],[ -7,-17, 17]], - [[ 17,-12, -5],[ 17, -7,-17]], - [[ 5,-17,-12],[ 17,-17, -7]], - [[ 12, 5, 17],[ 7, 17, 17]], - [[-17, 12, -5],[-17, 7,-17]], - [[ -5,-17, 12],[-17,-17, 7]], - [[-12, 5,-17],[ -7, 17,-17]], - [[ 17, 12, 5],[ 17, 7, 17]], - [[ 5, 17, 12],[ 17, 17, 7]], - [[ 12, -5,-17],[ 7,-17,-17]], - [[-17,-12, 5],[-17,-7, 17]]],dtype='float')} - - # Greninger--Troiano' orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - GTprime = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 7, 17, 17],[ 12, 5, 17]], - [[ 17, 7, 17],[ 17, 12, 5]], - [[ 17, 17, 7],[ 5, 17, 12]], - [[ -7,-17, 17],[-12, -5, 17]], - [[-17, -7, 17],[-17,-12, 5]], - [[-17,-17, 7],[ -5,-17, 12]], - [[ 7,-17,-17],[ 12, -5,-17]], - [[ 17, -7,-17],[ 17,-12, -5]], - [[ 17,-17, -7],[ 5,-17,-12]], - [[ -7, 17,-17],[-12, 5,-17]], - [[-17, 7,-17],[-17, 12, -5]], - [[-17, 17, -7],[ -5, 17,-12]], - [[ 7, 17, 17],[ 12, 17, 5]], - [[ 17, 7, 17],[ 5, 12, 17]], - [[ 17, 17, 7],[ 17, 5, 12]], - [[ -7,-17, 17],[-12,-17, 5]], - [[-17, -7, 17],[ -5,-12, 17]], - [[-17,-17, 7],[-17, -5, 12]], - [[ 7,-17,-17],[ 12,-17, -5]], - [[ 17, -7,-17],[ 5, -12,-17]], - [[ 17,-17, -7],[ 17, -5,-12]], - [[ -7, 17,-17],[-12, 17, -5]], - [[-17, 7,-17],[ -5, 12,-17]], - [[-17, 17, -7],[-17, 5,-12]]],dtype='float'), - 'directions': np.array([ - [[ 0, 1, -1],[ 1, 1, -1]], - [[ -1, 0, 1],[ -1, 1, 1]], - [[ 1, -1, 0],[ 1, -1, 1]], - [[ 0, -1, -1],[ -1, -1, -1]], - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, -1, 0],[ 1, -1, -1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ 1, 0, 1],[ 1, 1, 1]], - [[ -1, -1, 0],[ -1, -1, 1]], - [[ 0, -1, -1],[ 1, -1, -1]], - [[ -1, 0, 1],[ -1, -1, 1]], - [[ -1, -1, 0],[ -1, -1, -1]], - [[ 0, -1, 1],[ 1, -1, 1]], - [[ 1, 0, -1],[ 1, 1, -1]], - [[ -1, 1, 0],[ -1, 1, 1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ -1, 0, -1],[ -1, -1, -1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ -1, 0, -1],[ -1, 1, -1]], - [[ 1, 1, 0],[ 1, 1, 1]], - [[ 0, 1, 1],[ 1, 1, 1]], - [[ 1, 0, -1],[ 1, -1, -1]], - [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} - - # Nishiyama--Wassermann orientation relationship for fcc <-> bcc transformation - # from H. Kitahara et al., Materials Characterization 54:378-386, 2005 - NW = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]]],dtype='float'), - 'directions': np.array([ - [[ 2, -1, -1],[ 0, -1, 1]], - [[ -1, 2, -1],[ 0, -1, 1]], - [[ -1, -1, 2],[ 0, -1, 1]], - [[ -2, -1, -1],[ 0, -1, 1]], - [[ 1, 2, -1],[ 0, -1, 1]], - [[ 1, -1, 2],[ 0, -1, 1]], - [[ 2, 1, -1],[ 0, -1, 1]], - [[ -1, -2, -1],[ 0, -1, 1]], - [[ -1, 1, 2],[ 0, -1, 1]], - [[ 2, -1, 1],[ 0, -1, 1]], #It is wrong in the paper, but matrix is correct - [[ -1, 2, 1],[ 0, -1, 1]], - [[ -1, -1, -2],[ 0, -1, 1]]],dtype='float')} - - # Pitsch orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Acta Materialia 53:1179-1190, 2005 - Pitsch = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 0, 1, 0],[ -1, 0, 1]], - [[ 0, 0, 1],[ 1, -1, 0]], - [[ 1, 0, 0],[ 0, 1, -1]], - [[ 1, 0, 0],[ 0, -1, -1]], - [[ 0, 1, 0],[ -1, 0, -1]], - [[ 0, 0, 1],[ -1, -1, 0]], - [[ 0, 1, 0],[ -1, 0, -1]], - [[ 0, 0, 1],[ -1, -1, 0]], - [[ 1, 0, 0],[ 0, -1, -1]], - [[ 1, 0, 0],[ 0, -1, 1]], - [[ 0, 1, 0],[ 1, 0, -1]], - [[ 0, 0, 1],[ -1, 1, 0]]],dtype='float'), - 'directions': np.array([ - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, 1, 0],[ 1, 1, -1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ -1, 0, 1],[ -1, -1, 1]], - [[ 1, -1, 0],[ 1, -1, -1]], - [[ 1, 0, -1],[ 1, -1, -1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} - - # Bain orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - Bain = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 0, 0],[ 1, 0, 0]], - [[ 0, 1, 0],[ 0, 1, 0]], - [[ 0, 0, 1],[ 0, 0, 1]]],dtype='float'), - 'directions': np.array([ - [[ 0, 1, 0],[ 0, 1, 1]], - [[ 0, 0, 1],[ 1, 0, 1]], - [[ 1, 0, 0],[ 1, 1, 0]]],dtype='float')} - - def relationOperations(self,model): - """ - Crystallographic orientation relationships for phase transformations. - - References - ---------- - S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 - https://doi.org/10.1016/j.jallcom.2012.02.004 - - K. Kitahara et al., Acta Materialia 54(5):1279-1288, 2006 - https://doi.org/10.1016/j.actamat.2005.11.001 - - Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - https://doi.org/10.1107/S0021889805038276 - - H. Kitahara et al., Materials Characterization 54(4-5):378-386, 2005 - https://doi.org/10.1016/j.matchar.2004.12.015 - - Y. He et al., Acta Materialia 53(4):1179-1190, 2005 - https://doi.org/10.1016/j.actamat.2004.11.021 - - """ - models={'KS':self.KS, 'GT':self.GT, 'GT_prime':self.GTprime, - 'NW':self.NW, 'Pitsch': self.Pitsch, 'Bain':self.Bain} - try: - relationship = models[model] - except KeyError : - raise KeyError('Orientation relationship "{}" is unknown'.format(model)) - - if self.lattice not in relationship['mapping']: - raise ValueError('Relationship "{}" not supported for lattice "{}"'.format(model,self.lattice)) - - r = {'lattice':Lattice((set(relationship['mapping'])-{self.lattice}).pop()), # target lattice - 'rotations':[] } - - myPlane_id = relationship['mapping'][self.lattice] - otherPlane_id = (myPlane_id+1)%2 - myDir_id = myPlane_id +2 - otherDir_id = otherPlane_id +2 - - for miller in np.hstack((relationship['planes'],relationship['directions'])): - myPlane = miller[myPlane_id]/ np.linalg.norm(miller[myPlane_id]) - myDir = miller[myDir_id]/ np.linalg.norm(miller[myDir_id]) - myMatrix = np.array([myDir,np.cross(myPlane,myDir),myPlane]) - - otherPlane = miller[otherPlane_id]/ np.linalg.norm(miller[otherPlane_id]) - otherDir = miller[otherDir_id]/ np.linalg.norm(miller[otherDir_id]) - otherMatrix = np.array([otherDir,np.cross(otherPlane,otherDir),otherPlane]) - - r['rotations'].append(Rotation.fromMatrix(np.dot(otherMatrix.T,myMatrix))) - - return r - #################################################################################################### # Code below available according to the following conditions on https://github.com/MarDiehl/3Drotations #################################################################################################### From c84a6e90c91a5516de393ed6a8c426844270432c Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Fri, 21 Feb 2020 10:45:14 +0100 Subject: [PATCH 06/13] no 'dangling' functions @staticmethod is what we need here --- python/damask/rotation.py | 854 +++++++++++++++++++------------------- 1 file changed, 427 insertions(+), 427 deletions(-) diff --git a/python/damask/rotation.py b/python/damask/rotation.py index 1f8962105..4e9ff3a4d 100644 --- a/python/damask/rotation.py +++ b/python/damask/rotation.py @@ -4,6 +4,13 @@ from . import Lambert P = -1 +def isone(a): + return np.isclose(a,1.0,atol=1.0e-7,rtol=0.0) + +def iszero(a): + return np.isclose(a,0.0,atol=1.0e-12,rtol=0.0) + + #################################################################################################### class Rotation: u""" @@ -183,7 +190,7 @@ class Rotation: return angles in degrees. """ - eu = qu2eu(self.quaternion) + eu = Rotation.qu2eu(self.quaternion) if degrees: eu = np.degrees(eu) return eu @@ -201,13 +208,13 @@ class Rotation: return tuple of axis and angle. """ - ax = qu2ax(self.quaternion) + ax = Rotation.qu2ax(self.quaternion) if degrees: ax[3] = np.degrees(ax[3]) return (ax[:3],np.degrees(ax[3])) if pair else ax def asMatrix(self): """Rotation matrix.""" - return qu2om(self.quaternion) + return Rotation.qu2om(self.quaternion) def asRodrigues(self, vector = False): @@ -221,16 +228,16 @@ class Rotation: return as actual Rodrigues--Frank vector, i.e. rotation axis scaled by tan(ω/2). """ - ro = qu2ro(self.quaternion) + ro = Rotation.qu2ro(self.quaternion) return ro[:3]*ro[3] if vector else ro def asHomochoric(self): """Homochoric vector: (h_1, h_2, h_3).""" - return qu2ho(self.quaternion) + return Rotation.qu2ho(self.quaternion) def asCubochoric(self): """Cubochoric vector: (c_1, c_2, c_3).""" - return qu2cu(self.quaternion) + return Rotation.qu2cu(self.quaternion) def asM(self): """ @@ -276,7 +283,7 @@ class Rotation: if np.any(eu < 0.0) or np.any(eu > 2.0*np.pi) or eu[1] > np.pi: raise ValueError('Euler angles outside of [0..2π],[0..π],[0..2π].\n{} {} {}.'.format(*eu)) - return Rotation(eu2qu(eu)) + return Rotation(Rotation.eu2qu(eu)) @staticmethod def fromAxisAngle(angleAxis, @@ -294,7 +301,7 @@ class Rotation: if not np.isclose(np.linalg.norm(ax[0:3]), 1.0): raise ValueError('Axis angle rotation axis is not of unit length.\n{} {} {}'.format(*ax[0:3])) - return Rotation(ax2qu(ax)) + return Rotation(Rotation.ax2qu(ax)) @staticmethod def fromBasis(basis, @@ -316,7 +323,7 @@ class Rotation: or not np.isclose(np.dot(om[2],om[0]), 0.0): raise ValueError('matrix is not orthogonal.\n{}'.format(om)) - return Rotation(om2qu(om)) + return Rotation(Rotation.om2qu(om)) @staticmethod def fromMatrix(om, @@ -338,7 +345,7 @@ class Rotation: if ro[3] < 0.0: raise ValueError('Rodriques rotation angle not positive.\n'.format(ro[3])) - return Rotation(ro2qu(ro)) + return Rotation(Rotation.ro2qu(ro)) @staticmethod def fromHomochoric(homochoric, @@ -348,7 +355,7 @@ class Rotation: else np.array(homochoric,dtype=float) if P > 0: ho *= -1 # convert from P=1 to P=-1 - return Rotation(ho2qu(ho)) + return Rotation(Rotation.ho2qu(ho)) @staticmethod def fromCubochoric(cubochoric, @@ -356,10 +363,10 @@ class Rotation: cu = cubochoric if isinstance(cubochoric, np.ndarray) and cubochoric.dtype == np.dtype(float) \ else np.array(cubochoric,dtype=float) - ho = cu2ho(cu) + ho = Rotation.cu2ho(cu) if P > 0: ho *= -1 # convert from P=1 to P=-1 - return Rotation(ho2qu(ho)) + return Rotation(Rotation.ho2qu(ho)) @staticmethod @@ -437,417 +444,410 @@ class Rotation: # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #################################################################################################### - -def isone(a): - return np.isclose(a,1.0,atol=1.0e-7,rtol=0.0) - -def iszero(a): - return np.isclose(a,0.0,atol=1.0e-12,rtol=0.0) - -#---------- Quaternion ---------- - -def qu2om(qu): - """Quaternion to rotation matrix.""" - qq = qu[0]**2-(qu[1]**2 + qu[2]**2 + qu[3]**2) - om = np.diag(qq + 2.0*np.array([qu[1],qu[2],qu[3]])**2) - - om[1,0] = 2.0*(qu[2]*qu[1]+qu[0]*qu[3]) - om[0,1] = 2.0*(qu[1]*qu[2]-qu[0]*qu[3]) - om[2,1] = 2.0*(qu[3]*qu[2]+qu[0]*qu[1]) - om[1,2] = 2.0*(qu[2]*qu[3]-qu[0]*qu[1]) - om[0,2] = 2.0*(qu[1]*qu[3]+qu[0]*qu[2]) - om[2,0] = 2.0*(qu[3]*qu[1]-qu[0]*qu[2]) - return om if P > 0.0 else om.T - - -def qu2eu(qu): - """Quaternion to Bunge-Euler angles.""" - q03 = qu[0]**2+qu[3]**2 - q12 = qu[1]**2+qu[2]**2 - chi = np.sqrt(q03*q12) - - if iszero(chi): - eu = np.array([np.arctan2(-P*2.0*qu[0]*qu[3],qu[0]**2-qu[3]**2), 0.0, 0.0]) if iszero(q12) else \ - np.array([np.arctan2(2.0*qu[1]*qu[2],qu[1]**2-qu[2]**2), np.pi, 0.0]) - else: - eu = np.array([np.arctan2((-P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]-qu[2]*qu[3])*chi ), - np.arctan2( 2.0*chi, q03-q12 ), - np.arctan2(( P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]+qu[2]*qu[3])*chi )]) - - # reduce Euler angles to definition range, i.e a lower limit of 0.0 - eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) - return eu - - -def qu2ax(qu): - """ - Quaternion to axis angle pair. - - Modified version of the original formulation, should be numerically more stable - """ - if iszero(qu[1]**2+qu[2]**2+qu[3]**2): # set axis to [001] if the angle is 0/360 - ax = [ 0.0, 0.0, 1.0, 0.0 ] - elif not iszero(qu[0]): - s = np.sign(qu[0])/np.sqrt(qu[1]**2+qu[2]**2+qu[3]**2) - omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) - ax = [ qu[1]*s, qu[2]*s, qu[3]*s, omega ] - else: - ax = [ qu[1], qu[2], qu[3], np.pi] - - return np.array(ax) - - -def qu2ro(qu): - """Quaternion to Rodriques-Frank vector.""" - if iszero(qu[0]): - ro = [qu[1], qu[2], qu[3], np.inf] - else: - s = np.linalg.norm([qu[1],qu[2],qu[3]]) - ro = [0.0,0.0,P,0.0] if iszero(s) else \ - [ qu[1]/s, qu[2]/s, qu[3]/s, np.tan(np.arccos(np.clip(qu[0],-1.0,1.0)))] # avoid numerical difficulties - - return np.array(ro) - - -def qu2ho(qu): - """Quaternion to homochoric vector.""" - omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) # avoid numerical difficulties - - if iszero(omega): - ho = np.array([ 0.0, 0.0, 0.0 ]) - else: - ho = np.array([qu[1], qu[2], qu[3]]) - f = 0.75 * ( omega - np.sin(omega) ) - ho = ho/np.linalg.norm(ho) * f**(1./3.) - - return ho - - -def qu2cu(qu): - """Quaternion to cubochoric vector.""" - return ho2cu(qu2ho(qu)) - - -#---------- Rotation matrix ---------- - -def om2qu(om): - """ - Rotation matrix to quaternion. - - The original formulation (direct conversion) had (numerical?) issues - """ - return eu2qu(om2eu(om)) - - -def om2eu(om): - """Rotation matrix to Bunge-Euler angles.""" - if abs(om[2,2]) < 1.0: - zeta = 1.0/np.sqrt(1.0-om[2,2]**2) - eu = np.array([np.arctan2(om[2,0]*zeta,-om[2,1]*zeta), - np.arccos(om[2,2]), - np.arctan2(om[0,2]*zeta, om[1,2]*zeta)]) - else: - eu = np.array([np.arctan2( om[0,1],om[0,0]), np.pi*0.5*(1-om[2,2]),0.0]) # following the paper, not the reference implementation - - # reduce Euler angles to definition range, i.e a lower limit of 0.0 - eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) - return eu - - -def om2ax(om): - """Rotation matrix to axis angle pair.""" - ax=np.empty(4) - - # first get the rotation angle - t = 0.5*(om.trace() -1.0) - ax[3] = np.arccos(np.clip(t,-1.0,1.0)) - - if iszero(ax[3]): - ax = [ 0.0, 0.0, 1.0, 0.0] - else: - w,vr = np.linalg.eig(om) - # next, find the eigenvalue (1,0j) - i = np.where(np.isclose(w,1.0+0.0j))[0][0] - ax[0:3] = np.real(vr[0:3,i]) - diagDelta = np.array([om[1,2]-om[2,1],om[2,0]-om[0,2],om[0,1]-om[1,0]]) - ax[0:3] = np.where(iszero(diagDelta), ax[0:3],np.abs(ax[0:3])*np.sign(-P*diagDelta)) - - return np.array(ax) - - -def om2ro(om): - """Rotation matrix to Rodriques-Frank vector.""" - return eu2ro(om2eu(om)) - - -def om2ho(om): - """Rotation matrix to homochoric vector.""" - return ax2ho(om2ax(om)) - - -def om2cu(om): - """Rotation matrix to cubochoric vector.""" - return ho2cu(om2ho(om)) - - -#---------- Bunge-Euler angles ---------- - -def eu2qu(eu): - """Bunge-Euler angles to quaternion.""" - ee = 0.5*eu - cPhi = np.cos(ee[1]) - sPhi = np.sin(ee[1]) - qu = np.array([ cPhi*np.cos(ee[0]+ee[2]), - -P*sPhi*np.cos(ee[0]-ee[2]), - -P*sPhi*np.sin(ee[0]-ee[2]), - -P*cPhi*np.sin(ee[0]+ee[2]) ]) - if qu[0] < 0.0: qu*=-1 - return qu - - -def eu2om(eu): - """Bunge-Euler angles to rotation matrix.""" - c = np.cos(eu) - s = np.sin(eu) - - om = np.array([[+c[0]*c[2]-s[0]*s[2]*c[1], +s[0]*c[2]+c[0]*s[2]*c[1], +s[2]*s[1]], - [-c[0]*s[2]-s[0]*c[2]*c[1], -s[0]*s[2]+c[0]*c[2]*c[1], +c[2]*s[1]], - [+s[0]*s[1], -c[0]*s[1], +c[1] ]]) - - om[np.where(iszero(om))] = 0.0 - return om - - -def eu2ax(eu): - """Bunge-Euler angles to axis angle pair.""" - t = np.tan(eu[1]*0.5) - sigma = 0.5*(eu[0]+eu[2]) - delta = 0.5*(eu[0]-eu[2]) - tau = np.linalg.norm([t,np.sin(sigma)]) - alpha = np.pi if iszero(np.cos(sigma)) else \ - 2.0*np.arctan(tau/np.cos(sigma)) - - if iszero(alpha): - ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) - else: - ax = -P/tau * np.array([ t*np.cos(delta), t*np.sin(delta), np.sin(sigma) ]) # passive axis angle pair so a minus sign in front - ax = np.append(ax,alpha) - if alpha < 0.0: ax *= -1.0 # ensure alpha is positive - - return ax - - -def eu2ro(eu): - """Bunge-Euler angles to Rodriques-Frank vector.""" - ro = eu2ax(eu) # convert to axis angle pair representation - if ro[3] >= np.pi: # Differs from original implementation. check convention 5 - ro[3] = np.inf - elif iszero(ro[3]): - ro = np.array([ 0.0, 0.0, P, 0.0 ]) - else: - ro[3] = np.tan(ro[3]*0.5) - - return ro - - -def eu2ho(eu): - """Bunge-Euler angles to homochoric vector.""" - return ax2ho(eu2ax(eu)) - - -def eu2cu(eu): - """Bunge-Euler angles to cubochoric vector.""" - return ho2cu(eu2ho(eu)) - - -#---------- Axis angle pair ---------- - -def ax2qu(ax): - """Axis angle pair to quaternion.""" - if iszero(ax[3]): - qu = np.array([ 1.0, 0.0, 0.0, 0.0 ]) - else: - c = np.cos(ax[3]*0.5) - s = np.sin(ax[3]*0.5) - qu = np.array([ c, ax[0]*s, ax[1]*s, ax[2]*s ]) - - return qu - - -def ax2om(ax): - """Axis angle pair to rotation matrix.""" - c = np.cos(ax[3]) - s = np.sin(ax[3]) - omc = 1.0-c - om=np.diag(ax[0:3]**2*omc + c) - - for idx in [[0,1,2],[1,2,0],[2,0,1]]: - q = omc*ax[idx[0]] * ax[idx[1]] - om[idx[0],idx[1]] = q + s*ax[idx[2]] - om[idx[1],idx[0]] = q - s*ax[idx[2]] - - return om if P < 0.0 else om.T - - -def ax2eu(ax): - """Rotation matrix to Bunge Euler angles.""" - return om2eu(ax2om(ax)) - - -def ax2ro(ax): - """Axis angle pair to Rodriques-Frank vector.""" - if iszero(ax[3]): - ro = [ 0.0, 0.0, P, 0.0 ] - else: - ro = [ax[0], ax[1], ax[2]] - # 180 degree case - ro += [np.inf] if np.isclose(ax[3],np.pi,atol=1.0e-15,rtol=0.0) else \ - [np.tan(ax[3]*0.5)] - - return np.array(ro) - - -def ax2ho(ax): - """Axis angle pair to homochoric vector.""" - f = (0.75 * ( ax[3] - np.sin(ax[3]) ))**(1.0/3.0) - ho = ax[0:3] * f - return ho - - -def ax2cu(ax): - """Axis angle pair to cubochoric vector.""" - return ho2cu(ax2ho(ax)) - - -#---------- Rodrigues-Frank vector ---------- - -def ro2qu(ro): - """Rodriques-Frank vector to quaternion.""" - return ax2qu(ro2ax(ro)) - - -def ro2om(ro): - """Rodgrigues-Frank vector to rotation matrix.""" - return ax2om(ro2ax(ro)) - - -def ro2eu(ro): - """Rodriques-Frank vector to Bunge-Euler angles.""" - return om2eu(ro2om(ro)) - - -def ro2ax(ro): - """Rodriques-Frank vector to axis angle pair.""" - ta = ro[3] - - if iszero(ta): - ax = [ 0.0, 0.0, 1.0, 0.0 ] - elif not np.isfinite(ta): - ax = [ ro[0], ro[1], ro[2], np.pi ] - else: - angle = 2.0*np.arctan(ta) - ta = 1.0/np.linalg.norm(ro[0:3]) - ax = [ ro[0]/ta, ro[1]/ta, ro[2]/ta, angle ] - - return np.array(ax) - - -def ro2ho(ro): - """Rodriques-Frank vector to homochoric vector.""" - if iszero(np.sum(ro[0:3]**2.0)): - ho = [ 0.0, 0.0, 0.0 ] - else: - f = 2.0*np.arctan(ro[3]) -np.sin(2.0*np.arctan(ro[3])) if np.isfinite(ro[3]) else np.pi - ho = ro[0:3] * (0.75*f)**(1.0/3.0) - - return np.array(ho) - - -def ro2cu(ro): - """Rodriques-Frank vector to cubochoric vector.""" - return ho2cu(ro2ho(ro)) - - -#---------- Homochoric vector---------- - -def ho2qu(ho): - """Homochoric vector to quaternion.""" - return ax2qu(ho2ax(ho)) - - -def ho2om(ho): - """Homochoric vector to rotation matrix.""" - return ax2om(ho2ax(ho)) - - -def ho2eu(ho): - """Homochoric vector to Bunge-Euler angles.""" - return ax2eu(ho2ax(ho)) - - -def ho2ax(ho): - """Homochoric vector to axis angle pair.""" - tfit = np.array([+1.0000000000018852, -0.5000000002194847, - -0.024999992127593126, -0.003928701544781374, - -0.0008152701535450438, -0.0002009500426119712, - -0.00002397986776071756, -0.00008202868926605841, - +0.00012448715042090092, -0.0001749114214822577, - +0.0001703481934140054, -0.00012062065004116828, - +0.000059719705868660826, -0.00001980756723965647, - +0.000003953714684212874, -0.00000036555001439719544]) - # normalize h and store the magnitude - hmag_squared = np.sum(ho**2.) - if iszero(hmag_squared): - ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) - else: - hm = hmag_squared - - # convert the magnitude to the rotation angle - s = tfit[0] + tfit[1] * hmag_squared - for i in range(2,16): - hm *= hmag_squared - s += tfit[i] * hm - ax = np.append(ho/np.sqrt(hmag_squared),2.0*np.arccos(np.clip(s,-1.0,1.0))) - return ax - - -def ho2ro(ho): - """Axis angle pair to Rodriques-Frank vector.""" - return ax2ro(ho2ax(ho)) - - -def ho2cu(ho): - """Homochoric vector to cubochoric vector.""" - return Lambert.BallToCube(ho) - - -#---------- Cubochoric ---------- - -def cu2qu(cu): - """Cubochoric vector to quaternion.""" - return ho2qu(cu2ho(cu)) - - -def cu2om(cu): - """Cubochoric vector to rotation matrix.""" - return ho2om(cu2ho(cu)) - - -def cu2eu(cu): - """Cubochoric vector to Bunge-Euler angles.""" - return ho2eu(cu2ho(cu)) - - -def cu2ax(cu): - """Cubochoric vector to axis angle pair.""" - return ho2ax(cu2ho(cu)) - - -def cu2ro(cu): - """Cubochoric vector to Rodriques-Frank vector.""" - return ho2ro(cu2ho(cu)) - - -def cu2ho(cu): - """Cubochoric vector to homochoric vector.""" - return Lambert.CubeToBall(cu) + #---------- Quaternion ---------- + @staticmethod + def qu2om(qu): + """Quaternion to rotation matrix.""" + qq = qu[0]**2-(qu[1]**2 + qu[2]**2 + qu[3]**2) + om = np.diag(qq + 2.0*np.array([qu[1],qu[2],qu[3]])**2) + + om[1,0] = 2.0*(qu[2]*qu[1]+qu[0]*qu[3]) + om[0,1] = 2.0*(qu[1]*qu[2]-qu[0]*qu[3]) + om[2,1] = 2.0*(qu[3]*qu[2]+qu[0]*qu[1]) + om[1,2] = 2.0*(qu[2]*qu[3]-qu[0]*qu[1]) + om[0,2] = 2.0*(qu[1]*qu[3]+qu[0]*qu[2]) + om[2,0] = 2.0*(qu[3]*qu[1]-qu[0]*qu[2]) + return om if P > 0.0 else om.T + + @staticmethod + def qu2eu(qu): + """Quaternion to Bunge-Euler angles.""" + q03 = qu[0]**2+qu[3]**2 + q12 = qu[1]**2+qu[2]**2 + chi = np.sqrt(q03*q12) + + if iszero(chi): + eu = np.array([np.arctan2(-P*2.0*qu[0]*qu[3],qu[0]**2-qu[3]**2), 0.0, 0.0]) if iszero(q12) else \ + np.array([np.arctan2(2.0*qu[1]*qu[2],qu[1]**2-qu[2]**2), np.pi, 0.0]) + else: + eu = np.array([np.arctan2((-P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]-qu[2]*qu[3])*chi ), + np.arctan2( 2.0*chi, q03-q12 ), + np.arctan2(( P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]+qu[2]*qu[3])*chi )]) + + # reduce Euler angles to definition range, i.e a lower limit of 0.0 + eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) + return eu + + @staticmethod + def qu2ax(qu): + """ + Quaternion to axis angle pair. + + Modified version of the original formulation, should be numerically more stable + """ + if iszero(qu[1]**2+qu[2]**2+qu[3]**2): # set axis to [001] if the angle is 0/360 + ax = [ 0.0, 0.0, 1.0, 0.0 ] + elif not iszero(qu[0]): + s = np.sign(qu[0])/np.sqrt(qu[1]**2+qu[2]**2+qu[3]**2) + omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) + ax = [ qu[1]*s, qu[2]*s, qu[3]*s, omega ] + else: + ax = [ qu[1], qu[2], qu[3], np.pi] + + return np.array(ax) + + @staticmethod + def qu2ro(qu): + """Quaternion to Rodriques-Frank vector.""" + if iszero(qu[0]): + ro = [qu[1], qu[2], qu[3], np.inf] + else: + s = np.linalg.norm([qu[1],qu[2],qu[3]]) + ro = [0.0,0.0,P,0.0] if iszero(s) else \ + [ qu[1]/s, qu[2]/s, qu[3]/s, np.tan(np.arccos(np.clip(qu[0],-1.0,1.0)))] # avoid numerical difficulties + + return np.array(ro) + + @staticmethod + def qu2ho(qu): + """Quaternion to homochoric vector.""" + omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) # avoid numerical difficulties + + if iszero(omega): + ho = np.array([ 0.0, 0.0, 0.0 ]) + else: + ho = np.array([qu[1], qu[2], qu[3]]) + f = 0.75 * ( omega - np.sin(omega) ) + ho = ho/np.linalg.norm(ho) * f**(1./3.) + + return ho + + @staticmethod + def qu2cu(qu): + """Quaternion to cubochoric vector.""" + return Rotation.ho2cu(Rotation.qu2ho(qu)) + + + #---------- Rotation matrix ---------- + @staticmethod + def om2qu(om): + """ + Rotation matrix to quaternion. + + The original formulation (direct conversion) had (numerical?) issues + """ + return Rotation.eu2qu(Rotation.om2eu(om)) + + @staticmethod + def om2eu(om): + """Rotation matrix to Bunge-Euler angles.""" + if abs(om[2,2]) < 1.0: + zeta = 1.0/np.sqrt(1.0-om[2,2]**2) + eu = np.array([np.arctan2(om[2,0]*zeta,-om[2,1]*zeta), + np.arccos(om[2,2]), + np.arctan2(om[0,2]*zeta, om[1,2]*zeta)]) + else: + eu = np.array([np.arctan2( om[0,1],om[0,0]), np.pi*0.5*(1-om[2,2]),0.0]) # following the paper, not the reference implementation + + # reduce Euler angles to definition range, i.e a lower limit of 0.0 + eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) + return eu + + @staticmethod + def om2ax(om): + """Rotation matrix to axis angle pair.""" + ax=np.empty(4) + + # first get the rotation angle + t = 0.5*(om.trace() -1.0) + ax[3] = np.arccos(np.clip(t,-1.0,1.0)) + + if iszero(ax[3]): + ax = [ 0.0, 0.0, 1.0, 0.0] + else: + w,vr = np.linalg.eig(om) + # next, find the eigenvalue (1,0j) + i = np.where(np.isclose(w,1.0+0.0j))[0][0] + ax[0:3] = np.real(vr[0:3,i]) + diagDelta = np.array([om[1,2]-om[2,1],om[2,0]-om[0,2],om[0,1]-om[1,0]]) + ax[0:3] = np.where(iszero(diagDelta), ax[0:3],np.abs(ax[0:3])*np.sign(-P*diagDelta)) + + return np.array(ax) + + @staticmethod + def om2ro(om): + """Rotation matrix to Rodriques-Frank vector.""" + return Rotation.eu2ro(Rotation.om2eu(om)) + + @staticmethod + def om2ho(om): + """Rotation matrix to homochoric vector.""" + return Rotation.ax2ho(Rotation.om2ax(om)) + + @staticmethod + def om2cu(om): + """Rotation matrix to cubochoric vector.""" + return Rotation.ho2cu(Rotation.om2ho(om)) + + + #---------- Bunge-Euler angles ---------- + @staticmethod + def eu2qu(eu): + """Bunge-Euler angles to quaternion.""" + ee = 0.5*eu + cPhi = np.cos(ee[1]) + sPhi = np.sin(ee[1]) + qu = np.array([ cPhi*np.cos(ee[0]+ee[2]), + -P*sPhi*np.cos(ee[0]-ee[2]), + -P*sPhi*np.sin(ee[0]-ee[2]), + -P*cPhi*np.sin(ee[0]+ee[2]) ]) + if qu[0] < 0.0: qu*=-1 + return qu + + @staticmethod + def eu2om(eu): + """Bunge-Euler angles to rotation matrix.""" + c = np.cos(eu) + s = np.sin(eu) + + om = np.array([[+c[0]*c[2]-s[0]*s[2]*c[1], +s[0]*c[2]+c[0]*s[2]*c[1], +s[2]*s[1]], + [-c[0]*s[2]-s[0]*c[2]*c[1], -s[0]*s[2]+c[0]*c[2]*c[1], +c[2]*s[1]], + [+s[0]*s[1], -c[0]*s[1], +c[1] ]]) + + om[np.where(iszero(om))] = 0.0 + return om + + @staticmethod + def eu2ax(eu): + """Bunge-Euler angles to axis angle pair.""" + t = np.tan(eu[1]*0.5) + sigma = 0.5*(eu[0]+eu[2]) + delta = 0.5*(eu[0]-eu[2]) + tau = np.linalg.norm([t,np.sin(sigma)]) + alpha = np.pi if iszero(np.cos(sigma)) else \ + 2.0*np.arctan(tau/np.cos(sigma)) + + if iszero(alpha): + ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) + else: + ax = -P/tau * np.array([ t*np.cos(delta), t*np.sin(delta), np.sin(sigma) ]) # passive axis angle pair so a minus sign in front + ax = np.append(ax,alpha) + if alpha < 0.0: ax *= -1.0 # ensure alpha is positive + + return ax + + @staticmethod + def eu2ro(eu): + """Bunge-Euler angles to Rodriques-Frank vector.""" + ro = eu2ax(eu) # convert to axis angle pair representation + if ro[3] >= np.pi: # Differs from original implementation. check convention 5 + ro[3] = np.inf + elif iszero(ro[3]): + ro = np.array([ 0.0, 0.0, P, 0.0 ]) + else: + ro[3] = np.tan(ro[3]*0.5) + + return ro + + @staticmethod + def eu2ho(eu): + """Bunge-Euler angles to homochoric vector.""" + return Rotation.ax2ho(Rotation.eu2ax(eu)) + + @staticmethod + def eu2cu(eu): + """Bunge-Euler angles to cubochoric vector.""" + return Rotation.ho2cu(Rotation.eu2ho(eu)) + + + #---------- Axis angle pair ---------- + @staticmethod + def ax2qu(ax): + """Axis angle pair to quaternion.""" + if iszero(ax[3]): + qu = np.array([ 1.0, 0.0, 0.0, 0.0 ]) + else: + c = np.cos(ax[3]*0.5) + s = np.sin(ax[3]*0.5) + qu = np.array([ c, ax[0]*s, ax[1]*s, ax[2]*s ]) + + return qu + + @staticmethod + def ax2om(ax): + """Axis angle pair to rotation matrix.""" + c = np.cos(ax[3]) + s = np.sin(ax[3]) + omc = 1.0-c + om=np.diag(ax[0:3]**2*omc + c) + + for idx in [[0,1,2],[1,2,0],[2,0,1]]: + q = omc*ax[idx[0]] * ax[idx[1]] + om[idx[0],idx[1]] = q + s*ax[idx[2]] + om[idx[1],idx[0]] = q - s*ax[idx[2]] + + return om if P < 0.0 else om.T + + @staticmethod + def ax2eu(ax): + """Rotation matrix to Bunge Euler angles.""" + return Rotation.om2eu(Rotation.ax2om(ax)) + + @staticmethod + def ax2ro(ax): + """Axis angle pair to Rodriques-Frank vector.""" + if iszero(ax[3]): + ro = [ 0.0, 0.0, P, 0.0 ] + else: + ro = [ax[0], ax[1], ax[2]] + # 180 degree case + ro += [np.inf] if np.isclose(ax[3],np.pi,atol=1.0e-15,rtol=0.0) else \ + [np.tan(ax[3]*0.5)] + + return np.array(ro) + + @staticmethod + def ax2ho(ax): + """Axis angle pair to homochoric vector.""" + f = (0.75 * ( ax[3] - np.sin(ax[3]) ))**(1.0/3.0) + ho = ax[0:3] * f + return ho + + @staticmethod + def ax2cu(ax): + """Axis angle pair to cubochoric vector.""" + return Rotation.ho2cu(Rotation.ax2ho(ax)) + + + #---------- Rodrigues-Frank vector ---------- + @staticmethod + def ro2qu(ro): + """Rodriques-Frank vector to quaternion.""" + return Rotation.ax2qu(Rotation.ro2ax(ro)) + + @staticmethod + def ro2om(ro): + """Rodgrigues-Frank vector to rotation matrix.""" + return Rotation.ax2om(Rotation.ro2ax(ro)) + + @staticmethod + def ro2eu(ro): + """Rodriques-Frank vector to Bunge-Euler angles.""" + return Rotation.om2eu(Rotation.ro2om(ro)) + + @staticmethod + def ro2ax(ro): + """Rodriques-Frank vector to axis angle pair.""" + ta = ro[3] + + if iszero(ta): + ax = [ 0.0, 0.0, 1.0, 0.0 ] + elif not np.isfinite(ta): + ax = [ ro[0], ro[1], ro[2], np.pi ] + else: + angle = 2.0*np.arctan(ta) + ta = 1.0/np.linalg.norm(ro[0:3]) + ax = [ ro[0]/ta, ro[1]/ta, ro[2]/ta, angle ] + + return np.array(ax) + + @staticmethod + def ro2ho(ro): + """Rodriques-Frank vector to homochoric vector.""" + if iszero(np.sum(ro[0:3]**2.0)): + ho = [ 0.0, 0.0, 0.0 ] + else: + f = 2.0*np.arctan(ro[3]) -np.sin(2.0*np.arctan(ro[3])) if np.isfinite(ro[3]) else np.pi + ho = ro[0:3] * (0.75*f)**(1.0/3.0) + + return np.array(ho) + + @staticmethod + def ro2cu(ro): + """Rodriques-Frank vector to cubochoric vector.""" + return ho2cu(ro2ho(ro)) + + + #---------- Homochoric vector---------- + @staticmethod + def ho2qu(ho): + """Homochoric vector to quaternion.""" + return Rotation.ax2qu(Rotation.ho2ax(ho)) + + @staticmethod + def ho2om(ho): + """Homochoric vector to rotation matrix.""" + return Rotation.ax2om(Rotation.ho2ax(ho)) + + @staticmethod + def ho2eu(ho): + """Homochoric vector to Bunge-Euler angles.""" + return Rotation.ax2eu(Rotation.ho2ax(ho)) + + @staticmethod + def ho2ax(ho): + """Homochoric vector to axis angle pair.""" + tfit = np.array([+1.0000000000018852, -0.5000000002194847, + -0.024999992127593126, -0.003928701544781374, + -0.0008152701535450438, -0.0002009500426119712, + -0.00002397986776071756, -0.00008202868926605841, + +0.00012448715042090092, -0.0001749114214822577, + +0.0001703481934140054, -0.00012062065004116828, + +0.000059719705868660826, -0.00001980756723965647, + +0.000003953714684212874, -0.00000036555001439719544]) + # normalize h and store the magnitude + hmag_squared = np.sum(ho**2.) + if iszero(hmag_squared): + ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) + else: + hm = hmag_squared + + # convert the magnitude to the rotation angle + s = tfit[0] + tfit[1] * hmag_squared + for i in range(2,16): + hm *= hmag_squared + s += tfit[i] * hm + ax = np.append(ho/np.sqrt(hmag_squared),2.0*np.arccos(np.clip(s,-1.0,1.0))) + return ax + + @staticmethod + def ho2ro(ho): + """Axis angle pair to Rodriques-Frank vector.""" + return Rotation.ax2ro(Rotation.ho2ax(ho)) + + @staticmethod + def ho2cu(ho): + """Homochoric vector to cubochoric vector.""" + return Lambert.BallToCube(ho) + + + #---------- Cubochoric ---------- + @staticmethod + def cu2qu(cu): + """Cubochoric vector to quaternion.""" + return Rotation.ho2qu(Rotation.cu2ho(cu)) + + @staticmethod + def cu2om(cu): + """Cubochoric vector to rotation matrix.""" + return Rotation.ho2om(Rotation.cu2ho(cu)) + + @staticmethod + def cu2eu(cu): + """Cubochoric vector to Bunge-Euler angles.""" + return Rotation.ho2eu(Rotation.cu2ho(cu)) + + @staticmethod + def cu2ax(cu): + """Cubochoric vector to axis angle pair.""" + return Rotation.ho2ax(Rotation.cu2ho(cu)) + + @staticmethod + def cu2ro(cu): + """Cubochoric vector to Rodriques-Frank vector.""" + return Rotation.ho2ro(Rotation.cu2ho(cu)) + + @staticmethod + def cu2ho(cu): + """Cubochoric vector to homochoric vector.""" + return Lambert.CubeToBall(cu) From 58610e23a7d8d575e0e4f277b92590089cb9d9c0 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Fri, 21 Feb 2020 10:53:44 +0100 Subject: [PATCH 07/13] use 4 space for indentation --- python/damask/rotation.py | 497 +++++++++++++++++++------------------- 1 file changed, 243 insertions(+), 254 deletions(-) diff --git a/python/damask/rotation.py b/python/damask/rotation.py index 4e9ff3a4d..08ec7c564 100644 --- a/python/damask/rotation.py +++ b/python/damask/rotation.py @@ -447,407 +447,396 @@ class Rotation: #---------- Quaternion ---------- @staticmethod def qu2om(qu): - """Quaternion to rotation matrix.""" - qq = qu[0]**2-(qu[1]**2 + qu[2]**2 + qu[3]**2) - om = np.diag(qq + 2.0*np.array([qu[1],qu[2],qu[3]])**2) + """Quaternion to rotation matrix.""" + qq = qu[0]**2-(qu[1]**2 + qu[2]**2 + qu[3]**2) + om = np.diag(qq + 2.0*np.array([qu[1],qu[2],qu[3]])**2) - om[1,0] = 2.0*(qu[2]*qu[1]+qu[0]*qu[3]) - om[0,1] = 2.0*(qu[1]*qu[2]-qu[0]*qu[3]) - om[2,1] = 2.0*(qu[3]*qu[2]+qu[0]*qu[1]) - om[1,2] = 2.0*(qu[2]*qu[3]-qu[0]*qu[1]) - om[0,2] = 2.0*(qu[1]*qu[3]+qu[0]*qu[2]) - om[2,0] = 2.0*(qu[3]*qu[1]-qu[0]*qu[2]) - return om if P > 0.0 else om.T + om[1,0] = 2.0*(qu[2]*qu[1]+qu[0]*qu[3]) + om[0,1] = 2.0*(qu[1]*qu[2]-qu[0]*qu[3]) + om[2,1] = 2.0*(qu[3]*qu[2]+qu[0]*qu[1]) + om[1,2] = 2.0*(qu[2]*qu[3]-qu[0]*qu[1]) + om[0,2] = 2.0*(qu[1]*qu[3]+qu[0]*qu[2]) + om[2,0] = 2.0*(qu[3]*qu[1]-qu[0]*qu[2]) + return om if P > 0.0 else om.T @staticmethod def qu2eu(qu): - """Quaternion to Bunge-Euler angles.""" - q03 = qu[0]**2+qu[3]**2 - q12 = qu[1]**2+qu[2]**2 - chi = np.sqrt(q03*q12) + """Quaternion to Bunge-Euler angles.""" + q03 = qu[0]**2+qu[3]**2 + q12 = qu[1]**2+qu[2]**2 + chi = np.sqrt(q03*q12) - if iszero(chi): - eu = np.array([np.arctan2(-P*2.0*qu[0]*qu[3],qu[0]**2-qu[3]**2), 0.0, 0.0]) if iszero(q12) else \ - np.array([np.arctan2(2.0*qu[1]*qu[2],qu[1]**2-qu[2]**2), np.pi, 0.0]) - else: - eu = np.array([np.arctan2((-P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]-qu[2]*qu[3])*chi ), - np.arctan2( 2.0*chi, q03-q12 ), - np.arctan2(( P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]+qu[2]*qu[3])*chi )]) + if iszero(chi): + eu = np.array([np.arctan2(-P*2.0*qu[0]*qu[3],qu[0]**2-qu[3]**2), 0.0, 0.0]) if iszero(q12) else \ + np.array([np.arctan2(2.0*qu[1]*qu[2],qu[1]**2-qu[2]**2), np.pi, 0.0]) + else: + eu = np.array([np.arctan2((-P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]-qu[2]*qu[3])*chi ), + np.arctan2( 2.0*chi, q03-q12 ), + np.arctan2(( P*qu[0]*qu[2]+qu[1]*qu[3])*chi, (-P*qu[0]*qu[1]+qu[2]*qu[3])*chi )]) - # reduce Euler angles to definition range, i.e a lower limit of 0.0 - eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) - return eu + # reduce Euler angles to definition range, i.e a lower limit of 0.0 + eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) + return eu @staticmethod def qu2ax(qu): - """ - Quaternion to axis angle pair. + """ + Quaternion to axis angle pair. - Modified version of the original formulation, should be numerically more stable - """ - if iszero(qu[1]**2+qu[2]**2+qu[3]**2): # set axis to [001] if the angle is 0/360 - ax = [ 0.0, 0.0, 1.0, 0.0 ] - elif not iszero(qu[0]): - s = np.sign(qu[0])/np.sqrt(qu[1]**2+qu[2]**2+qu[3]**2) - omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) - ax = [ qu[1]*s, qu[2]*s, qu[3]*s, omega ] - else: - ax = [ qu[1], qu[2], qu[3], np.pi] - - return np.array(ax) + Modified version of the original formulation, should be numerically more stable + """ + if iszero(qu[1]**2+qu[2]**2+qu[3]**2): # set axis to [001] if the angle is 0/360 + ax = [ 0.0, 0.0, 1.0, 0.0 ] + elif not iszero(qu[0]): + s = np.sign(qu[0])/np.sqrt(qu[1]**2+qu[2]**2+qu[3]**2) + omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) + ax = [ qu[1]*s, qu[2]*s, qu[3]*s, omega ] + else: + ax = [ qu[1], qu[2], qu[3], np.pi] + return np.array(ax) @staticmethod def qu2ro(qu): - """Quaternion to Rodriques-Frank vector.""" - if iszero(qu[0]): - ro = [qu[1], qu[2], qu[3], np.inf] - else: - s = np.linalg.norm([qu[1],qu[2],qu[3]]) - ro = [0.0,0.0,P,0.0] if iszero(s) else \ - [ qu[1]/s, qu[2]/s, qu[3]/s, np.tan(np.arccos(np.clip(qu[0],-1.0,1.0)))] # avoid numerical difficulties - - return np.array(ro) + """Quaternion to Rodriques-Frank vector.""" + if iszero(qu[0]): + ro = [qu[1], qu[2], qu[3], np.inf] + else: + s = np.linalg.norm([qu[1],qu[2],qu[3]]) + ro = [0.0,0.0,P,0.0] if iszero(s) else \ + [ qu[1]/s, qu[2]/s, qu[3]/s, np.tan(np.arccos(np.clip(qu[0],-1.0,1.0)))] + return np.array(ro) @staticmethod def qu2ho(qu): - """Quaternion to homochoric vector.""" - omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) # avoid numerical difficulties + """Quaternion to homochoric vector.""" + omega = 2.0 * np.arccos(np.clip(qu[0],-1.0,1.0)) - if iszero(omega): - ho = np.array([ 0.0, 0.0, 0.0 ]) - else: - ho = np.array([qu[1], qu[2], qu[3]]) - f = 0.75 * ( omega - np.sin(omega) ) - ho = ho/np.linalg.norm(ho) * f**(1./3.) - - return ho + if iszero(omega): + ho = np.array([ 0.0, 0.0, 0.0 ]) + else: + ho = np.array([qu[1], qu[2], qu[3]]) + f = 0.75 * ( omega - np.sin(omega) ) + ho = ho/np.linalg.norm(ho) * f**(1./3.) + return ho @staticmethod def qu2cu(qu): - """Quaternion to cubochoric vector.""" - return Rotation.ho2cu(Rotation.qu2ho(qu)) + """Quaternion to cubochoric vector.""" + return Rotation.ho2cu(Rotation.qu2ho(qu)) #---------- Rotation matrix ---------- @staticmethod def om2qu(om): - """ - Rotation matrix to quaternion. + """ + Rotation matrix to quaternion. - The original formulation (direct conversion) had (numerical?) issues - """ - return Rotation.eu2qu(Rotation.om2eu(om)) + The original formulation (direct conversion) had (numerical?) issues + """ + return Rotation.eu2qu(Rotation.om2eu(om)) @staticmethod def om2eu(om): - """Rotation matrix to Bunge-Euler angles.""" - if abs(om[2,2]) < 1.0: - zeta = 1.0/np.sqrt(1.0-om[2,2]**2) - eu = np.array([np.arctan2(om[2,0]*zeta,-om[2,1]*zeta), - np.arccos(om[2,2]), - np.arctan2(om[0,2]*zeta, om[1,2]*zeta)]) - else: - eu = np.array([np.arctan2( om[0,1],om[0,0]), np.pi*0.5*(1-om[2,2]),0.0]) # following the paper, not the reference implementation + """Rotation matrix to Bunge-Euler angles.""" + if abs(om[2,2]) < 1.0: + zeta = 1.0/np.sqrt(1.0-om[2,2]**2) + eu = np.array([np.arctan2(om[2,0]*zeta,-om[2,1]*zeta), + np.arccos(om[2,2]), + np.arctan2(om[0,2]*zeta, om[1,2]*zeta)]) + else: + eu = np.array([np.arctan2( om[0,1],om[0,0]), np.pi*0.5*(1-om[2,2]),0.0]) # following the paper, not the reference implementation - # reduce Euler angles to definition range, i.e a lower limit of 0.0 - eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) - return eu + # reduce Euler angles to definition range, i.e a lower limit of 0.0 + eu = np.where(eu<0, (eu+2.0*np.pi)%np.array([2.0*np.pi,np.pi,2.0*np.pi]),eu) + return eu @staticmethod def om2ax(om): - """Rotation matrix to axis angle pair.""" - ax=np.empty(4) + """Rotation matrix to axis angle pair.""" + ax=np.empty(4) - # first get the rotation angle - t = 0.5*(om.trace() -1.0) - ax[3] = np.arccos(np.clip(t,-1.0,1.0)) + # first get the rotation angle + t = 0.5*(om.trace() -1.0) + ax[3] = np.arccos(np.clip(t,-1.0,1.0)) - if iszero(ax[3]): - ax = [ 0.0, 0.0, 1.0, 0.0] - else: - w,vr = np.linalg.eig(om) - # next, find the eigenvalue (1,0j) - i = np.where(np.isclose(w,1.0+0.0j))[0][0] - ax[0:3] = np.real(vr[0:3,i]) - diagDelta = np.array([om[1,2]-om[2,1],om[2,0]-om[0,2],om[0,1]-om[1,0]]) - ax[0:3] = np.where(iszero(diagDelta), ax[0:3],np.abs(ax[0:3])*np.sign(-P*diagDelta)) - - return np.array(ax) + if iszero(ax[3]): + ax = [ 0.0, 0.0, 1.0, 0.0] + else: + w,vr = np.linalg.eig(om) + # next, find the eigenvalue (1,0j) + i = np.where(np.isclose(w,1.0+0.0j))[0][0] + ax[0:3] = np.real(vr[0:3,i]) + diagDelta = np.array([om[1,2]-om[2,1],om[2,0]-om[0,2],om[0,1]-om[1,0]]) + ax[0:3] = np.where(iszero(diagDelta), ax[0:3],np.abs(ax[0:3])*np.sign(-P*diagDelta)) + return np.array(ax) @staticmethod def om2ro(om): - """Rotation matrix to Rodriques-Frank vector.""" - return Rotation.eu2ro(Rotation.om2eu(om)) + """Rotation matrix to Rodriques-Frank vector.""" + return Rotation.eu2ro(Rotation.om2eu(om)) @staticmethod def om2ho(om): - """Rotation matrix to homochoric vector.""" - return Rotation.ax2ho(Rotation.om2ax(om)) + """Rotation matrix to homochoric vector.""" + return Rotation.ax2ho(Rotation.om2ax(om)) @staticmethod def om2cu(om): - """Rotation matrix to cubochoric vector.""" - return Rotation.ho2cu(Rotation.om2ho(om)) + """Rotation matrix to cubochoric vector.""" + return Rotation.ho2cu(Rotation.om2ho(om)) #---------- Bunge-Euler angles ---------- @staticmethod def eu2qu(eu): - """Bunge-Euler angles to quaternion.""" - ee = 0.5*eu - cPhi = np.cos(ee[1]) - sPhi = np.sin(ee[1]) - qu = np.array([ cPhi*np.cos(ee[0]+ee[2]), - -P*sPhi*np.cos(ee[0]-ee[2]), - -P*sPhi*np.sin(ee[0]-ee[2]), - -P*cPhi*np.sin(ee[0]+ee[2]) ]) - if qu[0] < 0.0: qu*=-1 - return qu + """Bunge-Euler angles to quaternion.""" + ee = 0.5*eu + cPhi = np.cos(ee[1]) + sPhi = np.sin(ee[1]) + qu = np.array([ cPhi*np.cos(ee[0]+ee[2]), + -P*sPhi*np.cos(ee[0]-ee[2]), + -P*sPhi*np.sin(ee[0]-ee[2]), + -P*cPhi*np.sin(ee[0]+ee[2]) ]) + if qu[0] < 0.0: qu*=-1 + return qu @staticmethod def eu2om(eu): - """Bunge-Euler angles to rotation matrix.""" - c = np.cos(eu) - s = np.sin(eu) + """Bunge-Euler angles to rotation matrix.""" + c = np.cos(eu) + s = np.sin(eu) - om = np.array([[+c[0]*c[2]-s[0]*s[2]*c[1], +s[0]*c[2]+c[0]*s[2]*c[1], +s[2]*s[1]], - [-c[0]*s[2]-s[0]*c[2]*c[1], -s[0]*s[2]+c[0]*c[2]*c[1], +c[2]*s[1]], - [+s[0]*s[1], -c[0]*s[1], +c[1] ]]) + om = np.array([[+c[0]*c[2]-s[0]*s[2]*c[1], +s[0]*c[2]+c[0]*s[2]*c[1], +s[2]*s[1]], + [-c[0]*s[2]-s[0]*c[2]*c[1], -s[0]*s[2]+c[0]*c[2]*c[1], +c[2]*s[1]], + [+s[0]*s[1], -c[0]*s[1], +c[1] ]]) - om[np.where(iszero(om))] = 0.0 - return om + om[np.where(iszero(om))] = 0.0 + return om @staticmethod def eu2ax(eu): - """Bunge-Euler angles to axis angle pair.""" - t = np.tan(eu[1]*0.5) - sigma = 0.5*(eu[0]+eu[2]) - delta = 0.5*(eu[0]-eu[2]) - tau = np.linalg.norm([t,np.sin(sigma)]) - alpha = np.pi if iszero(np.cos(sigma)) else \ - 2.0*np.arctan(tau/np.cos(sigma)) + """Bunge-Euler angles to axis angle pair.""" + t = np.tan(eu[1]*0.5) + sigma = 0.5*(eu[0]+eu[2]) + delta = 0.5*(eu[0]-eu[2]) + tau = np.linalg.norm([t,np.sin(sigma)]) + alpha = np.pi if iszero(np.cos(sigma)) else \ + 2.0*np.arctan(tau/np.cos(sigma)) - if iszero(alpha): - ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) - else: - ax = -P/tau * np.array([ t*np.cos(delta), t*np.sin(delta), np.sin(sigma) ]) # passive axis angle pair so a minus sign in front - ax = np.append(ax,alpha) - if alpha < 0.0: ax *= -1.0 # ensure alpha is positive - - return ax + if iszero(alpha): + ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) + else: + ax = -P/tau * np.array([ t*np.cos(delta), t*np.sin(delta), np.sin(sigma) ]) # passive axis angle pair so a minus sign in front + ax = np.append(ax,alpha) + if alpha < 0.0: ax *= -1.0 # ensure alpha is positive + return ax @staticmethod def eu2ro(eu): - """Bunge-Euler angles to Rodriques-Frank vector.""" - ro = eu2ax(eu) # convert to axis angle pair representation - if ro[3] >= np.pi: # Differs from original implementation. check convention 5 - ro[3] = np.inf - elif iszero(ro[3]): - ro = np.array([ 0.0, 0.0, P, 0.0 ]) - else: - ro[3] = np.tan(ro[3]*0.5) - - return ro + """Bunge-Euler angles to Rodriques-Frank vector.""" + ro = eu2ax(eu) # convert to axis angle pair representation + if ro[3] >= np.pi: # Differs from original implementation. check convention 5 + ro[3] = np.inf + elif iszero(ro[3]): + ro = np.array([ 0.0, 0.0, P, 0.0 ]) + else: + ro[3] = np.tan(ro[3]*0.5) + return ro @staticmethod def eu2ho(eu): - """Bunge-Euler angles to homochoric vector.""" - return Rotation.ax2ho(Rotation.eu2ax(eu)) + """Bunge-Euler angles to homochoric vector.""" + return Rotation.ax2ho(Rotation.eu2ax(eu)) @staticmethod def eu2cu(eu): - """Bunge-Euler angles to cubochoric vector.""" - return Rotation.ho2cu(Rotation.eu2ho(eu)) + """Bunge-Euler angles to cubochoric vector.""" + return Rotation.ho2cu(Rotation.eu2ho(eu)) #---------- Axis angle pair ---------- @staticmethod def ax2qu(ax): - """Axis angle pair to quaternion.""" - if iszero(ax[3]): - qu = np.array([ 1.0, 0.0, 0.0, 0.0 ]) - else: - c = np.cos(ax[3]*0.5) - s = np.sin(ax[3]*0.5) - qu = np.array([ c, ax[0]*s, ax[1]*s, ax[2]*s ]) - - return qu + """Axis angle pair to quaternion.""" + if iszero(ax[3]): + qu = np.array([ 1.0, 0.0, 0.0, 0.0 ]) + else: + c = np.cos(ax[3]*0.5) + s = np.sin(ax[3]*0.5) + qu = np.array([ c, ax[0]*s, ax[1]*s, ax[2]*s ]) + return qu @staticmethod def ax2om(ax): - """Axis angle pair to rotation matrix.""" - c = np.cos(ax[3]) - s = np.sin(ax[3]) - omc = 1.0-c - om=np.diag(ax[0:3]**2*omc + c) + """Axis angle pair to rotation matrix.""" + c = np.cos(ax[3]) + s = np.sin(ax[3]) + omc = 1.0-c + om=np.diag(ax[0:3]**2*omc + c) - for idx in [[0,1,2],[1,2,0],[2,0,1]]: - q = omc*ax[idx[0]] * ax[idx[1]] - om[idx[0],idx[1]] = q + s*ax[idx[2]] - om[idx[1],idx[0]] = q - s*ax[idx[2]] - - return om if P < 0.0 else om.T + for idx in [[0,1,2],[1,2,0],[2,0,1]]: + q = omc*ax[idx[0]] * ax[idx[1]] + om[idx[0],idx[1]] = q + s*ax[idx[2]] + om[idx[1],idx[0]] = q - s*ax[idx[2]] + return om if P < 0.0 else om.T @staticmethod def ax2eu(ax): - """Rotation matrix to Bunge Euler angles.""" - return Rotation.om2eu(Rotation.ax2om(ax)) + """Rotation matrix to Bunge Euler angles.""" + return Rotation.om2eu(Rotation.ax2om(ax)) @staticmethod def ax2ro(ax): - """Axis angle pair to Rodriques-Frank vector.""" - if iszero(ax[3]): - ro = [ 0.0, 0.0, P, 0.0 ] - else: - ro = [ax[0], ax[1], ax[2]] - # 180 degree case - ro += [np.inf] if np.isclose(ax[3],np.pi,atol=1.0e-15,rtol=0.0) else \ - [np.tan(ax[3]*0.5)] - - return np.array(ro) + """Axis angle pair to Rodriques-Frank vector.""" + if iszero(ax[3]): + ro = [ 0.0, 0.0, P, 0.0 ] + else: + ro = [ax[0], ax[1], ax[2]] + # 180 degree case + ro += [np.inf] if np.isclose(ax[3],np.pi,atol=1.0e-15,rtol=0.0) else \ + [np.tan(ax[3]*0.5)] + return np.array(ro) @staticmethod def ax2ho(ax): - """Axis angle pair to homochoric vector.""" - f = (0.75 * ( ax[3] - np.sin(ax[3]) ))**(1.0/3.0) - ho = ax[0:3] * f - return ho + """Axis angle pair to homochoric vector.""" + f = (0.75 * ( ax[3] - np.sin(ax[3]) ))**(1.0/3.0) + ho = ax[0:3] * f + return ho @staticmethod def ax2cu(ax): - """Axis angle pair to cubochoric vector.""" - return Rotation.ho2cu(Rotation.ax2ho(ax)) + """Axis angle pair to cubochoric vector.""" + return Rotation.ho2cu(Rotation.ax2ho(ax)) #---------- Rodrigues-Frank vector ---------- @staticmethod def ro2qu(ro): - """Rodriques-Frank vector to quaternion.""" - return Rotation.ax2qu(Rotation.ro2ax(ro)) + """Rodriques-Frank vector to quaternion.""" + return Rotation.ax2qu(Rotation.ro2ax(ro)) @staticmethod def ro2om(ro): - """Rodgrigues-Frank vector to rotation matrix.""" - return Rotation.ax2om(Rotation.ro2ax(ro)) + """Rodgrigues-Frank vector to rotation matrix.""" + return Rotation.ax2om(Rotation.ro2ax(ro)) @staticmethod def ro2eu(ro): - """Rodriques-Frank vector to Bunge-Euler angles.""" - return Rotation.om2eu(Rotation.ro2om(ro)) + """Rodriques-Frank vector to Bunge-Euler angles.""" + return Rotation.om2eu(Rotation.ro2om(ro)) @staticmethod def ro2ax(ro): - """Rodriques-Frank vector to axis angle pair.""" - ta = ro[3] + """Rodriques-Frank vector to axis angle pair.""" + ta = ro[3] - if iszero(ta): - ax = [ 0.0, 0.0, 1.0, 0.0 ] - elif not np.isfinite(ta): - ax = [ ro[0], ro[1], ro[2], np.pi ] - else: - angle = 2.0*np.arctan(ta) - ta = 1.0/np.linalg.norm(ro[0:3]) - ax = [ ro[0]/ta, ro[1]/ta, ro[2]/ta, angle ] - - return np.array(ax) + if iszero(ta): + ax = [ 0.0, 0.0, 1.0, 0.0 ] + elif not np.isfinite(ta): + ax = [ ro[0], ro[1], ro[2], np.pi ] + else: + angle = 2.0*np.arctan(ta) + ta = 1.0/np.linalg.norm(ro[0:3]) + ax = [ ro[0]/ta, ro[1]/ta, ro[2]/ta, angle ] + return np.array(ax) @staticmethod def ro2ho(ro): - """Rodriques-Frank vector to homochoric vector.""" - if iszero(np.sum(ro[0:3]**2.0)): - ho = [ 0.0, 0.0, 0.0 ] - else: - f = 2.0*np.arctan(ro[3]) -np.sin(2.0*np.arctan(ro[3])) if np.isfinite(ro[3]) else np.pi - ho = ro[0:3] * (0.75*f)**(1.0/3.0) - - return np.array(ho) + """Rodriques-Frank vector to homochoric vector.""" + if iszero(np.sum(ro[0:3]**2.0)): + ho = [ 0.0, 0.0, 0.0 ] + else: + f = 2.0*np.arctan(ro[3]) -np.sin(2.0*np.arctan(ro[3])) if np.isfinite(ro[3]) else np.pi + ho = ro[0:3] * (0.75*f)**(1.0/3.0) + return np.array(ho) @staticmethod def ro2cu(ro): - """Rodriques-Frank vector to cubochoric vector.""" - return ho2cu(ro2ho(ro)) + """Rodriques-Frank vector to cubochoric vector.""" + return ho2cu(ro2ho(ro)) #---------- Homochoric vector---------- @staticmethod def ho2qu(ho): - """Homochoric vector to quaternion.""" - return Rotation.ax2qu(Rotation.ho2ax(ho)) + """Homochoric vector to quaternion.""" + return Rotation.ax2qu(Rotation.ho2ax(ho)) @staticmethod def ho2om(ho): - """Homochoric vector to rotation matrix.""" - return Rotation.ax2om(Rotation.ho2ax(ho)) + """Homochoric vector to rotation matrix.""" + return Rotation.ax2om(Rotation.ho2ax(ho)) @staticmethod def ho2eu(ho): - """Homochoric vector to Bunge-Euler angles.""" - return Rotation.ax2eu(Rotation.ho2ax(ho)) + """Homochoric vector to Bunge-Euler angles.""" + return Rotation.ax2eu(Rotation.ho2ax(ho)) @staticmethod def ho2ax(ho): - """Homochoric vector to axis angle pair.""" - tfit = np.array([+1.0000000000018852, -0.5000000002194847, - -0.024999992127593126, -0.003928701544781374, - -0.0008152701535450438, -0.0002009500426119712, - -0.00002397986776071756, -0.00008202868926605841, - +0.00012448715042090092, -0.0001749114214822577, - +0.0001703481934140054, -0.00012062065004116828, - +0.000059719705868660826, -0.00001980756723965647, - +0.000003953714684212874, -0.00000036555001439719544]) - # normalize h and store the magnitude - hmag_squared = np.sum(ho**2.) - if iszero(hmag_squared): - ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) - else: - hm = hmag_squared + """Homochoric vector to axis angle pair.""" + tfit = np.array([+1.0000000000018852, -0.5000000002194847, + -0.024999992127593126, -0.003928701544781374, + -0.0008152701535450438, -0.0002009500426119712, + -0.00002397986776071756, -0.00008202868926605841, + +0.00012448715042090092, -0.0001749114214822577, + +0.0001703481934140054, -0.00012062065004116828, + +0.000059719705868660826, -0.00001980756723965647, + +0.000003953714684212874, -0.00000036555001439719544]) + # normalize h and store the magnitude + hmag_squared = np.sum(ho**2.) + if iszero(hmag_squared): + ax = np.array([ 0.0, 0.0, 1.0, 0.0 ]) + else: + hm = hmag_squared - # convert the magnitude to the rotation angle - s = tfit[0] + tfit[1] * hmag_squared - for i in range(2,16): - hm *= hmag_squared - s += tfit[i] * hm - ax = np.append(ho/np.sqrt(hmag_squared),2.0*np.arccos(np.clip(s,-1.0,1.0))) - return ax + # convert the magnitude to the rotation angle + s = tfit[0] + tfit[1] * hmag_squared + for i in range(2,16): + hm *= hmag_squared + s += tfit[i] * hm + ax = np.append(ho/np.sqrt(hmag_squared),2.0*np.arccos(np.clip(s,-1.0,1.0))) + return ax @staticmethod def ho2ro(ho): - """Axis angle pair to Rodriques-Frank vector.""" - return Rotation.ax2ro(Rotation.ho2ax(ho)) + """Axis angle pair to Rodriques-Frank vector.""" + return Rotation.ax2ro(Rotation.ho2ax(ho)) @staticmethod def ho2cu(ho): - """Homochoric vector to cubochoric vector.""" - return Lambert.BallToCube(ho) + """Homochoric vector to cubochoric vector.""" + return Lambert.BallToCube(ho) #---------- Cubochoric ---------- @staticmethod def cu2qu(cu): - """Cubochoric vector to quaternion.""" - return Rotation.ho2qu(Rotation.cu2ho(cu)) + """Cubochoric vector to quaternion.""" + return Rotation.ho2qu(Rotation.cu2ho(cu)) @staticmethod def cu2om(cu): - """Cubochoric vector to rotation matrix.""" - return Rotation.ho2om(Rotation.cu2ho(cu)) + """Cubochoric vector to rotation matrix.""" + return Rotation.ho2om(Rotation.cu2ho(cu)) @staticmethod def cu2eu(cu): - """Cubochoric vector to Bunge-Euler angles.""" - return Rotation.ho2eu(Rotation.cu2ho(cu)) + """Cubochoric vector to Bunge-Euler angles.""" + return Rotation.ho2eu(Rotation.cu2ho(cu)) @staticmethod def cu2ax(cu): - """Cubochoric vector to axis angle pair.""" - return Rotation.ho2ax(Rotation.cu2ho(cu)) + """Cubochoric vector to axis angle pair.""" + return Rotation.ho2ax(Rotation.cu2ho(cu)) @staticmethod def cu2ro(cu): - """Cubochoric vector to Rodriques-Frank vector.""" - return Rotation.ho2ro(Rotation.cu2ho(cu)) + """Cubochoric vector to Rodriques-Frank vector.""" + return Rotation.ho2ro(Rotation.cu2ho(cu)) @staticmethod def cu2ho(cu): - """Cubochoric vector to homochoric vector.""" - return Lambert.CubeToBall(cu) + """Cubochoric vector to homochoric vector.""" + return Lambert.CubeToBall(cu) From c2ae657f5b531e330f9e9f3f79ebe4b7f208bc4e Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Fri, 21 Feb 2020 21:04:23 +0100 Subject: [PATCH 08/13] arguments should be documented --- python/damask/util.py | 65 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/python/damask/util.py b/python/damask/util.py index 0065daba5..ee03b6bf6 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -40,7 +40,17 @@ class bcolors: # ----------------------------- def srepr(arg,glue = '\n'): - """Joins arguments as individual lines.""" + """ + Join arguments as individual lines. + + Parameters + ---------- + arg : iterable + Items to join. + glue : str, optional + Defaults to \n. + + """ if (not hasattr(arg, "strip") and (hasattr(arg, "__getitem__") or hasattr(arg, "__iter__"))): @@ -49,7 +59,17 @@ def srepr(arg,glue = '\n'): # ----------------------------- def croak(what, newline = True): - """Writes formated to stderr.""" + """ + Write formated to stderr. + + Parameters + ---------- + what : str or iterable + Content to be displayed + newline : bool, optional + Separate items of what by newline. Defaults to True. + + """ if what is not None: sys.stderr.write(srepr(what,glue = '\n') + ('\n' if newline else '')) sys.stderr.flush() @@ -57,7 +77,12 @@ def croak(what, newline = True): # ----------------------------- def report(who = None, what = None): - """Reports script and file name.""" + """ + Reports script and file name. + + DEPRECATED + + """ croak( (emph(who)+': ' if who is not None else '') + (what if what is not None else '') + '\n' ) @@ -86,7 +111,22 @@ def execute(cmd, streamIn = None, wd = './', env = None): - """Executes a command in given directory and returns stdout and stderr for optional stdin.""" + """ + Execute command. + + + Parameters + ---------- + cmd : str + Command to be executed. + streanIn :, optional + Input (via pipe) for executed process. + wd : str, optional + Working directory of process. Defaults to ./ . + env : + Environment + + """ initialPath = os.getcwd() os.chdir(wd) myEnv = os.environ if env is None else env @@ -109,6 +149,7 @@ class extendableOption(Option): Used for definition of new option parser action 'extend', which enables to take multiple option arguments. Adopted from online tutorial http://docs.python.org/library/optparse.html + DEPRECATED """ ACTIONS = Option.ACTIONS + ("extend",) @@ -123,17 +164,21 @@ class extendableOption(Option): else: Option.take_action(self, action, dest, opt, value, values, parser) -# Print iterations progress # from https://gist.github.com/aubricus/f91fb55dc6ba5557fbab06119420dd6a def progressBar(iteration, total, prefix='', bar_length=50): """ Call in a loop to create terminal progress bar. - @params: - iteration - Required : current iteration (Int) - total - Required : total iterations (Int) - prefix - Optional : prefix string (Str) - bar_length - Optional : character length of bar (Int) + Parameters + ---------- + iteration : int + Current iteration. + total : int + Total iterations. + prefix : str, optional + Prefix string. + bar_length : int, optional + Character length of bar. Defaults to 50. """ fraction = iteration / float(total) if not hasattr(progressBar, "last_fraction"): # first call to function From 1dddfa040e699447df04049a0b83b6910d5d8597 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Fri, 21 Feb 2020 23:25:22 +0100 Subject: [PATCH 09/13] adjusting indents should be always 4 spaces --- python/damask/colormaps.py | 558 ++++++++++++++++++------------------- python/damask/util.py | 313 ++++++++++----------- 2 files changed, 434 insertions(+), 437 deletions(-) diff --git a/python/damask/colormaps.py b/python/damask/colormaps.py index e4183e830..e84d3338b 100644 --- a/python/damask/colormaps.py +++ b/python/damask/colormaps.py @@ -1,359 +1,355 @@ import numpy as np class Color(): - """Color representation in and conversion between different color-spaces.""" + """Color representation in and conversion between different color-spaces.""" - __slots__ = [ - 'model', - 'color', - '__dict__', - ] + __slots__ = [ + 'model', + 'color', + '__dict__', + ] -# ------------------------------------------------------------------ - def __init__(self, - model = 'RGB', - color = np.zeros(3,'d')): - """ - Create a Color object. - - Parameters - ---------- - model : string - color model - color : numpy.ndarray - vector representing the color according to the selected model + def __init__(self, + model = 'RGB', + color = np.zeros(3,'d')): + """ + Create a Color object. - """ - self.__transforms__ = \ - {'HSV': {'index': 0, 'next': self._HSV2HSL}, - 'HSL': {'index': 1, 'next': self._HSL2RGB, 'prev': self._HSL2HSV}, - 'RGB': {'index': 2, 'next': self._RGB2XYZ, 'prev': self._RGB2HSL}, - 'XYZ': {'index': 3, 'next': self._XYZ2CIELAB, 'prev': self._XYZ2RGB}, - 'CIELAB': {'index': 4, 'next': self._CIELAB2MSH, 'prev': self._CIELAB2XYZ}, - 'MSH': {'index': 5, 'prev': self._MSH2CIELAB}, - } + Parameters + ---------- + model : string + color model + color : numpy.ndarray + vector representing the color according to the selected model - model = model.upper() - if model not in list(self.__transforms__.keys()): model = 'RGB' - if model == 'RGB' and max(color) > 1.0: # are we RGB255 ? - for i in range(3): - color[i] /= 255.0 # rescale to RGB + """ + self.__transforms__ = \ + {'HSV': {'index': 0, 'next': self._HSV2HSL}, + 'HSL': {'index': 1, 'next': self._HSL2RGB, 'prev': self._HSL2HSV}, + 'RGB': {'index': 2, 'next': self._RGB2XYZ, 'prev': self._RGB2HSL}, + 'XYZ': {'index': 3, 'next': self._XYZ2CIELAB, 'prev': self._XYZ2RGB}, + 'CIELAB': {'index': 4, 'next': self._CIELAB2MSH, 'prev': self._CIELAB2XYZ}, + 'MSH': {'index': 5, 'prev': self._MSH2CIELAB}, + } - if model == 'HSL': # are we HSL ? - if abs(color[0]) > 1.0: color[0] /= 360.0 # with angular hue? - while color[0] >= 1.0: color[0] -= 1.0 # rewind to proper range - while color[0] < 0.0: color[0] += 1.0 # rewind to proper range + model = model.upper() + if model not in list(self.__transforms__.keys()): model = 'RGB' + if model == 'RGB' and max(color) > 1.0: # are we RGB255 ? + for i in range(3): + color[i] /= 255.0 # rescale to RGB - self.model = model - self.color = np.array(color,'d') + if model == 'HSL': # are we HSL ? + if abs(color[0]) > 1.0: color[0] /= 360.0 # with angular hue? + while color[0] >= 1.0: color[0] -= 1.0 # rewind to proper range + while color[0] < 0.0: color[0] += 1.0 # rewind to proper range + + self.model = model + self.color = np.array(color,'d') -# ------------------------------------------------------------------ - def __repr__(self): - """Color model and values.""" - return 'Model: %s Color: %s'%(self.model,str(self.color)) + def __repr__(self): + """Color model and values.""" + return 'Model: %s Color: %s'%(self.model,str(self.color)) -# ------------------------------------------------------------------ - def __str__(self): - """Color model and values.""" - return self.__repr__() + def __str__(self): + """Color model and values.""" + return self.__repr__() -# ------------------------------------------------------------------ - def convertTo(self,toModel = 'RGB'): - """ - Change the color model permanently. - - Parameters - ---------- - toModel : string - color model + def convert_to(self,toModel = 'RGB'): + """ + Change the color model permanently. - """ - toModel = toModel.upper() - if toModel not in list(self.__transforms__.keys()): return + Parameters + ---------- + toModel : string + color model - sourcePos = self.__transforms__[self.model]['index'] - targetPos = self.__transforms__[toModel]['index'] + """ + toModel = toModel.upper() + if toModel not in list(self.__transforms__.keys()): return - while sourcePos < targetPos: - self.__transforms__[self.model]['next']() - sourcePos += 1 + sourcePos = self.__transforms__[self.model]['index'] + targetPos = self.__transforms__[toModel]['index'] - while sourcePos > targetPos: - self.__transforms__[self.model]['prev']() - sourcePos -= 1 - return self + while sourcePos < targetPos: + self.__transforms__[self.model]['next']() + sourcePos += 1 + + while sourcePos > targetPos: + self.__transforms__[self.model]['prev']() + sourcePos -= 1 + return self -# ------------------------------------------------------------------ - def expressAs(self,asModel = 'RGB'): - """ - Return the color in a different model. - - Parameters - ---------- - asModel : string - color model + def express_as(self,asModel = 'RGB'): + """ + Return the color in a different model. - """ - return self.__class__(self.model,self.color).convertTo(asModel) + Parameters + ---------- + asModel : string + color model + + """ + return self.__class__(self.model,self.color).convert_to(asModel) - def _HSV2HSL(self): - """ - Convert H(ue) S(aturation) V(alue or brightness) to H(ue) S(aturation) L(uminance). + def _HSV2HSL(self): + """ + Convert H(ue) S(aturation) V(alue or brightness) to H(ue) S(aturation) L(uminance). - All values are in the range [0,1] - http://codeitdown.com/hsl-hsb-hsv-color - """ - if self.model != 'HSV': return + All values are in the range [0,1] + http://codeitdown.com/hsl-hsb-hsv-color + """ + if self.model != 'HSV': return - converted = Color('HSL',np.array([ - self.color[0], - 1. if self.color[2] == 0.0 or (self.color[1] == 0.0 and self.color[2] == 1.0) \ - else self.color[1]*self.color[2]/(1.-abs(self.color[2]*(2.-self.color[1])-1.)), - 0.5*self.color[2]*(2.-self.color[1]), - ])) + converted = Color('HSL',np.array([ + self.color[0], + 1. if self.color[2] == 0.0 or (self.color[1] == 0.0 and self.color[2] == 1.0) \ + else self.color[1]*self.color[2]/(1.-abs(self.color[2]*(2.-self.color[1])-1.)), + 0.5*self.color[2]*(2.-self.color[1]), + ])) - self.model = converted.model - self.color = converted.color + self.model = converted.model + self.color = converted.color - def _HSL2HSV(self): - """ - Convert H(ue) S(aturation) L(uminance) to H(ue) S(aturation) V(alue or brightness). + def _HSL2HSV(self): + """ + Convert H(ue) S(aturation) L(uminance) to H(ue) S(aturation) V(alue or brightness). - All values are in the range [0,1] - http://codeitdown.com/hsl-hsb-hsv-color - """ - if self.model != 'HSL': return + All values are in the range [0,1] + http://codeitdown.com/hsl-hsb-hsv-color + """ + if self.model != 'HSL': return - h = self.color[0] - b = self.color[2]+0.5*(self.color[1]*(1.-abs(2*self.color[2]-1))) - s = 1.0 if b == 0.0 else 2.*(b-self.color[2])/b + h = self.color[0] + b = self.color[2]+0.5*(self.color[1]*(1.-abs(2*self.color[2]-1))) + s = 1.0 if b == 0.0 else 2.*(b-self.color[2])/b - converted = Color('HSV',np.array([h,s,b])) + converted = Color('HSV',np.array([h,s,b])) - self.model = converted.model - self.color = converted.color + self.model = converted.model + self.color = converted.color - def _HSL2RGB(self): - """ - Convert H(ue) S(aturation) L(uminance) to R(red) G(reen) B(lue). + def _HSL2RGB(self): + """ + Convert H(ue) S(aturation) L(uminance) to R(red) G(reen) B(lue). - All values are in the range [0,1] - from http://en.wikipedia.org/wiki/HSL_and_HSV - """ - if self.model != 'HSL': return + All values are in the range [0,1] + from http://en.wikipedia.org/wiki/HSL_and_HSV + """ + if self.model != 'HSL': return - sextant = self.color[0]*6.0 - c = (1.0 - abs(2.0 * self.color[2] - 1.0))*self.color[1] - x = c*(1.0 - abs(sextant%2 - 1.0)) - m = self.color[2] - 0.5*c + sextant = self.color[0]*6.0 + c = (1.0 - abs(2.0 * self.color[2] - 1.0))*self.color[1] + x = c*(1.0 - abs(sextant%2 - 1.0)) + m = self.color[2] - 0.5*c - converted = Color('RGB',np.array([ - [c+m, x+m, m], - [x+m, c+m, m], - [m, c+m, x+m], - [m, x+m, c+m], - [x+m, m, c+m], - [c+m, m, x+m], - ][int(sextant)],'d')) - self.model = converted.model - self.color = converted.color + converted = Color('RGB',np.array([ + [c+m, x+m, m], + [x+m, c+m, m], + [m, c+m, x+m], + [m, x+m, c+m], + [x+m, m, c+m], + [c+m, m, x+m], + ][int(sextant)],'d')) + self.model = converted.model + self.color = converted.color - def _RGB2HSL(self): - """ - Convert R(ed) G(reen) B(lue) to H(ue) S(aturation) L(uminance). + def _RGB2HSL(self): + """ + Convert R(ed) G(reen) B(lue) to H(ue) S(aturation) L(uminance). - All values are in the range [0,1] - from http://130.113.54.154/~monger/hsl-rgb.html - """ - if self.model != 'RGB': return + All values are in the range [0,1] + from http://130.113.54.154/~monger/hsl-rgb.html + """ + if self.model != 'RGB': return - HSL = np.zeros(3,'d') - maxcolor = self.color.max() - mincolor = self.color.min() - HSL[2] = (maxcolor + mincolor)/2.0 - if(mincolor == maxcolor): - HSL[0] = 0.0 - HSL[1] = 0.0 - else: - if (HSL[2]<0.5): - HSL[1] = (maxcolor - mincolor)/(maxcolor + mincolor) + HSL = np.zeros(3,'d') + maxcolor = self.color.max() + mincolor = self.color.min() + HSL[2] = (maxcolor + mincolor)/2.0 + if(mincolor == maxcolor): + HSL[0] = 0.0 + HSL[1] = 0.0 else: - HSL[1] = (maxcolor - mincolor)/(2.0 - maxcolor - mincolor) - if (maxcolor == self.color[0]): - HSL[0] = 0.0 + (self.color[1] - self.color[2])/(maxcolor - mincolor) - elif (maxcolor == self.color[1]): - HSL[0] = 2.0 + (self.color[2] - self.color[0])/(maxcolor - mincolor) - elif (maxcolor == self.color[2]): - HSL[0] = 4.0 + (self.color[0] - self.color[1])/(maxcolor - mincolor) - HSL[0] = HSL[0]*60.0 # scaling to 360 might be dangerous for small values - if (HSL[0] < 0.0): - HSL[0] = HSL[0] + 360.0 - for i in range(2): - HSL[i+1] = min(HSL[i+1],1.0) - HSL[i+1] = max(HSL[i+1],0.0) + if (HSL[2]<0.5): + HSL[1] = (maxcolor - mincolor)/(maxcolor + mincolor) + else: + HSL[1] = (maxcolor - mincolor)/(2.0 - maxcolor - mincolor) + if (maxcolor == self.color[0]): + HSL[0] = 0.0 + (self.color[1] - self.color[2])/(maxcolor - mincolor) + elif (maxcolor == self.color[1]): + HSL[0] = 2.0 + (self.color[2] - self.color[0])/(maxcolor - mincolor) + elif (maxcolor == self.color[2]): + HSL[0] = 4.0 + (self.color[0] - self.color[1])/(maxcolor - mincolor) + HSL[0] = HSL[0]*60.0 # scaling to 360 might be dangerous for small values + if (HSL[0] < 0.0): + HSL[0] = HSL[0] + 360.0 + for i in range(2): + HSL[i+1] = min(HSL[i+1],1.0) + HSL[i+1] = max(HSL[i+1],0.0) - converted = Color('HSL', HSL) - self.model = converted.model - self.color = converted.color + converted = Color('HSL', HSL) + self.model = converted.model + self.color = converted.color - def _RGB2XYZ(self): - """ - Convert R(ed) G(reen) B(lue) to CIE XYZ. + def _RGB2XYZ(self): + """ + Convert R(ed) G(reen) B(lue) to CIE XYZ. - All values are in the range [0,1] - from http://www.cs.rit.edu/~ncs/color/t_convert.html - """ - if self.model != 'RGB': return + All values are in the range [0,1] + from http://www.cs.rit.edu/~ncs/color/t_convert.html + """ + if self.model != 'RGB': return - XYZ = np.zeros(3,'d') - RGB_lin = np.zeros(3,'d') - convert = np.array([[0.412453,0.357580,0.180423], - [0.212671,0.715160,0.072169], - [0.019334,0.119193,0.950227]]) + XYZ = np.zeros(3,'d') + RGB_lin = np.zeros(3,'d') + convert = np.array([[0.412453,0.357580,0.180423], + [0.212671,0.715160,0.072169], + [0.019334,0.119193,0.950227]]) - for i in range(3): - if (self.color[i] > 0.04045): RGB_lin[i] = ((self.color[i]+0.0555)/1.0555)**2.4 - else: RGB_lin[i] = self.color[i] /12.92 - XYZ = np.dot(convert,RGB_lin) - for i in range(3): + for i in range(3): + if (self.color[i] > 0.04045): RGB_lin[i] = ((self.color[i]+0.0555)/1.0555)**2.4 + else: RGB_lin[i] = self.color[i] /12.92 + XYZ = np.dot(convert,RGB_lin) + for i in range(3): - XYZ[i] = max(XYZ[i],0.0) + XYZ[i] = max(XYZ[i],0.0) - converted = Color('XYZ', XYZ) - self.model = converted.model - self.color = converted.color + converted = Color('XYZ', XYZ) + self.model = converted.model + self.color = converted.color - def _XYZ2RGB(self): - """ - Convert CIE XYZ to R(ed) G(reen) B(lue). + def _XYZ2RGB(self): + """ + Convert CIE XYZ to R(ed) G(reen) B(lue). - All values are in the range [0,1] - from http://www.cs.rit.edu/~ncs/color/t_convert.html - """ - if self.model != 'XYZ': - return + All values are in the range [0,1] + from http://www.cs.rit.edu/~ncs/color/t_convert.html + """ + if self.model != 'XYZ': + return - convert = np.array([[ 3.240479,-1.537150,-0.498535], - [-0.969256, 1.875992, 0.041556], - [ 0.055648,-0.204043, 1.057311]]) - RGB_lin = np.dot(convert,self.color) - RGB = np.zeros(3,'d') + convert = np.array([[ 3.240479,-1.537150,-0.498535], + [-0.969256, 1.875992, 0.041556], + [ 0.055648,-0.204043, 1.057311]]) + RGB_lin = np.dot(convert,self.color) + RGB = np.zeros(3,'d') - for i in range(3): - if (RGB_lin[i] > 0.0031308): RGB[i] = ((RGB_lin[i])**(1.0/2.4))*1.0555-0.0555 - else: RGB[i] = RGB_lin[i] *12.92 - for i in range(3): - RGB[i] = min(RGB[i],1.0) - RGB[i] = max(RGB[i],0.0) + for i in range(3): + if (RGB_lin[i] > 0.0031308): RGB[i] = ((RGB_lin[i])**(1.0/2.4))*1.0555-0.0555 + else: RGB[i] = RGB_lin[i] *12.92 + for i in range(3): + RGB[i] = min(RGB[i],1.0) + RGB[i] = max(RGB[i],0.0) - maxVal = max(RGB) # clipping colors according to the display gamut - if (maxVal > 1.0): RGB /= maxVal + maxVal = max(RGB) # clipping colors according to the display gamut + if (maxVal > 1.0): RGB /= maxVal - converted = Color('RGB', RGB) - self.model = converted.model - self.color = converted.color + converted = Color('RGB', RGB) + self.model = converted.model + self.color = converted.color - def _CIELAB2XYZ(self): - """ - Convert CIE Lab to CIE XYZ. + def _CIELAB2XYZ(self): + """ + Convert CIE Lab to CIE XYZ. - All values are in the range [0,1] - from http://www.easyrgb.com/index.php?X=MATH&H=07#text7 - """ - if self.model != 'CIELAB': return + All values are in the range [0,1] + from http://www.easyrgb.com/index.php?X=MATH&H=07#text7 + """ + if self.model != 'CIELAB': return - ref_white = np.array([.95047, 1.00000, 1.08883],'d') # Observer = 2, Illuminant = D65 - XYZ = np.zeros(3,'d') + ref_white = np.array([.95047, 1.00000, 1.08883],'d') # Observer = 2, Illuminant = D65 + XYZ = np.zeros(3,'d') - XYZ[1] = (self.color[0] + 16.0 ) / 116.0 - XYZ[0] = XYZ[1] + self.color[1]/ 500.0 - XYZ[2] = XYZ[1] - self.color[2]/ 200.0 + XYZ[1] = (self.color[0] + 16.0 ) / 116.0 + XYZ[0] = XYZ[1] + self.color[1]/ 500.0 + XYZ[2] = XYZ[1] - self.color[2]/ 200.0 - for i in range(len(XYZ)): - if (XYZ[i] > 6./29. ): XYZ[i] = XYZ[i]**3. - else: XYZ[i] = 108./841. * (XYZ[i] - 4./29.) + for i in range(len(XYZ)): + if (XYZ[i] > 6./29. ): XYZ[i] = XYZ[i]**3. + else: XYZ[i] = 108./841. * (XYZ[i] - 4./29.) - converted = Color('XYZ', XYZ*ref_white) - self.model = converted.model - self.color = converted.color - - def _XYZ2CIELAB(self): - """ - Convert CIE XYZ to CIE Lab. - - All values are in the range [0,1] - from http://en.wikipedia.org/wiki/Lab_color_space, - http://www.cs.rit.edu/~ncs/color/t_convert.html - """ - if self.model != 'XYZ': return - - ref_white = np.array([.95047, 1.00000, 1.08883],'d') # Observer = 2, Illuminant = D65 - XYZ = self.color/ref_white - - for i in range(len(XYZ)): - if (XYZ[i] > 216./24389 ): XYZ[i] = XYZ[i]**(1.0/3.0) - else: XYZ[i] = (841./108. * XYZ[i]) + 16.0/116.0 - - converted = Color('CIELAB', np.array([ 116.0 * XYZ[1] - 16.0, - 500.0 * (XYZ[0] - XYZ[1]), - 200.0 * (XYZ[1] - XYZ[2]) ])) - self.model = converted.model - self.color = converted.color + converted = Color('XYZ', XYZ*ref_white) + self.model = converted.model + self.color = converted.color - def _CIELAB2MSH(self): - """ - Convert CIE Lab to Msh colorspace. + def _XYZ2CIELAB(self): + """ + Convert CIE XYZ to CIE Lab. - from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls - """ - if self.model != 'CIELAB': return + All values are in the range [0,1] + from http://en.wikipedia.org/wiki/Lab_color_space, + http://www.cs.rit.edu/~ncs/color/t_convert.html + """ + if self.model != 'XYZ': return - Msh = np.zeros(3,'d') - Msh[0] = np.sqrt(np.dot(self.color,self.color)) - if (Msh[0] > 0.001): - Msh[1] = np.arccos(self.color[0]/Msh[0]) - if (self.color[1] != 0.0): - Msh[2] = np.arctan2(self.color[2],self.color[1]) + ref_white = np.array([.95047, 1.00000, 1.08883],'d') # Observer = 2, Illuminant = D65 + XYZ = self.color/ref_white - converted = Color('MSH', Msh) - self.model = converted.model - self.color = converted.color + for i in range(len(XYZ)): + if (XYZ[i] > 216./24389 ): XYZ[i] = XYZ[i]**(1.0/3.0) + else: XYZ[i] = (841./108. * XYZ[i]) + 16.0/116.0 + + converted = Color('CIELAB', np.array([ 116.0 * XYZ[1] - 16.0, + 500.0 * (XYZ[0] - XYZ[1]), + 200.0 * (XYZ[1] - XYZ[2]) ])) + self.model = converted.model + self.color = converted.color - def _MSH2CIELAB(self): - """ - Convert Msh colorspace to CIE Lab. + def _CIELAB2MSH(self): + """ + Convert CIE Lab to Msh colorspace. - with s,h in radians - from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls - """ - if self.model != 'MSH': return + from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls + """ + if self.model != 'CIELAB': return - Lab = np.zeros(3,'d') - Lab[0] = self.color[0] * np.cos(self.color[1]) - Lab[1] = self.color[0] * np.sin(self.color[1]) * np.cos(self.color[2]) - Lab[2] = self.color[0] * np.sin(self.color[1]) * np.sin(self.color[2]) + Msh = np.zeros(3,'d') + Msh[0] = np.sqrt(np.dot(self.color,self.color)) + if (Msh[0] > 0.001): + Msh[1] = np.arccos(self.color[0]/Msh[0]) + if (self.color[1] != 0.0): + Msh[2] = np.arctan2(self.color[2],self.color[1]) - converted = Color('CIELAB', Lab) - self.model = converted.model - self.color = converted.color + converted = Color('MSH', Msh) + self.model = converted.model + self.color = converted.color + + + def _MSH2CIELAB(self): + """ + Convert Msh colorspace to CIE Lab. + + with s,h in radians + from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls + """ + if self.model != 'MSH': return + + Lab = np.zeros(3,'d') + Lab[0] = self.color[0] * np.cos(self.color[1]) + Lab[1] = self.color[0] * np.sin(self.color[1]) * np.cos(self.color[2]) + Lab[2] = self.color[0] * np.sin(self.color[1]) * np.sin(self.color[2]) + + converted = Color('CIELAB', Lab) + self.model = converted.model + self.color = converted.color class Colormap(): @@ -416,7 +412,7 @@ class Colormap(): ): """ Create a Colormap object. - + Parameters ---------- left : Color @@ -498,13 +494,13 @@ class Colormap(): def interpolate_linear(lo, hi, frac): """Linear interpolation between lo and hi color at given fraction; output in model of lo color.""" interpolation = (1.0 - frac) * np.array(lo.color[:]) \ - + frac * np.array(hi.expressAs(lo.model).color[:]) + + frac * np.array(hi.express_as(lo.model).color[:]) return Color(lo.model,interpolation) if self.interpolate == 'perceptualuniform': - return interpolate_Msh(self.left.expressAs('MSH').color, - self.right.expressAs('MSH').color,fraction) + return interpolate_Msh(self.left.express_as('MSH').color, + self.right.express_as('MSH').color,fraction) elif self.interpolate == 'linear': return interpolate_linear(self.left, self.right,fraction) @@ -528,7 +524,7 @@ class Colormap(): """ format = format.lower() # consistent comparison basis frac = 0.5*(np.array(crop) + 1.0) # rescale crop range to fractions - colors = [self.color(float(i)/(steps-1)*(frac[1]-frac[0])+frac[0]).expressAs(model).color for i in range(steps)] + colors = [self.color(float(i)/(steps-1)*(frac[1]-frac[0])+frac[0]).express_as(model).color for i in range(steps)] if format == 'paraview': colormap = ['[\n {{\n "ColorSpace": "RGB", "Name": "{}", "DefaultMap": true,\n "RGBPoints" : ['.format(name)] \ + [' {:4d},{:8.6f},{:8.6f},{:8.6f},'.format(i,color[0],color[1],color[2],) \ diff --git a/python/damask/util.py b/python/damask/util.py index ee03b6bf6..3d17ddd06 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -38,192 +38,193 @@ class bcolors: self.CROSSOUT = '' -# ----------------------------- def srepr(arg,glue = '\n'): - """ - Join arguments as individual lines. - - Parameters - ---------- - arg : iterable - Items to join. - glue : str, optional - Defaults to \n. + """ + Join arguments as individual lines. + + Parameters + ---------- + arg : iterable + Items to join. + glue : str, optional + Defaults to \n. + + """ + if (not hasattr(arg, "strip") and + (hasattr(arg, "__getitem__") or + hasattr(arg, "__iter__"))): + return glue.join(str(x) for x in arg) + return arg if isinstance(arg,str) else repr(arg) - """ - if (not hasattr(arg, "strip") and - (hasattr(arg, "__getitem__") or - hasattr(arg, "__iter__"))): - return glue.join(str(x) for x in arg) - return arg if isinstance(arg,str) else repr(arg) -# ----------------------------- def croak(what, newline = True): - """ - Write formated to stderr. - - Parameters - ---------- - what : str or iterable - Content to be displayed - newline : bool, optional - Separate items of what by newline. Defaults to True. + """ + Write formated to stderr. + + Parameters + ---------- + what : str or iterable + Content to be displayed + newline : bool, optional + Separate items of what by newline. Defaults to True. + + """ + if not what: + sys.stderr.write(srepr(what,glue = '\n') + ('\n' if newline else '')) + sys.stderr.flush() - """ - if what is not None: - sys.stderr.write(srepr(what,glue = '\n') + ('\n' if newline else '')) - sys.stderr.flush() -# ----------------------------- def report(who = None, what = None): - """ - Reports script and file name. + """ + Reports script and file name. - DEPRECATED + DEPRECATED - """ - croak( (emph(who)+': ' if who is not None else '') + (what if what is not None else '') + '\n' ) + """ + croak( (emph(who)+': ' if who is not None else '') + (what if what is not None else '') + '\n' ) -# ----------------------------- def emph(what): - """Formats string with emphasis.""" - return bcolors.BOLD+srepr(what)+bcolors.ENDC + """Formats string with emphasis.""" + return bcolors.BOLD+srepr(what)+bcolors.ENDC + -# ----------------------------- def deemph(what): - """Formats string with deemphasis.""" - return bcolors.DIM+srepr(what)+bcolors.ENDC + """Formats string with deemphasis.""" + return bcolors.DIM+srepr(what)+bcolors.ENDC + -# ----------------------------- def delete(what): - """Formats string as deleted.""" - return bcolors.DIM+srepr(what)+bcolors.ENDC + """Formats string as deleted.""" + return bcolors.DIM+srepr(what)+bcolors.ENDC + -# ----------------------------- def strikeout(what): - """Formats string as strikeout.""" - return bcolors.CROSSOUT+srepr(what)+bcolors.ENDC + """Formats string as strikeout.""" + return bcolors.CROSSOUT+srepr(what)+bcolors.ENDC -# ----------------------------- def execute(cmd, streamIn = None, wd = './', env = None): - """ - Execute command. - - - Parameters - ---------- - cmd : str - Command to be executed. - streanIn :, optional - Input (via pipe) for executed process. - wd : str, optional - Working directory of process. Defaults to ./ . - env : - Environment - - """ - initialPath = os.getcwd() - os.chdir(wd) - myEnv = os.environ if env is None else env - process = subprocess.Popen(shlex.split(cmd), - stdout = subprocess.PIPE, - stderr = subprocess.PIPE, - stdin = subprocess.PIPE, - env = myEnv) - out,error = [i for i in (process.communicate() if streamIn is None - else process.communicate(streamIn.read().encode('utf-8')))] - out = out.decode('utf-8').replace('\x08','') - error = error.decode('utf-8').replace('\x08','') - os.chdir(initialPath) - if process.returncode != 0: raise RuntimeError('{} failed with returncode {}'.format(cmd,process.returncode)) - return out,error - -# ----------------------------- -class extendableOption(Option): - """ - Used for definition of new option parser action 'extend', which enables to take multiple option arguments. - - Adopted from online tutorial http://docs.python.org/library/optparse.html - DEPRECATED - """ - - ACTIONS = Option.ACTIONS + ("extend",) - STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",) - TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",) - ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",) - - def take_action(self, action, dest, opt, value, values, parser): - if action == "extend": - lvalue = value.split(",") - values.ensure_value(dest, []).extend(lvalue) - else: - Option.take_action(self, action, dest, opt, value, values, parser) - -# from https://gist.github.com/aubricus/f91fb55dc6ba5557fbab06119420dd6a -def progressBar(iteration, total, prefix='', bar_length=50): - """ - Call in a loop to create terminal progress bar. - - Parameters - ---------- - iteration : int - Current iteration. - total : int - Total iterations. - prefix : str, optional - Prefix string. - bar_length : int, optional - Character length of bar. Defaults to 50. - """ - fraction = iteration / float(total) - if not hasattr(progressBar, "last_fraction"): # first call to function - progressBar.start_time = time.time() - progressBar.last_fraction = -1.0 - remaining_time = ' n/a' - else: - if fraction <= progressBar.last_fraction or iteration == 0: # reset: called within a new loop - progressBar.start_time = time.time() - progressBar.last_fraction = -1.0 - remaining_time = ' n/a' - else: - progressBar.last_fraction = fraction - remainder = (total - iteration) * (time.time()-progressBar.start_time)/iteration - remaining_time = '{: 3d}:'.format(int( remainder//3600)) + \ - '{:02d}:'.format(int((remainder//60)%60)) + \ - '{:02d}' .format(int( remainder %60)) - - filled_length = int(round(bar_length * fraction)) - bar = '█' * filled_length + '░' * (bar_length - filled_length) - - sys.stderr.write('\r{} {} {}'.format(prefix, bar, remaining_time)), - - if iteration == total: sys.stderr.write('\n') - sys.stderr.flush() - - -class return_message(): - """Object with formatted return message.""" - - def __init__(self,message): """ - Sets return message. + Execute command. + Parameters ---------- - message : str or list of str - message for output to screen + cmd : str + Command to be executed. + streanIn :, optional + Input (via pipe) for executed process. + wd : str, optional + Working directory of process. Defaults to ./ . + env : + Environment """ - self.message = message - - def __repr__(self): - """Return message suitable for interactive shells.""" - return srepr(self.message) + initialPath = os.getcwd() + os.chdir(wd) + myEnv = os.environ if env is None else env + process = subprocess.Popen(shlex.split(cmd), + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + stdin = subprocess.PIPE, + env = myEnv) + out,error = [i for i in (process.communicate() if streamIn is None + else process.communicate(streamIn.read().encode('utf-8')))] + out = out.decode('utf-8').replace('\x08','') + error = error.decode('utf-8').replace('\x08','') + os.chdir(initialPath) + if process.returncode != 0: + raise RuntimeError('{} failed with returncode {}'.format(cmd,process.returncode)) + return out,error + + +class extendableOption(Option): + """ + Used for definition of new option parser action 'extend', which enables to take multiple option arguments. + + Adopted from online tutorial http://docs.python.org/library/optparse.html + DEPRECATED + """ + + ACTIONS = Option.ACTIONS + ("extend",) + STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",) + TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",) + ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",) + + def take_action(self, action, dest, opt, value, values, parser): + if action == "extend": + lvalue = value.split(",") + values.ensure_value(dest, []).extend(lvalue) + else: + Option.take_action(self, action, dest, opt, value, values, parser) + + +def progressBar(iteration, total, prefix='', bar_length=50): + """ + Call in a loop to create terminal progress bar. + + From https://gist.github.com/aubricus/f91fb55dc6ba5557fbab06119420dd6a + + Parameters + ---------- + iteration : int + Current iteration. + total : int + Total iterations. + prefix : str, optional + Prefix string. + bar_length : int, optional + Character length of bar. Defaults to 50. + """ + fraction = iteration / float(total) + if not hasattr(progressBar, "last_fraction"): # first call to function + progressBar.start_time = time.time() + progressBar.last_fraction = -1.0 + remaining_time = ' n/a' + else: + if fraction <= progressBar.last_fraction or iteration == 0: # reset: called within a new loop + progressBar.start_time = time.time() + progressBar.last_fraction = -1.0 + remaining_time = ' n/a' + else: + progressBar.last_fraction = fraction + remainder = (total - iteration) * (time.time()-progressBar.start_time)/iteration + remaining_time = '{: 3d}:'.format(int( remainder//3600)) + \ + '{:02d}:'.format(int((remainder//60)%60)) + \ + '{:02d}' .format(int( remainder %60)) + + filled_length = int(round(bar_length * fraction)) + bar = '█' * filled_length + '░' * (bar_length - filled_length) + + sys.stderr.write('\r{} {} {}'.format(prefix, bar, remaining_time)), + + if iteration == total: + sys.stderr.write('\n') + sys.stderr.flush() + + +class return_message(): + """Object with formatted return message.""" + + def __init__(self,message): + """ + Sets return message. + + Parameters + ---------- + message : str or list of str + message for output to screen + + """ + self.message = message + + def __repr__(self): + """Return message suitable for interactive shells.""" + return srepr(self.message) class ThreadPool: From a6279c8284732dcce16ffc2544836965adb98410 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 22 Feb 2020 00:03:54 +0100 Subject: [PATCH 10/13] use separate test definitions per class --- python/tests/test_Orientation.py | 65 ++++++++++++++++++++++++++++++++ python/tests/test_Rotation.py | 43 --------------------- 2 files changed, 65 insertions(+), 43 deletions(-) create mode 100644 python/tests/test_Orientation.py diff --git a/python/tests/test_Orientation.py b/python/tests/test_Orientation.py new file mode 100644 index 000000000..08f060887 --- /dev/null +++ b/python/tests/test_Orientation.py @@ -0,0 +1,65 @@ +import os +from itertools import permutations + +import pytest +import numpy as np + +import damask +from damask import Rotation +from damask import Orientation +from damask import Lattice + +n = 1000 + +@pytest.fixture +def default(): + """A set of n random rotations.""" + return [Rotation.fromRandom() for r in range(n)] + +@pytest.fixture +def reference_dir(reference_dir_base): + """Directory containing reference results.""" + return os.path.join(reference_dir_base,'Rotation') + + +class TestOrientation: + + @pytest.mark.parametrize('color',[{'label':'red', 'RGB':[1,0,0],'direction':[0,0,1]}, + {'label':'green','RGB':[0,1,0],'direction':[0,1,1]}, + {'label':'blue', 'RGB':[0,0,1],'direction':[1,1,1]}]) + @pytest.mark.parametrize('lattice',['fcc','bcc']) + def test_IPF_cubic(self,default,color,lattice): + cube = damask.Orientation(damask.Rotation(),lattice) + for direction in set(permutations(np.array(color['direction']))): + assert np.allclose(cube.IPFcolor(direction),np.array(color['RGB'])) + + @pytest.mark.parametrize('lattice',Lattice.lattices) + def test_IPF(self,lattice): + direction = np.random.random(3)*2.0-1 + for rot in [Rotation.fromRandom() for r in range(n//100)]: + R = damask.Orientation(rot,lattice) + color = R.IPFcolor(direction) + for equivalent in R.equivalentOrientations(): + assert np.allclose(color,R.IPFcolor(direction)) + + @pytest.mark.parametrize('model',['Bain','KS','GT','GT_prime','NW','Pitsch']) + @pytest.mark.parametrize('lattice',['fcc','bcc']) + def test_relationship_forward_backward(self,model,lattice): + ori = Orientation(Rotation.fromRandom(),lattice) + for i,r in enumerate(ori.relatedOrientations(model)): + ori2 = r.relatedOrientations(model)[i] + misorientation = ori.rotation.misorientation(ori2.rotation) + assert misorientation.asAxisAngle(degrees=True)[3]<1.0e-5 + + @pytest.mark.parametrize('model',['Bain','KS','GT','GT_prime','NW','Pitsch']) + @pytest.mark.parametrize('lattice',['fcc','bcc']) + def test_relationship_reference(self,update,reference_dir,model,lattice): + reference = os.path.join(reference_dir,'{}_{}.txt'.format(lattice,model)) + ori = Orientation(Rotation(),lattice) + eu = np.array([o.rotation.asEulers(degrees=True) for o in ori.relatedOrientations(model)]) + if update: + coords = np.array([(1,i+1) for i,x in enumerate(eu)]) + table = damask.Table(eu,{'Eulers':(3,)}) + table.add('pos',coords) + table.to_ASCII(reference) + assert np.allclose(eu,damask.Table.from_ASCII(reference).get('Eulers')) diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index 012c95469..dd9e20363 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -6,8 +6,6 @@ import numpy as np import damask from damask import Rotation -from damask import Orientation -from damask import Lattice n = 1000 @@ -58,44 +56,3 @@ class TestRotation: for rot in default: assert np.allclose(rot.asCubochoric(), Rotation.fromQuaternion(rot.asQuaternion()).asCubochoric()) - - - @pytest.mark.parametrize('color',[{'label':'red', 'RGB':[1,0,0],'direction':[0,0,1]}, - {'label':'green','RGB':[0,1,0],'direction':[0,1,1]}, - {'label':'blue', 'RGB':[0,0,1],'direction':[1,1,1]}]) - @pytest.mark.parametrize('lattice',['fcc','bcc']) - def test_IPF_cubic(self,default,color,lattice): - cube = damask.Orientation(damask.Rotation(),lattice) - for direction in set(permutations(np.array(color['direction']))): - assert np.allclose(cube.IPFcolor(direction),np.array(color['RGB'])) - - @pytest.mark.parametrize('lattice',Lattice.lattices) - def test_IPF(self,lattice): - direction = np.random.random(3)*2.0-1 - for rot in [Rotation.fromRandom() for r in range(n//100)]: - R = damask.Orientation(rot,lattice) - color = R.IPFcolor(direction) - for equivalent in R.equivalentOrientations(): - assert np.allclose(color,R.IPFcolor(direction)) - - @pytest.mark.parametrize('model',['Bain','KS','GT','GT_prime','NW','Pitsch']) - @pytest.mark.parametrize('lattice',['fcc','bcc']) - def test_relationship_forward_backward(self,model,lattice): - ori = Orientation(Rotation.fromRandom(),lattice) - for i,r in enumerate(ori.relatedOrientations(model)): - ori2 = r.relatedOrientations(model)[i] - misorientation = ori.rotation.misorientation(ori2.rotation) - assert misorientation.asAxisAngle(degrees=True)[3]<1.0e-5 - - @pytest.mark.parametrize('model',['Bain','KS','GT','GT_prime','NW','Pitsch']) - @pytest.mark.parametrize('lattice',['fcc','bcc']) - def test_relationship_reference(self,update,reference_dir,model,lattice): - reference = os.path.join(reference_dir,'{}_{}.txt'.format(lattice,model)) - ori = Orientation(Rotation(),lattice) - eu = np.array([o.rotation.asEulers(degrees=True) for o in ori.relatedOrientations(model)]) - if update: - coords = np.array([(1,i+1) for i,x in enumerate(eu)]) - table = damask.Table(eu,{'Eulers':(3,)}) - table.add('pos',coords) - table.to_ASCII(reference) - assert np.allclose(eu,damask.Table.from_ASCII(reference).get('Eulers')) From fd11f073f02602d419dbc738994244b47b4ae741 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 22 Feb 2020 00:06:51 +0100 Subject: [PATCH 11/13] fixing prospector complaints --- python/damask/lattice.py | 1 - python/damask/rotation.py | 7 +++---- python/damask/util.py | 4 ++-- python/tests/test_Rotation.py | 2 -- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/python/damask/lattice.py b/python/damask/lattice.py index fe3965547..1887bae7c 100644 --- a/python/damask/lattice.py +++ b/python/damask/lattice.py @@ -1,7 +1,6 @@ import numpy as np from .rotation import Rotation -from . import Lambert P = -1 diff --git a/python/damask/rotation.py b/python/damask/rotation.py index 08ec7c564..084e4bf38 100644 --- a/python/damask/rotation.py +++ b/python/damask/rotation.py @@ -219,8 +219,7 @@ class Rotation: def asRodrigues(self, vector = False): """ - Rodrigues-Frank vector representation [n_1, n_2, n_3, tan(ω/2)] unless vector == True: - [n_1, n_2, n_3] * tan(ω/2). + Rodrigues-Frank vector representation [n_1, n_2, n_3, tan(ω/2)] unless vector == True: [n_1, n_2, n_3] * tan(ω/2). Parameters ---------- @@ -634,7 +633,7 @@ class Rotation: @staticmethod def eu2ro(eu): """Bunge-Euler angles to Rodriques-Frank vector.""" - ro = eu2ax(eu) # convert to axis angle pair representation + ro = Rotation.eu2ax(eu) # convert to axis angle pair representation if ro[3] >= np.pi: # Differs from original implementation. check convention 5 ro[3] = np.inf elif iszero(ro[3]): @@ -754,7 +753,7 @@ class Rotation: @staticmethod def ro2cu(ro): """Rodriques-Frank vector to cubochoric vector.""" - return ho2cu(ro2ho(ro)) + return Rotation.ho2cu(Rotation.ro2ho(ro)) #---------- Homochoric vector---------- diff --git a/python/damask/util.py b/python/damask/util.py index 3d17ddd06..553ae0c6e 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -39,7 +39,7 @@ class bcolors: def srepr(arg,glue = '\n'): - """ + r""" Join arguments as individual lines. Parameters @@ -110,7 +110,6 @@ def execute(cmd, env = None): """ Execute command. - Parameters ---------- @@ -179,6 +178,7 @@ def progressBar(iteration, total, prefix='', bar_length=50): Prefix string. bar_length : int, optional Character length of bar. Defaults to 50. + """ fraction = iteration / float(total) if not hasattr(progressBar, "last_fraction"): # first call to function diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index dd9e20363..c545a7172 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -1,10 +1,8 @@ import os -from itertools import permutations import pytest import numpy as np -import damask from damask import Rotation n = 1000 From 9d4cbe516806c319d9e92f615d318e150cedfdcf Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 22 Feb 2020 00:19:27 +0100 Subject: [PATCH 12/13] 4 space indentation --- python/damask/lattice.py | 626 +++++++++++++++++------------------ python/damask/orientation.py | 168 +++++----- python/damask/rotation.py | 6 +- 3 files changed, 398 insertions(+), 402 deletions(-) diff --git a/python/damask/lattice.py b/python/damask/lattice.py index 1887bae7c..ee4cbff3c 100644 --- a/python/damask/lattice.py +++ b/python/damask/lattice.py @@ -312,330 +312,330 @@ class Symmetry: # ****************************************************************************************** class Lattice: - """ - Lattice system. - - Currently, this contains only a mapping from Bravais lattice to symmetry - and orientation relationships. It could include twin and slip systems. - - References - ---------- - https://en.wikipedia.org/wiki/Bravais_lattice - - """ - - lattices = { - 'triclinic':{'symmetry':None}, - 'bct':{'symmetry':'tetragonal'}, - 'hex':{'symmetry':'hexagonal'}, - 'fcc':{'symmetry':'cubic','c/a':1.0}, - 'bcc':{'symmetry':'cubic','c/a':1.0}, - } - - - def __init__(self, lattice): """ - New lattice of given type. + Lattice system. - Parameters - ---------- - lattice : str - Bravais lattice. - - """ - self.lattice = lattice - self.symmetry = Symmetry(self.lattices[lattice]['symmetry']) - - - def __repr__(self): - """Report basic lattice information.""" - return 'Bravais lattice {} ({} symmetry)'.format(self.lattice,self.symmetry) - - - # Kurdjomov--Sachs orientation relationship for fcc <-> bcc transformation - # from S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 - # also see K. Kitahara et al., Acta Materialia 54:1279-1288, 2006 - KS = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]], - [[ 1, 1, -1],[ 0, 1, 1]]],dtype='float'), - 'directions': np.array([ - [[ -1, 0, 1],[ -1, -1, 1]], - [[ -1, 0, 1],[ -1, 1, -1]], - [[ 0, 1, -1],[ -1, -1, 1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ 1, -1, 0],[ -1, -1, 1]], - [[ 1, -1, 0],[ -1, 1, -1]], - [[ 1, 0, -1],[ -1, -1, 1]], - [[ 1, 0, -1],[ -1, 1, -1]], - [[ -1, -1, 0],[ -1, -1, 1]], - [[ -1, -1, 0],[ -1, 1, -1]], - [[ 0, 1, 1],[ -1, -1, 1]], - [[ 0, 1, 1],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ 0, -1, 1],[ -1, 1, -1]], - [[ -1, 0, -1],[ -1, -1, 1]], - [[ -1, 0, -1],[ -1, 1, -1]], - [[ 1, 1, 0],[ -1, -1, 1]], - [[ 1, 1, 0],[ -1, 1, -1]], - [[ -1, 1, 0],[ -1, -1, 1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, -1],[ -1, -1, 1]], - [[ 0, -1, -1],[ -1, 1, -1]], - [[ 1, 0, 1],[ -1, -1, 1]], - [[ 1, 0, 1],[ -1, 1, -1]]],dtype='float')} - - # Greninger--Troiano orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - GT = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 1, 0, 1]], - [[ 1, 1, 1],[ 1, 1, 0]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ -1, 0, 1]], - [[ -1, -1, 1],[ -1, -1, 0]], - [[ -1, -1, 1],[ 0, -1, 1]], - [[ -1, 1, 1],[ -1, 0, 1]], - [[ -1, 1, 1],[ -1, 1, 0]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 1, 0, 1]], - [[ 1, -1, 1],[ 1, -1, 0]], - [[ 1, -1, 1],[ 0, -1, 1]], - [[ 1, 1, 1],[ 1, 1, 0]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 1, 0, 1]], - [[ -1, -1, 1],[ -1, -1, 0]], - [[ -1, -1, 1],[ 0, -1, 1]], - [[ -1, -1, 1],[ -1, 0, 1]], - [[ -1, 1, 1],[ -1, 1, 0]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ -1, 0, 1]], - [[ 1, -1, 1],[ 1, -1, 0]], - [[ 1, -1, 1],[ 0, -1, 1]], - [[ 1, -1, 1],[ 1, 0, 1]]],dtype='float'), - 'directions': np.array([ - [[ -5,-12, 17],[-17, -7, 17]], - [[ 17, -5,-12],[ 17,-17, -7]], - [[-12, 17, -5],[ -7, 17,-17]], - [[ 5, 12, 17],[ 17, 7, 17]], - [[-17, 5,-12],[-17, 17, -7]], - [[ 12,-17, -5],[ 7,-17,-17]], - [[ -5, 12,-17],[-17, 7,-17]], - [[ 17, 5, 12],[ 17, 17, 7]], - [[-12,-17, 5],[ -7,-17, 17]], - [[ 5,-12,-17],[ 17, -7,-17]], - [[-17, -5, 12],[-17,-17, 7]], - [[ 12, 17, 5],[ 7, 17, 17]], - [[ -5, 17,-12],[-17, 17, -7]], - [[-12, -5, 17],[ -7,-17, 17]], - [[ 17,-12, -5],[ 17, -7,-17]], - [[ 5,-17,-12],[ 17,-17, -7]], - [[ 12, 5, 17],[ 7, 17, 17]], - [[-17, 12, -5],[-17, 7,-17]], - [[ -5,-17, 12],[-17,-17, 7]], - [[-12, 5,-17],[ -7, 17,-17]], - [[ 17, 12, 5],[ 17, 7, 17]], - [[ 5, 17, 12],[ 17, 17, 7]], - [[ 12, -5,-17],[ 7,-17,-17]], - [[-17,-12, 5],[-17,-7, 17]]],dtype='float')} - - # Greninger--Troiano' orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - GTprime = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 7, 17, 17],[ 12, 5, 17]], - [[ 17, 7, 17],[ 17, 12, 5]], - [[ 17, 17, 7],[ 5, 17, 12]], - [[ -7,-17, 17],[-12, -5, 17]], - [[-17, -7, 17],[-17,-12, 5]], - [[-17,-17, 7],[ -5,-17, 12]], - [[ 7,-17,-17],[ 12, -5,-17]], - [[ 17, -7,-17],[ 17,-12, -5]], - [[ 17,-17, -7],[ 5,-17,-12]], - [[ -7, 17,-17],[-12, 5,-17]], - [[-17, 7,-17],[-17, 12, -5]], - [[-17, 17, -7],[ -5, 17,-12]], - [[ 7, 17, 17],[ 12, 17, 5]], - [[ 17, 7, 17],[ 5, 12, 17]], - [[ 17, 17, 7],[ 17, 5, 12]], - [[ -7,-17, 17],[-12,-17, 5]], - [[-17, -7, 17],[ -5,-12, 17]], - [[-17,-17, 7],[-17, -5, 12]], - [[ 7,-17,-17],[ 12,-17, -5]], - [[ 17, -7,-17],[ 5, -12,-17]], - [[ 17,-17, -7],[ 17, -5,-12]], - [[ -7, 17,-17],[-12, 17, -5]], - [[-17, 7,-17],[ -5, 12,-17]], - [[-17, 17, -7],[-17, 5,-12]]],dtype='float'), - 'directions': np.array([ - [[ 0, 1, -1],[ 1, 1, -1]], - [[ -1, 0, 1],[ -1, 1, 1]], - [[ 1, -1, 0],[ 1, -1, 1]], - [[ 0, -1, -1],[ -1, -1, -1]], - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, -1, 0],[ 1, -1, -1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ 1, 0, 1],[ 1, 1, 1]], - [[ -1, -1, 0],[ -1, -1, 1]], - [[ 0, -1, -1],[ 1, -1, -1]], - [[ -1, 0, 1],[ -1, -1, 1]], - [[ -1, -1, 0],[ -1, -1, -1]], - [[ 0, -1, 1],[ 1, -1, 1]], - [[ 1, 0, -1],[ 1, 1, -1]], - [[ -1, 1, 0],[ -1, 1, 1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ -1, 0, -1],[ -1, -1, -1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ -1, 0, -1],[ -1, 1, -1]], - [[ 1, 1, 0],[ 1, 1, 1]], - [[ 0, 1, 1],[ 1, 1, 1]], - [[ 1, 0, -1],[ 1, -1, -1]], - [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} - - # Nishiyama--Wassermann orientation relationship for fcc <-> bcc transformation - # from H. Kitahara et al., Materials Characterization 54:378-386, 2005 - NW = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ 1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ -1, 1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ 1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]], - [[ -1, -1, 1],[ 0, 1, 1]]],dtype='float'), - 'directions': np.array([ - [[ 2, -1, -1],[ 0, -1, 1]], - [[ -1, 2, -1],[ 0, -1, 1]], - [[ -1, -1, 2],[ 0, -1, 1]], - [[ -2, -1, -1],[ 0, -1, 1]], - [[ 1, 2, -1],[ 0, -1, 1]], - [[ 1, -1, 2],[ 0, -1, 1]], - [[ 2, 1, -1],[ 0, -1, 1]], - [[ -1, -2, -1],[ 0, -1, 1]], - [[ -1, 1, 2],[ 0, -1, 1]], - [[ 2, -1, 1],[ 0, -1, 1]], #It is wrong in the paper, but matrix is correct - [[ -1, 2, 1],[ 0, -1, 1]], - [[ -1, -1, -2],[ 0, -1, 1]]],dtype='float')} - - # Pitsch orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Acta Materialia 53:1179-1190, 2005 - Pitsch = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 0, 1, 0],[ -1, 0, 1]], - [[ 0, 0, 1],[ 1, -1, 0]], - [[ 1, 0, 0],[ 0, 1, -1]], - [[ 1, 0, 0],[ 0, -1, -1]], - [[ 0, 1, 0],[ -1, 0, -1]], - [[ 0, 0, 1],[ -1, -1, 0]], - [[ 0, 1, 0],[ -1, 0, -1]], - [[ 0, 0, 1],[ -1, -1, 0]], - [[ 1, 0, 0],[ 0, -1, -1]], - [[ 1, 0, 0],[ 0, -1, 1]], - [[ 0, 1, 0],[ 1, 0, -1]], - [[ 0, 0, 1],[ -1, 1, 0]]],dtype='float'), - 'directions': np.array([ - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, 1, 0],[ 1, 1, -1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ 0, 1, -1],[ -1, 1, -1]], - [[ -1, 0, 1],[ -1, -1, 1]], - [[ 1, -1, 0],[ 1, -1, -1]], - [[ 1, 0, -1],[ 1, -1, -1]], - [[ -1, 1, 0],[ -1, 1, -1]], - [[ 0, -1, 1],[ -1, -1, 1]], - [[ 0, 1, 1],[ -1, 1, 1]], - [[ 1, 0, 1],[ 1, -1, 1]], - [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} - - # Bain orientation relationship for fcc <-> bcc transformation - # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - Bain = {'mapping':{'fcc':0,'bcc':1}, - 'planes': np.array([ - [[ 1, 0, 0],[ 1, 0, 0]], - [[ 0, 1, 0],[ 0, 1, 0]], - [[ 0, 0, 1],[ 0, 0, 1]]],dtype='float'), - 'directions': np.array([ - [[ 0, 1, 0],[ 0, 1, 1]], - [[ 0, 0, 1],[ 1, 0, 1]], - [[ 1, 0, 0],[ 1, 1, 0]]],dtype='float')} - - def relationOperations(self,model): - """ - Crystallographic orientation relationships for phase transformations. + Currently, this contains only a mapping from Bravais lattice to symmetry + and orientation relationships. It could include twin and slip systems. References ---------- - S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 - https://doi.org/10.1016/j.jallcom.2012.02.004 - - K. Kitahara et al., Acta Materialia 54(5):1279-1288, 2006 - https://doi.org/10.1016/j.actamat.2005.11.001 - - Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 - https://doi.org/10.1107/S0021889805038276 - - H. Kitahara et al., Materials Characterization 54(4-5):378-386, 2005 - https://doi.org/10.1016/j.matchar.2004.12.015 - - Y. He et al., Acta Materialia 53(4):1179-1190, 2005 - https://doi.org/10.1016/j.actamat.2004.11.021 + https://en.wikipedia.org/wiki/Bravais_lattice """ - models={'KS':self.KS, 'GT':self.GT, 'GT_prime':self.GTprime, - 'NW':self.NW, 'Pitsch': self.Pitsch, 'Bain':self.Bain} - try: - relationship = models[model] - except KeyError : - raise KeyError('Orientation relationship "{}" is unknown'.format(model)) - if self.lattice not in relationship['mapping']: - raise ValueError('Relationship "{}" not supported for lattice "{}"'.format(model,self.lattice)) + lattices = { + 'triclinic':{'symmetry':None}, + 'bct':{'symmetry':'tetragonal'}, + 'hex':{'symmetry':'hexagonal'}, + 'fcc':{'symmetry':'cubic','c/a':1.0}, + 'bcc':{'symmetry':'cubic','c/a':1.0}, + } - r = {'lattice':Lattice((set(relationship['mapping'])-{self.lattice}).pop()), # target lattice - 'rotations':[] } - myPlane_id = relationship['mapping'][self.lattice] - otherPlane_id = (myPlane_id+1)%2 - myDir_id = myPlane_id +2 - otherDir_id = otherPlane_id +2 + def __init__(self, lattice): + """ + New lattice of given type. - for miller in np.hstack((relationship['planes'],relationship['directions'])): - myPlane = miller[myPlane_id]/ np.linalg.norm(miller[myPlane_id]) - myDir = miller[myDir_id]/ np.linalg.norm(miller[myDir_id]) - myMatrix = np.array([myDir,np.cross(myPlane,myDir),myPlane]) + Parameters + ---------- + lattice : str + Bravais lattice. - otherPlane = miller[otherPlane_id]/ np.linalg.norm(miller[otherPlane_id]) - otherDir = miller[otherDir_id]/ np.linalg.norm(miller[otherDir_id]) - otherMatrix = np.array([otherDir,np.cross(otherPlane,otherDir),otherPlane]) + """ + self.lattice = lattice + self.symmetry = Symmetry(self.lattices[lattice]['symmetry']) - r['rotations'].append(Rotation.fromMatrix(np.dot(otherMatrix.T,myMatrix))) - return r + def __repr__(self): + """Report basic lattice information.""" + return 'Bravais lattice {} ({} symmetry)'.format(self.lattice,self.symmetry) + + + # Kurdjomov--Sachs orientation relationship for fcc <-> bcc transformation + # from S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 + # also see K. Kitahara et al., Acta Materialia 54:1279-1288, 2006 + KS = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]], + [[ 1, 1, -1],[ 0, 1, 1]]],dtype='float'), + 'directions': np.array([ + [[ -1, 0, 1],[ -1, -1, 1]], + [[ -1, 0, 1],[ -1, 1, -1]], + [[ 0, 1, -1],[ -1, -1, 1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ 1, -1, 0],[ -1, -1, 1]], + [[ 1, -1, 0],[ -1, 1, -1]], + [[ 1, 0, -1],[ -1, -1, 1]], + [[ 1, 0, -1],[ -1, 1, -1]], + [[ -1, -1, 0],[ -1, -1, 1]], + [[ -1, -1, 0],[ -1, 1, -1]], + [[ 0, 1, 1],[ -1, -1, 1]], + [[ 0, 1, 1],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ 0, -1, 1],[ -1, 1, -1]], + [[ -1, 0, -1],[ -1, -1, 1]], + [[ -1, 0, -1],[ -1, 1, -1]], + [[ 1, 1, 0],[ -1, -1, 1]], + [[ 1, 1, 0],[ -1, 1, -1]], + [[ -1, 1, 0],[ -1, -1, 1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, -1],[ -1, -1, 1]], + [[ 0, -1, -1],[ -1, 1, -1]], + [[ 1, 0, 1],[ -1, -1, 1]], + [[ 1, 0, 1],[ -1, 1, -1]]],dtype='float')} + + # Greninger--Troiano orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + GT = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 1, 0, 1]], + [[ 1, 1, 1],[ 1, 1, 0]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ -1, 0, 1]], + [[ -1, -1, 1],[ -1, -1, 0]], + [[ -1, -1, 1],[ 0, -1, 1]], + [[ -1, 1, 1],[ -1, 0, 1]], + [[ -1, 1, 1],[ -1, 1, 0]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 1, 0, 1]], + [[ 1, -1, 1],[ 1, -1, 0]], + [[ 1, -1, 1],[ 0, -1, 1]], + [[ 1, 1, 1],[ 1, 1, 0]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 1, 0, 1]], + [[ -1, -1, 1],[ -1, -1, 0]], + [[ -1, -1, 1],[ 0, -1, 1]], + [[ -1, -1, 1],[ -1, 0, 1]], + [[ -1, 1, 1],[ -1, 1, 0]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ -1, 0, 1]], + [[ 1, -1, 1],[ 1, -1, 0]], + [[ 1, -1, 1],[ 0, -1, 1]], + [[ 1, -1, 1],[ 1, 0, 1]]],dtype='float'), + 'directions': np.array([ + [[ -5,-12, 17],[-17, -7, 17]], + [[ 17, -5,-12],[ 17,-17, -7]], + [[-12, 17, -5],[ -7, 17,-17]], + [[ 5, 12, 17],[ 17, 7, 17]], + [[-17, 5,-12],[-17, 17, -7]], + [[ 12,-17, -5],[ 7,-17,-17]], + [[ -5, 12,-17],[-17, 7,-17]], + [[ 17, 5, 12],[ 17, 17, 7]], + [[-12,-17, 5],[ -7,-17, 17]], + [[ 5,-12,-17],[ 17, -7,-17]], + [[-17, -5, 12],[-17,-17, 7]], + [[ 12, 17, 5],[ 7, 17, 17]], + [[ -5, 17,-12],[-17, 17, -7]], + [[-12, -5, 17],[ -7,-17, 17]], + [[ 17,-12, -5],[ 17, -7,-17]], + [[ 5,-17,-12],[ 17,-17, -7]], + [[ 12, 5, 17],[ 7, 17, 17]], + [[-17, 12, -5],[-17, 7,-17]], + [[ -5,-17, 12],[-17,-17, 7]], + [[-12, 5,-17],[ -7, 17,-17]], + [[ 17, 12, 5],[ 17, 7, 17]], + [[ 5, 17, 12],[ 17, 17, 7]], + [[ 12, -5,-17],[ 7,-17,-17]], + [[-17,-12, 5],[-17,-7, 17]]],dtype='float')} + + # Greninger--Troiano' orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + GTprime = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 7, 17, 17],[ 12, 5, 17]], + [[ 17, 7, 17],[ 17, 12, 5]], + [[ 17, 17, 7],[ 5, 17, 12]], + [[ -7,-17, 17],[-12, -5, 17]], + [[-17, -7, 17],[-17,-12, 5]], + [[-17,-17, 7],[ -5,-17, 12]], + [[ 7,-17,-17],[ 12, -5,-17]], + [[ 17, -7,-17],[ 17,-12, -5]], + [[ 17,-17, -7],[ 5,-17,-12]], + [[ -7, 17,-17],[-12, 5,-17]], + [[-17, 7,-17],[-17, 12, -5]], + [[-17, 17, -7],[ -5, 17,-12]], + [[ 7, 17, 17],[ 12, 17, 5]], + [[ 17, 7, 17],[ 5, 12, 17]], + [[ 17, 17, 7],[ 17, 5, 12]], + [[ -7,-17, 17],[-12,-17, 5]], + [[-17, -7, 17],[ -5,-12, 17]], + [[-17,-17, 7],[-17, -5, 12]], + [[ 7,-17,-17],[ 12,-17, -5]], + [[ 17, -7,-17],[ 5, -12,-17]], + [[ 17,-17, -7],[ 17, -5,-12]], + [[ -7, 17,-17],[-12, 17, -5]], + [[-17, 7,-17],[ -5, 12,-17]], + [[-17, 17, -7],[-17, 5,-12]]],dtype='float'), + 'directions': np.array([ + [[ 0, 1, -1],[ 1, 1, -1]], + [[ -1, 0, 1],[ -1, 1, 1]], + [[ 1, -1, 0],[ 1, -1, 1]], + [[ 0, -1, -1],[ -1, -1, -1]], + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, -1, 0],[ 1, -1, -1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ 1, 0, 1],[ 1, 1, 1]], + [[ -1, -1, 0],[ -1, -1, 1]], + [[ 0, -1, -1],[ 1, -1, -1]], + [[ -1, 0, 1],[ -1, -1, 1]], + [[ -1, -1, 0],[ -1, -1, -1]], + [[ 0, -1, 1],[ 1, -1, 1]], + [[ 1, 0, -1],[ 1, 1, -1]], + [[ -1, 1, 0],[ -1, 1, 1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ -1, 0, -1],[ -1, -1, -1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ -1, 0, -1],[ -1, 1, -1]], + [[ 1, 1, 0],[ 1, 1, 1]], + [[ 0, 1, 1],[ 1, 1, 1]], + [[ 1, 0, -1],[ 1, -1, -1]], + [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} + + # Nishiyama--Wassermann orientation relationship for fcc <-> bcc transformation + # from H. Kitahara et al., Materials Characterization 54:378-386, 2005 + NW = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ 1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ -1, 1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ 1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]], + [[ -1, -1, 1],[ 0, 1, 1]]],dtype='float'), + 'directions': np.array([ + [[ 2, -1, -1],[ 0, -1, 1]], + [[ -1, 2, -1],[ 0, -1, 1]], + [[ -1, -1, 2],[ 0, -1, 1]], + [[ -2, -1, -1],[ 0, -1, 1]], + [[ 1, 2, -1],[ 0, -1, 1]], + [[ 1, -1, 2],[ 0, -1, 1]], + [[ 2, 1, -1],[ 0, -1, 1]], + [[ -1, -2, -1],[ 0, -1, 1]], + [[ -1, 1, 2],[ 0, -1, 1]], + [[ 2, -1, 1],[ 0, -1, 1]], #It is wrong in the paper, but matrix is correct + [[ -1, 2, 1],[ 0, -1, 1]], + [[ -1, -1, -2],[ 0, -1, 1]]],dtype='float')} + + # Pitsch orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Acta Materialia 53:1179-1190, 2005 + Pitsch = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 0, 1, 0],[ -1, 0, 1]], + [[ 0, 0, 1],[ 1, -1, 0]], + [[ 1, 0, 0],[ 0, 1, -1]], + [[ 1, 0, 0],[ 0, -1, -1]], + [[ 0, 1, 0],[ -1, 0, -1]], + [[ 0, 0, 1],[ -1, -1, 0]], + [[ 0, 1, 0],[ -1, 0, -1]], + [[ 0, 0, 1],[ -1, -1, 0]], + [[ 1, 0, 0],[ 0, -1, -1]], + [[ 1, 0, 0],[ 0, -1, 1]], + [[ 0, 1, 0],[ 1, 0, -1]], + [[ 0, 0, 1],[ -1, 1, 0]]],dtype='float'), + 'directions': np.array([ + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, 1, 0],[ 1, 1, -1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ 0, 1, -1],[ -1, 1, -1]], + [[ -1, 0, 1],[ -1, -1, 1]], + [[ 1, -1, 0],[ 1, -1, -1]], + [[ 1, 0, -1],[ 1, -1, -1]], + [[ -1, 1, 0],[ -1, 1, -1]], + [[ 0, -1, 1],[ -1, -1, 1]], + [[ 0, 1, 1],[ -1, 1, 1]], + [[ 1, 0, 1],[ 1, -1, 1]], + [[ 1, 1, 0],[ 1, 1, -1]]],dtype='float')} + + # Bain orientation relationship for fcc <-> bcc transformation + # from Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + Bain = {'mapping':{'fcc':0,'bcc':1}, + 'planes': np.array([ + [[ 1, 0, 0],[ 1, 0, 0]], + [[ 0, 1, 0],[ 0, 1, 0]], + [[ 0, 0, 1],[ 0, 0, 1]]],dtype='float'), + 'directions': np.array([ + [[ 0, 1, 0],[ 0, 1, 1]], + [[ 0, 0, 1],[ 1, 0, 1]], + [[ 1, 0, 0],[ 1, 1, 0]]],dtype='float')} + + def relationOperations(self,model): + """ + Crystallographic orientation relationships for phase transformations. + + References + ---------- + S. Morito et al., Journal of Alloys and Compounds 577:s587-s592, 2013 + https://doi.org/10.1016/j.jallcom.2012.02.004 + + K. Kitahara et al., Acta Materialia 54(5):1279-1288, 2006 + https://doi.org/10.1016/j.actamat.2005.11.001 + + Y. He et al., Journal of Applied Crystallography 39:72-81, 2006 + https://doi.org/10.1107/S0021889805038276 + + H. Kitahara et al., Materials Characterization 54(4-5):378-386, 2005 + https://doi.org/10.1016/j.matchar.2004.12.015 + + Y. He et al., Acta Materialia 53(4):1179-1190, 2005 + https://doi.org/10.1016/j.actamat.2004.11.021 + + """ + models={'KS':self.KS, 'GT':self.GT, 'GT_prime':self.GTprime, + 'NW':self.NW, 'Pitsch': self.Pitsch, 'Bain':self.Bain} + try: + relationship = models[model] + except KeyError : + raise KeyError('Orientation relationship "{}" is unknown'.format(model)) + + if self.lattice not in relationship['mapping']: + raise ValueError('Relationship "{}" not supported for lattice "{}"'.format(model,self.lattice)) + + r = {'lattice':Lattice((set(relationship['mapping'])-{self.lattice}).pop()), # target lattice + 'rotations':[] } + + myPlane_id = relationship['mapping'][self.lattice] + otherPlane_id = (myPlane_id+1)%2 + myDir_id = myPlane_id +2 + otherDir_id = otherPlane_id +2 + + for miller in np.hstack((relationship['planes'],relationship['directions'])): + myPlane = miller[myPlane_id]/ np.linalg.norm(miller[myPlane_id]) + myDir = miller[myDir_id]/ np.linalg.norm(miller[myDir_id]) + myMatrix = np.array([myDir,np.cross(myPlane,myDir),myPlane]) + + otherPlane = miller[otherPlane_id]/ np.linalg.norm(miller[otherPlane_id]) + otherDir = miller[otherDir_id]/ np.linalg.norm(miller[otherDir_id]) + otherMatrix = np.array([otherDir,np.cross(otherPlane,otherDir),otherPlane]) + + r['rotations'].append(Rotation.fromMatrix(np.dot(otherMatrix.T,myMatrix))) + + return r diff --git a/python/damask/orientation.py b/python/damask/orientation.py index 55a58959c..4916c1679 100644 --- a/python/damask/orientation.py +++ b/python/damask/orientation.py @@ -13,137 +13,137 @@ class Orientation: __slots__ = ['rotation','lattice'] def __repr__(self): - """Report lattice type and orientation.""" - return self.lattice.__repr__()+'\n'+self.rotation.__repr__() + """Report lattice type and orientation.""" + return self.lattice.__repr__()+'\n'+self.rotation.__repr__() def __init__(self, rotation, lattice): - """ - New orientation from rotation and lattice. + """ + New orientation from rotation and lattice. - Parameters - ---------- - rotation : Rotation - Rotation specifying the lattice orientation. - lattice : Lattice - Lattice type of the crystal. + Parameters + ---------- + rotation : Rotation + Rotation specifying the lattice orientation. + lattice : Lattice + Lattice type of the crystal. - """ - if isinstance(lattice, Lattice): - self.lattice = lattice - else: - self.lattice = Lattice(lattice) # assume string + """ + if isinstance(lattice, Lattice): + self.lattice = lattice + else: + self.lattice = Lattice(lattice) # assume string - if isinstance(rotation, Rotation): - self.rotation = rotation - else: - self.rotation = Rotation.fromQuaternion(rotation) # assume quaternion + if isinstance(rotation, Rotation): + self.rotation = rotation + else: + self.rotation = Rotation.fromQuaternion(rotation) # assume quaternion def disorientation(self, other, SST = True, symmetries = False): - """ - Disorientation between myself and given other orientation. + """ + Disorientation between myself and given other orientation. - Rotation axis falls into SST if SST == True. - (Currently requires same symmetry for both orientations. - Look into A. Heinz and P. Neumann 1991 for cases with differing sym.) - """ - if self.lattice.symmetry != other.lattice.symmetry: - raise NotImplementedError('disorientation between different symmetry classes not supported yet.') + Rotation axis falls into SST if SST == True. + (Currently requires same symmetry for both orientations. + Look into A. Heinz and P. Neumann 1991 for cases with differing sym.) + """ + if self.lattice.symmetry != other.lattice.symmetry: + raise NotImplementedError('disorientation between different symmetry classes not supported yet.') - mySymEqs = self.equivalentOrientations() if SST else self.equivalentOrientations([0]) # take all or only first sym operation - otherSymEqs = other.equivalentOrientations() + mySymEqs = self.equivalentOrientations() if SST else self.equivalentOrientations([0]) # take all or only first sym operation + otherSymEqs = other.equivalentOrientations() - for i,sA in enumerate(mySymEqs): - aInv = sA.rotation.inversed() - for j,sB in enumerate(otherSymEqs): - b = sB.rotation - r = b*aInv - for k in range(2): - r.inverse() - breaker = self.lattice.symmetry.inFZ(r.asRodrigues(vector=True)) \ - and (not SST or other.lattice.symmetry.inDisorientationSST(r.asRodrigues(vector=True))) + for i,sA in enumerate(mySymEqs): + aInv = sA.rotation.inversed() + for j,sB in enumerate(otherSymEqs): + b = sB.rotation + r = b*aInv + for k in range(2): + r.inverse() + breaker = self.lattice.symmetry.inFZ(r.asRodrigues(vector=True)) \ + and (not SST or other.lattice.symmetry.inDisorientationSST(r.asRodrigues(vector=True))) + if breaker: break if breaker: break if breaker: break - if breaker: break - return (Orientation(r,self.lattice), i,j, k == 1) if symmetries else r # disorientation ... + return (Orientation(r,self.lattice), i,j, k == 1) if symmetries else r # disorientation ... # ... own sym, other sym, # self-->other: True, self<--other: False def inFZ(self): - return self.lattice.symmetry.inFZ(self.rotation.asRodrigues(vector=True)) + return self.lattice.symmetry.inFZ(self.rotation.asRodrigues(vector=True)) def equivalentOrientations(self,members=[]): - """List of orientations which are symmetrically equivalent.""" - try: - iter(members) # asking for (even empty) list of members? - except TypeError: - return self.__class__(self.lattice.symmetry.symmetryOperations(members)*self.rotation,self.lattice) # no, return rotation object - else: - return [self.__class__(q*self.rotation,self.lattice) \ - for q in self.lattice.symmetry.symmetryOperations(members)] # yes, return list of rotations + """List of orientations which are symmetrically equivalent.""" + try: + iter(members) # asking for (even empty) list of members? + except TypeError: + return self.__class__(self.lattice.symmetry.symmetryOperations(members)*self.rotation,self.lattice) # no, return rotation object + else: + return [self.__class__(q*self.rotation,self.lattice) \ + for q in self.lattice.symmetry.symmetryOperations(members)] # yes, return list of rotations def relatedOrientations(self,model): - """List of orientations related by the given orientation relationship.""" - r = self.lattice.relationOperations(model) - return [self.__class__(o*self.rotation,r['lattice']) for o in r['rotations']] + """List of orientations related by the given orientation relationship.""" + r = self.lattice.relationOperations(model) + return [self.__class__(o*self.rotation,r['lattice']) for o in r['rotations']] def reduced(self): - """Transform orientation to fall into fundamental zone according to symmetry.""" - for me in self.equivalentOrientations(): - if self.lattice.symmetry.inFZ(me.rotation.asRodrigues(vector=True)): break + """Transform orientation to fall into fundamental zone according to symmetry.""" + for me in self.equivalentOrientations(): + if self.lattice.symmetry.inFZ(me.rotation.asRodrigues(vector=True)): break - return self.__class__(me.rotation,self.lattice) + return self.__class__(me.rotation,self.lattice) def inversePole(self, axis, proper = False, SST = True): - """Axis rotated according to orientation (using crystal symmetry to ensure location falls into SST).""" - if SST: # pole requested to be within SST - for i,o in enumerate(self.equivalentOrientations()): # test all symmetric equivalent quaternions - pole = o.rotation*axis # align crystal direction to axis - if self.lattice.symmetry.inSST(pole,proper): break # found SST version - else: - pole = self.rotation*axis # align crystal direction to axis + """Axis rotated according to orientation (using crystal symmetry to ensure location falls into SST).""" + if SST: # pole requested to be within SST + for i,o in enumerate(self.equivalentOrientations()): # test all symmetric equivalent quaternions + pole = o.rotation*axis # align crystal direction to axis + if self.lattice.symmetry.inSST(pole,proper): break # found SST version + else: + pole = self.rotation*axis # align crystal direction to axis - return (pole,i if SST else 0) + return (pole,i if SST else 0) def IPFcolor(self,axis): - """TSL color of inverse pole figure for given axis.""" - color = np.zeros(3,'d') + """TSL color of inverse pole figure for given axis.""" + color = np.zeros(3,'d') - for o in self.equivalentOrientations(): - pole = o.rotation*axis # align crystal direction to axis - inSST,color = self.lattice.symmetry.inSST(pole,color=True) - if inSST: break + for o in self.equivalentOrientations(): + pole = o.rotation*axis # align crystal direction to axis + inSST,color = self.lattice.symmetry.inSST(pole,color=True) + if inSST: break - return color + return color @staticmethod def fromAverage(orientations, weights = []): - """Create orientation from average of list of orientations.""" - if not all(isinstance(item, Orientation) for item in orientations): - raise TypeError("Only instances of Orientation can be averaged.") + """Create orientation from average of list of orientations.""" + if not all(isinstance(item, Orientation) for item in orientations): + raise TypeError("Only instances of Orientation can be averaged.") - closest = [] - ref = orientations[0] - for o in orientations: - closest.append(o.equivalentOrientations( - ref.disorientation(o, - SST = False, # select (o[ther]'s) sym orientation - symmetries = True)[2]).rotation) # with lowest misorientation + closest = [] + ref = orientations[0] + for o in orientations: + closest.append(o.equivalentOrientations( + ref.disorientation(o, + SST = False, # select (o[ther]'s) sym orientation + symmetries = True)[2]).rotation) # with lowest misorientation - return Orientation(Rotation.fromAverage(closest,weights),ref.lattice) + return Orientation(Rotation.fromAverage(closest,weights),ref.lattice) def average(self,other): - """Calculate the average rotation.""" - return Orientation.fromAverage([self,other]) + """Calculate the average rotation.""" + return Orientation.fromAverage([self,other]) diff --git a/python/damask/rotation.py b/python/damask/rotation.py index 084e4bf38..78686ab1f 100644 --- a/python/damask/rotation.py +++ b/python/damask/rotation.py @@ -4,14 +4,10 @@ from . import Lambert P = -1 -def isone(a): - return np.isclose(a,1.0,atol=1.0e-7,rtol=0.0) - def iszero(a): - return np.isclose(a,0.0,atol=1.0e-12,rtol=0.0) + return np.isclose(a,0.0,atol=1.0e-12,rtol=0.0) -#################################################################################################### class Rotation: u""" Orientation stored with functionality for conversion to different representations. From f256493e79a67f3a55fd77fd51064739810197ca Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 22 Feb 2020 00:54:15 +0100 Subject: [PATCH 13/13] fixed indentation --- python/damask/Lambert.py | 148 +++++++++++++++++++-------------------- python/damask/geom.py | 6 +- python/damask/lattice.py | 6 +- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/python/damask/Lambert.py b/python/damask/Lambert.py index e6ae996ea..0ff177f9b 100644 --- a/python/damask/Lambert.py +++ b/python/damask/Lambert.py @@ -6,27 +6,27 @@ # Copyright (c) 2013-2014, Marc De Graef/Carnegie Mellon University # All rights reserved. # -# Redistribution and use in source and binary forms, with or without modification, are +# Redistribution and use in source and binary forms, with or without modification, are # permitted provided that the following conditions are met: # -# - Redistributions of source code must retain the above copyright notice, this list +# - Redistributions of source code must retain the above copyright notice, this list # of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or +# - Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or # other materials provided with the distribution. -# - Neither the names of Marc De Graef, Carnegie Mellon University nor the names -# of its contributors may be used to endorse or promote products derived from +# - Neither the names of Marc De Graef, Carnegie Mellon University nor the names +# of its contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #################################################################################################### @@ -44,7 +44,7 @@ def CubeToBall(cube): ---------- cube : numpy.ndarray coordinates of a point in a uniform refinable cubical grid. - + References ---------- D. Roşca et al., Modelling and Simulation in Materials Science and Engineering 22:075013, 2014 @@ -52,37 +52,37 @@ def CubeToBall(cube): """ if np.abs(np.max(cube))>np.pi**(2./3.) * 0.5: - raise ValueError - + raise ValueError + # transform to the sphere grid via the curved square, and intercept the zero point if np.allclose(cube,0.0,rtol=0.0,atol=1.0e-300): - ball = np.zeros(3) + ball = np.zeros(3) else: - # get pyramide and scale by grid parameter ratio - p = get_order(cube) - XYZ = cube[p] * sc + # get pyramide and scale by grid parameter ratio + p = get_order(cube) + XYZ = cube[p] * sc - # intercept all the points along the z-axis - if np.allclose(XYZ[0:2],0.0,rtol=0.0,atol=1.0e-300): - ball = np.array([0.0, 0.0, np.sqrt(6.0/np.pi) * XYZ[2]]) - else: - order = [1,0] if np.abs(XYZ[1]) <= np.abs(XYZ[0]) else [0,1] - q = np.pi/12.0 * XYZ[order[0]]/XYZ[order[1]] - c = np.cos(q) - s = np.sin(q) - q = R1*2.0**0.25/beta * XYZ[order[1]] / np.sqrt(np.sqrt(2.0)-c) - T = np.array([ (np.sqrt(2.0)*c - 1.0), np.sqrt(2.0) * s]) * q + # intercept all the points along the z-axis + if np.allclose(XYZ[0:2],0.0,rtol=0.0,atol=1.0e-300): + ball = np.array([0.0, 0.0, np.sqrt(6.0/np.pi) * XYZ[2]]) + else: + order = [1,0] if np.abs(XYZ[1]) <= np.abs(XYZ[0]) else [0,1] + q = np.pi/12.0 * XYZ[order[0]]/XYZ[order[1]] + c = np.cos(q) + s = np.sin(q) + q = R1*2.0**0.25/beta * XYZ[order[1]] / np.sqrt(np.sqrt(2.0)-c) + T = np.array([ (np.sqrt(2.0)*c - 1.0), np.sqrt(2.0) * s]) * q - # transform to sphere grid (inverse Lambert) - # note that there is no need to worry about dividing by zero, since XYZ[2] can not become zero - c = np.sum(T**2) - s = c * np.pi/24.0 /XYZ[2]**2 - c = c * np.sqrt(np.pi/24.0)/XYZ[2] - q = np.sqrt( 1.0 - s ) - ball = np.array([ T[order[1]] * q, T[order[0]] * q, np.sqrt(6.0/np.pi) * XYZ[2] - c ]) - - # reverse the coordinates back to the regular order according to the original pyramid number - ball = ball[p] + # transform to sphere grid (inverse Lambert) + # note that there is no need to worry about dividing by zero, since XYZ[2] can not become zero + c = np.sum(T**2) + s = c * np.pi/24.0 /XYZ[2]**2 + c = c * np.sqrt(np.pi/24.0)/XYZ[2] + q = np.sqrt( 1.0 - s ) + ball = np.array([ T[order[1]] * q, T[order[0]] * q, np.sqrt(6.0/np.pi) * XYZ[2] - c ]) + + # reverse the coordinates back to the regular order according to the original pyramid number + ball = ball[p] return ball @@ -103,46 +103,46 @@ def BallToCube(ball): """ rs = np.linalg.norm(ball) - if rs > R1: - raise ValueError - + if rs > R1: + raise ValueError + if np.allclose(ball,0.0,rtol=0.0,atol=1.0e-300): - cube = np.zeros(3) + cube = np.zeros(3) else: - p = get_order(ball) - xyz3 = ball[p] + p = get_order(ball) + xyz3 = ball[p] - # inverse M_3 - xyz2 = xyz3[0:2] * np.sqrt( 2.0*rs/(rs+np.abs(xyz3[2])) ) - - # inverse M_2 - qxy = np.sum(xyz2**2) - - if np.isclose(qxy,0.0,rtol=0.0,atol=1.0e-300): - Tinv = np.zeros(2) - else: - q2 = qxy + np.max(np.abs(xyz2))**2 - sq2 = np.sqrt(q2) - q = (beta/np.sqrt(2.0)/R1) * np.sqrt(q2*qxy/(q2-np.max(np.abs(xyz2))*sq2)) - tt = np.clip((np.min(np.abs(xyz2))**2+np.max(np.abs(xyz2))*sq2)/np.sqrt(2.0)/qxy,-1.0,1.0) - Tinv = np.array([1.0,np.arccos(tt)/np.pi*12.0]) if np.abs(xyz2[1]) <= np.abs(xyz2[0]) else \ - np.array([np.arccos(tt)/np.pi*12.0,1.0]) - Tinv = q * np.where(xyz2<0.0,-Tinv,Tinv) - - # inverse M_1 - cube = np.array([ Tinv[0], Tinv[1], (-1.0 if xyz3[2] < 0.0 else 1.0) * rs / np.sqrt(6.0/np.pi) ]) /sc + # inverse M_3 + xyz2 = xyz3[0:2] * np.sqrt( 2.0*rs/(rs+np.abs(xyz3[2])) ) + + # inverse M_2 + qxy = np.sum(xyz2**2) + + if np.isclose(qxy,0.0,rtol=0.0,atol=1.0e-300): + Tinv = np.zeros(2) + else: + q2 = qxy + np.max(np.abs(xyz2))**2 + sq2 = np.sqrt(q2) + q = (beta/np.sqrt(2.0)/R1) * np.sqrt(q2*qxy/(q2-np.max(np.abs(xyz2))*sq2)) + tt = np.clip((np.min(np.abs(xyz2))**2+np.max(np.abs(xyz2))*sq2)/np.sqrt(2.0)/qxy,-1.0,1.0) + Tinv = np.array([1.0,np.arccos(tt)/np.pi*12.0]) if np.abs(xyz2[1]) <= np.abs(xyz2[0]) else \ + np.array([np.arccos(tt)/np.pi*12.0,1.0]) + Tinv = q * np.where(xyz2<0.0,-Tinv,Tinv) + + # inverse M_1 + cube = np.array([ Tinv[0], Tinv[1], (-1.0 if xyz3[2] < 0.0 else 1.0) * rs / np.sqrt(6.0/np.pi) ]) /sc + # reverse the coordinates back to the regular order according to the original pyramid number + cube = cube[p] - # reverse the coordinates back to the regular order according to the original pyramid number - cube = cube[p] - return cube + def get_order(xyz): """ Get order of the coordinates. Depending on the pyramid in which the point is located, the order need to be adjusted. - + Parameters ---------- xyz : numpy.ndarray @@ -157,10 +157,10 @@ def get_order(xyz): """ if (abs(xyz[0])<= xyz[2]) and (abs(xyz[1])<= xyz[2]) or \ (abs(xyz[0])<=-xyz[2]) and (abs(xyz[1])<=-xyz[2]): - return [0,1,2] + return [0,1,2] elif (abs(xyz[2])<= xyz[0]) and (abs(xyz[1])<= xyz[0]) or \ (abs(xyz[2])<=-xyz[0]) and (abs(xyz[1])<=-xyz[0]): - return [1,2,0] + return [1,2,0] elif (abs(xyz[0])<= xyz[1]) and (abs(xyz[2])<= xyz[1]) or \ (abs(xyz[0])<=-xyz[1]) and (abs(xyz[2])<=-xyz[1]): - return [2,0,1] + return [2,0,1] diff --git a/python/damask/geom.py b/python/damask/geom.py index 640618cd4..9a1677191 100644 --- a/python/damask/geom.py +++ b/python/damask/geom.py @@ -80,7 +80,7 @@ class Geom(): if size is not None: self.set_size(size) elif rescale: - self.set_size(self.get_grid()/grid_old*self.size) + self.set_size(self.get_grid()/grid_old*self.size) message = ['grid a b c: {}'.format(' x '.join(map(str,grid_old)))] if np.any(grid_old != self.get_grid()): @@ -269,7 +269,7 @@ class Geom(): comments = [] for i,line in enumerate(content[:header_length]): items = line.lower().strip().split() - key = items[0] if len(items) > 0 else '' + key = items[0] if items else '' if key == 'grid': grid = np.array([ int(dict(zip(items[1::2],items[2::2]))[i]) for i in ['a','b','c']]) elif key == 'size': @@ -524,7 +524,7 @@ class Geom(): """Renumber sorted microstructure indices to 1,...,N.""" renumbered = np.empty(self.get_grid(),dtype=self.microstructure.dtype) for i, oldID in enumerate(np.unique(self.microstructure)): - renumbered = np.where(self.microstructure == oldID, i+1, renumbered) + renumbered = np.where(self.microstructure == oldID, i+1, renumbered) return self.update(renumbered) #self.add_comments('tbd') diff --git a/python/damask/lattice.py b/python/damask/lattice.py index ee4cbff3c..528ee7e09 100644 --- a/python/damask/lattice.py +++ b/python/damask/lattice.py @@ -204,7 +204,7 @@ class Symmetry: """ if (len(rodrigues) != 3): - raise ValueError('Input is not a Rodriques-Frank vector.\n') + raise ValueError('Input is not a Rodriques-Frank vector.\n') R = rodrigues epsilon = 0.0 @@ -288,8 +288,8 @@ class Symmetry: theComponents = np.around(np.dot(basis['improper'],v),12) inSST = np.all(theComponents >= 0.0) if not inSST: # ... and proper SST - theComponents = np.around(np.dot(basis['proper'],v),12) - inSST = np.all(theComponents >= 0.0) + theComponents = np.around(np.dot(basis['proper'],v),12) + inSST = np.all(theComponents >= 0.0) else: v[2] = abs(v[2]) # z component projects identical theComponents = np.around(np.dot(basis['improper'],v),12) # for positive and negative values