import numpy as np

from . import tensor
from . import util
from . import grid_filters

_P = -1

# parameters for conversion from/to cubochoric
_sc   = np.pi**(1./6.)/6.**(1./6.)
_beta = np.pi**(5./6.)/6.**(1./6.)/2.
_R1   = (3.*np.pi/4.)**(1./3.)

[docs]class Rotation: u""" Rotation with functionality for conversion between different representations. The following conventions apply: - Coordinate frames are right-handed. - 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. - Rotations will be interpreted in the passive sense. - Euler angle triplets are implemented using the Bunge convention, with angular ranges of [0,2π], [0,π], [0,2π]. - The rotation angle ω is limited to the interval [0,π]. - The real part of a quaternion is positive, Re(q) ≥ 0 - P = -1 (as default). Examples -------- Rotate vector 'a' (defined in coordinate system 'A') to coordinates 'b' expressed in system 'B': >>> import damask >>> import numpy as np >>> Q = damask.Rotation.from_random() >>> a = np.random.rand(3) >>> b = Q @ a >>> np.allclose(np.dot(Q.as_matrix(),a),b) True Compound rotations R1 (first) and R2 (second): >>> import damask >>> import numpy as np >>> R1 = damask.Rotation.from_random() >>> R2 = damask.Rotation.from_random() >>> R = R2 * R1 >>> np.allclose(R.as_matrix(), np.dot(R2.as_matrix(),R1.as_matrix())) True 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 """ __slots__ = ['quaternion'] def __init__(self,rotation = np.array([1.0,0.0,0.0,0.0])): """ New rotation. Parameters ---------- rotation : list, numpy.ndarray, Rotation, optional Unit quaternion in positive real hemisphere. Use .from_quaternion to perform a sanity check. Defaults to no rotation. """ if isinstance(rotation,Rotation): self.quaternion = rotation.quaternion.copy() elif np.array(rotation).shape[-1] == 4: self.quaternion = np.array(rotation) else: raise TypeError('"rotation" is neither a Rotation nor a quaternion') def __repr__(self): """Represent rotation as unit quaternion(s).""" return f'Quaternion{" " if self.quaternion.shape == (4,) else "s of shape "+str(self.quaternion.shape[:-1])+chr(10)}'\ + str(self.quaternion) def __copy__(self,**kwargs): """Create deep copy.""" return self.__class__(rotation=kwargs['rotation'] if 'rotation' in kwargs else self.quaternion) copy = __copy__ def __getitem__(self,item): """Return slice according to item.""" return self.copy() \ if self.shape == () else \ self.copy(rotation=self.quaternion[item+(slice(None),)] if isinstance(item,tuple) else self.quaternion[item]) def __eq__(self,other): """ Equal to other. Parameters ---------- other : Rotation Rotation to check for equality. """ return np.logical_or(np.all(self.quaternion == other.quaternion,axis=-1), np.all(self.quaternion == -1.0*other.quaternion,axis=-1)) def __ne__(self,other): """ Not equal to other. Parameters ---------- other : Rotation Rotation to check for inequality. """ return np.logical_not(self==other)
[docs] def isclose(self,other,rtol=1e-5,atol=1e-8,equal_nan=True): """ Report where values are approximately equal to corresponding ones of other Rotation. Parameters ---------- other : Rotation Rotation to compare against. rtol : float, optional Relative tolerance of equality. atol : float, optional Absolute tolerance of equality. equal_nan : bool, optional Consider matching NaN values as equal. Defaults to True. Returns ------- mask : numpy.ndarray bool Mask indicating where corresponding rotations are close. """ s = self.quaternion o = other.quaternion return np.logical_or(np.all(np.isclose(s, o,rtol,atol,equal_nan),axis=-1), np.all(np.isclose(s,-1.0*o,rtol,atol,equal_nan),axis=-1))
[docs] def allclose(self,other,rtol=1e-5,atol=1e-8,equal_nan=True): """ Test whether all values are approximately equal to corresponding ones of other Rotation. Parameters ---------- other : Rotation Rotation to compare against. rtol : float, optional Relative tolerance of equality. atol : float, optional Absolute tolerance of equality. equal_nan : bool, optional Consider matching NaN values as equal. Defaults to True. Returns ------- answer : bool Whether all values are close between both rotations. """ return np.all(self.isclose(other,rtol,atol,equal_nan))
def __array__(self): """Initializer for numpy.""" return self.quaternion @property def size(self): return self.quaternion[...,0].size @property def shape(self): return self.quaternion[...,0].shape def __len__(self): """Length of leading/leftmost dimension of array.""" return 0 if self.shape == () else self.shape[0] def __invert__(self): """Inverse rotation (backward rotation).""" dup = self.copy() dup.quaternion[...,1:] *= -1 return dup def __pow__(self,exp): """ Perform the rotation 'exp' times. Parameters ---------- exp : float Exponent. """ phi = np.arccos(self.quaternion[...,0:1]) p = self.quaternion[...,1:]/np.linalg.norm(self.quaternion[...,1:],axis=-1,keepdims=True) return self.copy(rotation=Rotation(np.block([np.cos(exp*phi),np.sin(exp*phi)*p]))._standardize()) def __ipow__(self,exp): """ Perform the rotation 'exp' times (in-place). Parameters ---------- exp : float Exponent. """ return self**exp def __mul__(self,other): """ Compose with other. Parameters ---------- other : Rotation of shape (self.shape) Rotation for composition. Returns ------- composition : Rotation Compound rotation self*other, i.e. first other then self rotation. """ if isinstance(other,Rotation): q_m = self.quaternion[...,0:1] p_m = self.quaternion[...,1:] q_o = other.quaternion[...,0:1] p_o = other.quaternion[...,1:] q = (q_m*q_o - np.einsum('...i,...i',p_m,p_o).reshape(self.shape+(1,))) p = q_m*p_o + q_o*p_m + _P * np.cross(p_m,p_o) return Rotation(np.block([q,p]))._standardize() else: raise TypeError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') def __imul__(self,other): """ Compose with other (in-place). Parameters ---------- other : Rotation of shape (self.shape) Rotation for composition. """ return self*other def __truediv__(self,other): """ Compose with inverse of other. Parameters ---------- other : damask.Rotation of shape (self.shape) Rotation to invert for composition. Returns ------- composition : Rotation Compound rotation self*(~other), i.e. first inverse of other then self rotation. """ if isinstance(other,Rotation): return self*~other else: raise TypeError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') def __itruediv__(self,other): """ Compose with inverse of other (in-place). Parameters ---------- other : Rotation of shape (self.shape) Rotation to invert for composition. """ return self/other def __matmul__(self,other): """ Rotate vector, second order tensor, or fourth order tensor. Parameters ---------- other : numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) Vector or tensor on which to apply the rotation. Returns ------- rotated : numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) Rotated vector or tensor, i.e. transformed to frame defined by rotation. """ if isinstance(other,np.ndarray): if self.shape + (3,) == other.shape: q_m = self.quaternion[...,0] p_m = self.quaternion[...,1:] A = q_m**2.0 - np.einsum('...i,...i',p_m,p_m) B = 2.0 * np.einsum('...i,...i',p_m,other) C = 2.0 * _P * q_m return np.block([(A * other[...,i]).reshape(self.shape+(1,)) + (B * p_m[...,i]).reshape(self.shape+(1,)) + (C * ( p_m[...,(i+1)%3]*other[...,(i+2)%3]\ - p_m[...,(i+2)%3]*other[...,(i+1)%3])).reshape(self.shape+(1,)) for i in [0,1,2]]) if self.shape + (3,3) == other.shape: R = self.as_matrix() return np.einsum('...im,...jn,...mn',R,R,other) if self.shape + (3,3,3,3) == other.shape: R = self.as_matrix() return np.einsum('...im,...jn,...ko,...lp,...mnop',R,R,R,R,other) else: raise ValueError('Can only rotate vectors, 2nd order tensors, and 4th order tensors') elif isinstance(other,Rotation): raise TypeError('Use "R1*R2", i.e. multiplication, to compose rotations "R1" and "R2"') else: raise TypeError(f'Cannot rotate {type(other)}') apply = __matmul__ def _standardize(self): """Standardize quaternion (ensure positive real hemisphere).""" self.quaternion[self.quaternion[...,0] < 0.0] *= -1 return self
[docs] def append(self,other): """ Extend array along first dimension with other array(s). Parameters ---------- other : damask.Rotation """ return self.copy(rotation=np.vstack(tuple(map(lambda x:x.quaternion, [self]+other if isinstance(other,list) else [self,other]))))
[docs] def flatten(self,order = 'C'): """ Flatten array. Returns ------- flattened : damask.Rotation Rotation flattened to single dimension. """ return self.copy(rotation=self.quaternion.reshape((-1,4),order=order))
[docs] def reshape(self,shape,order = 'C'): """ Reshape array. Returns ------- reshaped : damask.Rotation Rotation of given shape. """ if isinstance(shape,(int,np.integer)): shape = (shape,) return self.copy(rotation=self.quaternion.reshape(tuple(shape)+(4,),order=order))
[docs] def broadcast_to(self,shape,mode = 'right'): """ Broadcast array. Parameters ---------- shape : tuple Shape of broadcasted array. mode : str, optional Where to preferentially locate missing dimensions. Either 'left' or 'right' (default). Returns ------- broadcasted : damask.Rotation Rotation broadcasted to given shape. """ if isinstance(shape,(int,np.integer)): shape = (shape,) return self.copy(rotation=np.broadcast_to(self.quaternion.reshape(util.shapeshifter(self.shape,shape,mode)+(4,)), shape+(4,)))
[docs] def average(self,weights = None): """ Average along last array dimension. Parameters ---------- weights : list of floats, optional Relative weight of each rotation. Returns ------- average : damask.Rotation Weighted average of original Rotation field. References ---------- F. Landis Markley et al., Journal of Guidance, Control, and Dynamics 30(4):1193-1197, 2007 https://doi.org/10.2514/1.28949 """ def _M(quat): """Intermediate representation supporting quaternion averaging.""" return np.einsum('...i,...j',quat,quat) if weights is None: weights = np.ones(self.shape,dtype=float) eig, vec = np.linalg.eig(np.sum(_M(self.quaternion) * weights[...,np.newaxis,np.newaxis],axis=-3) \ /np.sum( weights[...,np.newaxis,np.newaxis],axis=-3)) return Rotation.from_quaternion(np.real( np.squeeze( np.take_along_axis(vec, eig.argmax(axis=-1)[...,np.newaxis,np.newaxis], axis=-1), axis=-1)), accept_homomorph = True)
[docs] def misorientation(self,other): """ Calculate misorientation to other Rotation. Parameters ---------- other : damask.Rotation Rotation to which the misorientation is computed. Returns ------- g : damask.Rotation Misorientation. """ return other/self
################################################################################################ # convert to different orientation representations (numpy arrays)
[docs] def as_quaternion(self): """ Represent as unit quaternion. Returns ------- q : numpy.ndarray of shape (...,4) Unit quaternion (q_0, q_1, q_2, q_3) in positive real hemisphere, i.e. ǀqǀ = 1, q_0 ≥ 0. """ return self.quaternion.copy()
[docs] def as_Euler_angles(self, degrees = False): """ Represent as Bunge Euler angles. Parameters ---------- degrees : bool, optional Return angles in degrees. Returns ------- phi : numpy.ndarray of shape (...,3) Bunge Euler angles (φ_1 ∈ [0,2π], ϕ ∈ [0,π], φ_2 ∈ [0,2π]) or (φ_1 ∈ [0,360], ϕ ∈ [0,180], φ_2 ∈ [0,360]) if degrees == True. Notes ----- Bunge Euler angles correspond to a rotation axis sequence of z–x'–z''. Examples -------- Cube orientation as Bunge Euler angles. >>> import damask >>> import numpy as np >>> damask.Rotation(np.array([1,0,0,0])).as_Euler_angles() array([0., 0., 0.]) """ eu = Rotation._qu2eu(self.quaternion) if degrees: eu = np.degrees(eu) return eu
[docs] def as_axis_angle(self, degrees = False, pair = False): """ Represent as axis–angle pair. Parameters ---------- degrees : bool, optional Return rotation angle in degrees. Defaults to False. pair : bool, optional Return tuple of axis and angle. Defaults to False. Returns ------- axis_angle : numpy.ndarray of shape (...,4) or tuple ((...,3), (...)) if pair == True Axis and angle [n_1, n_2, n_3, ω] with ǀnǀ = 1 and ω ∈ [0,π] or ω ∈ [0,180] if degrees == True. Examples -------- Cube orientation as axis–angle pair. >>> import damask >>> import numpy as np >>> damask.Rotation(np.array([1,0,0,0])).as_axis_angle(pair=True) (array([0., 0., 1.]), array(0.)) """ ax = Rotation._qu2ax(self.quaternion) if degrees: ax[...,3] = np.degrees(ax[...,3]) return (ax[...,:3],ax[...,3]) if pair else ax
[docs] def as_matrix(self): """ Represent as rotation matrix. Returns ------- R : numpy.ndarray of shape (...,3,3) Rotation matrix R with det(R) = 1, R.T ∙ R = I. Examples -------- Cube orientation as rotation matrix. >>> import damask >>> import numpy as np >>> damask.Rotation(np.array([1,0,0,0])).as_matrix() array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) """ return Rotation._qu2om(self.quaternion)
[docs] def as_Rodrigues_vector(self, compact = False): """ Represent as Rodrigues–Frank vector with separate axis and angle argument. Parameters ---------- compact : bool, optional Return three-component Rodrigues–Frank vector, i.e. axis and angle argument are not separated. Returns ------- rho : numpy.ndarray of shape (...,4) or (...,3) if compact == True Rodrigues–Frank vector [n_1, n_2, n_3, tan(ω/2)] with ǀnǀ = 1 and ω ∈ [0,π] or [n_1, n_2, n_3] with ǀnǀ = tan(ω/2) and ω ∈ [0,π] if compact == True. Examples -------- Cube orientation as three-component Rodrigues–Frank vector. >>> import damask >>> import numpy as np >>> damask.Rotation(np.array([1,0,0,0])).as_Rodrigues_vector(compact=True) array([ 0., 0., 0.]) """ ro = Rotation._qu2ro(self.quaternion) if compact: with np.errstate(invalid='ignore'): return ro[...,:3]*ro[...,3:4] else: return ro
[docs] def as_homochoric(self): """ Represent as homochoric vector. Returns ------- h : numpy.ndarray of shape (...,3) Homochoric vector (h_1, h_2, h_3) with ǀhǀ < (3/4*π)^(1/3). Examples -------- Cube orientation as homochoric vector. >>> import damask >>> import numpy as np >>> damask.Rotation(np.array([1,0,0,0])).as_homochoric() array([0., 0., 0.]) """ return Rotation._qu2ho(self.quaternion)
[docs] def as_cubochoric(self): """ Represent as cubochoric vector. Returns ------- x : numpy.ndarray of shape (...,3) Cubochoric vector (x_1, x_2, x_3) with max(x_i) < 1/2*π^(2/3). Examples -------- Cube orientation as cubochoric vector. >>> import damask >>> import numpy as np >>> damask.Rotation(np.array([1,0,0,0])).as_cubochoric() array([0., 0., 0.]) """ return Rotation._qu2cu(self.quaternion)
################################################################################################ # Static constructors. The input data needs to follow the conventions, options allow to # relax the conventions.
[docs] @staticmethod def from_quaternion(q, accept_homomorph = False, P = -1): """ Initialize from quaternion. Parameters ---------- q : numpy.ndarray of shape (...,4) Unit quaternion (q_0, q_1, q_2, q_3) in positive real hemisphere, i.e. ǀqǀ = 1, q_0 ≥ 0. accept_homomorph : boolean, optional Allow homomorphic variants, i.e. q_0 < 0 (negative real hemisphere). Defaults to False. P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. """ qu = np.array(q,dtype=float) if qu.shape[:-2:-1] != (4,): raise ValueError('Invalid shape.') if abs(P) != 1: raise ValueError('P ∉ {-1,1}') qu[...,1:4] *= -P if accept_homomorph: qu[qu[...,0] < 0.0] *= -1 else: if np.any(qu[...,0] < 0.0): raise ValueError('Quaternion with negative first (real) component.') if not np.all(np.isclose(np.linalg.norm(qu,axis=-1), 1.0,rtol=0.0)): raise ValueError('Quaternion is not of unit length.') return Rotation(qu)
[docs] @staticmethod def from_Euler_angles(phi, degrees = False): """ Initialize from Bunge Euler angles. Parameters ---------- phi : numpy.ndarray of shape (...,3) Euler angles (φ_1 ∈ [0,2π], ϕ ∈ [0,π], φ_2 ∈ [0,2π]) or (φ_1 ∈ [0,360], ϕ ∈ [0,180], φ_2 ∈ [0,360]) if degrees == True. degrees : boolean, optional Euler angles are given in degrees. Defaults to False. Notes ----- Bunge Euler angles correspond to a rotation axis sequence of z–x'–z''. """ eu = np.array(phi,dtype=float) if eu.shape[:-2:-1] != (3,): raise ValueError('Invalid shape.') eu = np.radians(eu) if degrees else eu if np.any(eu < 0.0) or np.any(eu > 2.0*np.pi) or np.any(eu[...,1] > np.pi): # ToDo: No separate check for PHI raise ValueError('Euler angles outside of [0..2π],[0..π],[0..2π].') return Rotation(Rotation._eu2qu(eu))
[docs] @staticmethod def from_axis_angle(axis_angle, degrees = False, normalize = False, P = -1): """ Initialize from Axis angle pair. Parameters ---------- axis_angle : numpy.ndarray of shape (...,4) Axis and angle (n_1, n_2, n_3, ω) with ǀnǀ = 1 and ω ∈ [0,π] or ω ∈ [0,180] if degrees == True. degrees : boolean, optional Angle ω is given in degrees. Defaults to False. normalize: boolean, optional Allow ǀnǀ ≠ 1. Defaults to False. P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. """ ax = np.array(axis_angle,dtype=float) if ax.shape[:-2:-1] != (4,): raise ValueError('Invalid shape.') if abs(P) != 1: raise ValueError('P ∉ {-1,1}') ax[...,0:3] *= -P if degrees: ax[..., 3] = np.radians(ax[...,3]) if normalize: ax[...,0:3] /= np.linalg.norm(ax[...,0:3],axis=-1,keepdims=True) if np.any(ax[...,3] < 0.0) or np.any(ax[...,3] > np.pi): raise ValueError('Axis–angle rotation angle outside of [0..π].') if not np.all(np.isclose(np.linalg.norm(ax[...,0:3],axis=-1), 1.0)): print(np.linalg.norm(ax[...,0:3],axis=-1)) raise ValueError('Axis–angle rotation axis is not of unit length.') return Rotation(Rotation._ax2qu(ax))
[docs] @staticmethod def from_basis(basis, orthonormal = True, reciprocal = False): """ Initialize from lattice basis vectors. Parameters ---------- basis : numpy.ndarray of shape (...,3,3) Three three-dimensional lattice basis vectors. orthonormal : boolean, optional Basis is strictly orthonormal, i.e. is free of stretch components. Defaults to True. reciprocal : boolean, optional Basis vectors are given in reciprocal (instead of real) space. Defaults to False. """ om = np.array(basis,dtype=float) if om.shape[-2:] != (3,3): raise ValueError('Invalid shape.') if reciprocal: om = np.linalg.inv(tensor.transpose(om)/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.einsum('...ij,...jl',U,Vh) if not np.all(np.isclose(np.linalg.det(om),1.0)): raise ValueError('Orientation matrix has determinant ≠ 1.') if not np.all(np.isclose(np.einsum('...i,...i',om[...,0],om[...,1]), 0.0)) \ or not np.all(np.isclose(np.einsum('...i,...i',om[...,1],om[...,2]), 0.0)) \ or not np.all(np.isclose(np.einsum('...i,...i',om[...,2],om[...,0]), 0.0)): raise ValueError('Orientation matrix is not orthogonal.') return Rotation(Rotation._om2qu(om))
[docs] @staticmethod def from_matrix(R): """ Initialize from rotation matrix. Parameters ---------- R : numpy.ndarray of shape (...,3,3) Rotation matrix with det(R) = 1, R.T ∙ R = I. """ return Rotation.from_basis(R)
[docs] @staticmethod def from_parallel(a,b, **kwargs): """ Initialize from pairs of two orthogonal lattice basis vectors. Parameters ---------- a : numpy.ndarray of shape (...,2,3) Two three-dimensional lattice vectors of first orthogonal basis. b : numpy.ndarray of shape (...,2,3) Corresponding three-dimensional lattice vectors of second basis. """ a_ = np.array(a) b_ = np.array(b) if a_.shape[-2:] != (2,3) or b_.shape[-2:] != (2,3) or a_.shape != b_.shape: raise ValueError('Invalid shape.') am = np.stack([ a_[...,0,:], a_[...,1,:], np.cross(a_[...,0,:],a_[...,1,:]) ],axis=-2) bm = np.stack([ b_[...,0,:], b_[...,1,:], np.cross(b_[...,0,:],b_[...,1,:]) ],axis=-2) return Rotation.from_basis(np.swapaxes(am/np.linalg.norm(am,axis=-1,keepdims=True),-1,-2))\ .misorientation(Rotation.from_basis(np.swapaxes(bm/np.linalg.norm(bm,axis=-1,keepdims=True),-1,-2)))
[docs] @staticmethod def from_Rodrigues_vector(rho, normalize = False, P = -1): """ Initialize from Rodrigues–Frank vector (angle separated from axis). Parameters ---------- rho : numpy.ndarray of shape (...,4) Rodrigues–Frank vector (n_1, n_2, n_3, tan(ω/2)) with ǀnǀ = 1 and ω ∈ [0,π]. normalize : boolean, optional Allow ǀnǀ ≠ 1. Defaults to False. P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. """ ro = np.array(rho,dtype=float) if ro.shape[:-2:-1] != (4,): raise ValueError('Invalid shape.') if abs(P) != 1: raise ValueError('P ∉ {-1,1}') ro[...,0:3] *= -P if normalize: ro[...,0:3] /= np.linalg.norm(ro[...,0:3],axis=-1,keepdims=True) if np.any(ro[...,3] < 0.0): raise ValueError('Rodrigues vector rotation angle is negative.') if not np.all(np.isclose(np.linalg.norm(ro[...,0:3],axis=-1), 1.0)): raise ValueError('Rodrigues vector rotation axis is not of unit length.') return Rotation(Rotation._ro2qu(ro))
[docs] @staticmethod def from_homochoric(h, P = -1): """ Initialize from homochoric vector. Parameters ---------- h : numpy.ndarray of shape (...,3) Homochoric vector (h_1, h_2, h_3) with ǀhǀ < (3/4*π)^(1/3). P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. """ ho = np.array(h,dtype=float) if ho.shape[:-2:-1] != (3,): raise ValueError('Invalid shape.') if abs(P) != 1: raise ValueError('P ∉ {-1,1}') ho *= -P if np.any(np.linalg.norm(ho,axis=-1) >_R1+1e-9): raise ValueError('Homochoric coordinate outside of the sphere.') return Rotation(Rotation._ho2qu(ho))
[docs] @staticmethod def from_cubochoric(x, P = -1): """ Initialize from cubochoric vector. Parameters ---------- x : numpy.ndarray of shape (...,3) Cubochoric vector (x_1, x_2, x_3) with max(x_i) < 1/2*π^(2/3). P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. """ cu = np.array(x,dtype=float) if cu.shape[:-2:-1] != (3,): raise ValueError('Invalid shape.') if abs(P) != 1: raise ValueError('P ∉ {-1,1}') if np.abs(np.max(cu)) > np.pi**(2./3.) * 0.5+1e-9: raise ValueError('Cubochoric coordinate outside of the cube.') ho = -P * Rotation._cu2ho(cu) return Rotation(Rotation._ho2qu(ho))
[docs] @staticmethod def from_random(shape = None, rng_seed = None): """ Initialize with random rotation. Rotations are uniformly distributed. Parameters ---------- shape : tuple of ints, optional Shape of the sample. Defaults to None, which gives a single rotation. rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. """ rng = np.random.default_rng(rng_seed) r = rng.random(3 if shape is None else tuple(shape)+(3,) if hasattr(shape, '__iter__') else (shape,3)) A = np.sqrt(r[...,2]) B = np.sqrt(1.0-r[...,2]) q = np.stack([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],axis=-1) return Rotation(q if shape is None else q.reshape(r.shape[:-1]+(4,)))._standardize()
[docs] @staticmethod def from_ODF(weights, phi, N = 500, degrees = True, fractions = True, rng_seed = None, **kwargs): """ Sample discrete values from a binned orientation distribution function (ODF). Parameters ---------- weights : numpy.ndarray of shape (n) Texture intensity values (probability density or volume fraction) at Euler space grid points. phi : numpy.ndarray of shape (n,3) Grid coordinates in Euler space at which weights are defined. N : integer, optional Number of discrete orientations to be sampled from the given ODF. Defaults to 500. degrees : boolean, optional Euler space grid coordinates are in degrees. Defaults to True. fractions : boolean, optional ODF values correspond to volume fractions, not probability densities. Defaults to True. rng_seed: {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. Returns ------- samples : damask.Rotation of shape (N) Array of sampled rotations closely representing the input ODF. Notes ----- Due to the distortion of Euler space in the vicinity of ϕ = 0, probability densities, p, defined on grid points with ϕ = 0 will never result in reconstructed orientations as their dV/V = p dγ = p × 0. Hence, it is recommended to transform any such dataset to cell centers that avoid grid points at ϕ = 0. References ---------- P. Eisenlohr and F. Roters, Computational Materials Science 42(4):670-678, 2008 https://doi.org/10.1016/j.commatsci.2007.09.015 """ def _dg(eu,deg): """Return infinitesimal Euler space volume of bin(s).""" phi_sorted = eu[np.lexsort((eu[:,0],eu[:,1],eu[:,2]))] steps,size,_ = grid_filters.cellsSizeOrigin_coordinates0_point(phi_sorted) delta = np.radians(size/steps) if deg else size/steps return delta[0]*2.0*np.sin(delta[1]/2.0)*delta[2] / 8.0 / np.pi**2 * np.sin(np.radians(eu[:,1]) if deg else eu[:,1]) dg = 1.0 if fractions else _dg(phi,degrees) dV_V = dg * np.maximum(0.0,weights.squeeze()) return Rotation.from_Euler_angles(phi[util.hybrid_IA(dV_V,N,rng_seed)],degrees)
[docs] @staticmethod def from_spherical_component(center, sigma, N = 500, degrees = True, rng_seed = None): """ Calculate set of rotations with Gaussian distribution around center. Parameters ---------- center : Rotation Central Rotation. sigma : float Standard deviation of (Gaussian) misorientation distribution. N : int, optional Number of samples. Defaults to 500. degrees : boolean, optional sigma is given in degrees. Defaults to True. rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. """ rng = np.random.default_rng(rng_seed) sigma = np.radians(sigma) if degrees else sigma u,Theta = (rng.random((N,2)) * 2.0 * np.array([1,np.pi]) - np.array([1.0, 0])).T omega = abs(rng.normal(scale=sigma,size=N)) p = np.column_stack([np.sqrt(1-u**2)*np.cos(Theta), np.sqrt(1-u**2)*np.sin(Theta), u, omega]) return Rotation.from_axis_angle(p) * center
[docs] @staticmethod def from_fiber_component(alpha, beta, sigma = 0.0, N = 500, degrees = True, rng_seed = None): """ Calculate set of rotations with Gaussian distribution around direction. Parameters ---------- alpha : numpy.ndarray of shape (2) Polar coordinates (phi from x, theta from z) of fiber direction in crystal frame. beta : numpy.ndarray of shape (2) Polar coordinates (phi from x, theta from z) of fiber direction in sample frame. sigma : float, optional Standard deviation of (Gaussian) misorientation distribution. Defaults to 0. N : int, optional Number of samples. Defaults to 500. degrees : boolean, optional sigma, alpha, and beta are given in degrees. rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. """ rng = np.random.default_rng(rng_seed) sigma_,alpha_,beta_ = map(np.radians,(sigma,alpha,beta)) if degrees else (sigma,alpha,beta) d_cr = np.array([np.sin(alpha_[0])*np.cos(alpha_[1]), np.sin(alpha_[0])*np.sin(alpha_[1]), np.cos(alpha_[0])]) d_lab = np.array([np.sin( beta_[0])*np.cos( beta_[1]), np.sin( beta_[0])*np.sin( beta_[1]), np.cos( beta_[0])]) ax_align = np.append(np.cross(d_lab,d_cr), np.arccos(np.dot(d_lab,d_cr))) if np.isclose(ax_align[3],0.0): ax_align[:3] = np.array([1,0,0]) R_align = Rotation.from_axis_angle(ax_align if ax_align[3] > 0.0 else -ax_align,normalize=True) # rotate fiber axis from sample to crystal frame u,Theta = (rng.random((N,2)) * 2.0 * np.array([1,np.pi]) - np.array([1.0, 0])).T omega = abs(rng.normal(scale=sigma_,size=N)) p = np.column_stack([np.sqrt(1-u**2)*np.cos(Theta), np.sqrt(1-u**2)*np.sin(Theta), u, omega]) p[:,:3] = np.einsum('ij,...j',np.eye(3)-np.outer(d_lab,d_lab),p[:,:3]) # remove component along fiber axis f = np.column_stack((np.broadcast_to(d_lab,(N,3)),rng.random(N)*np.pi)) f[::2,:3] *= -1 # flip half the rotation axes to negative sense return R_align.broadcast_to(N) \ * Rotation.from_axis_angle(p,normalize=True) \ * Rotation.from_axis_angle(f)