vectorize Rotation.fromXXX functions
This commit is contained in:
parent
3d10266fbc
commit
23fc58699f
|
@ -1,2 +1,5 @@
|
|||
[run]
|
||||
omit = tests/*
|
||||
damask/_asciitable.py
|
||||
damask/_test.py
|
||||
damask/config/*
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import numpy as np
|
||||
|
||||
from ._Lambert import ball_to_cube, cube_to_ball
|
||||
from . import mechanics
|
||||
|
||||
_P = -1
|
||||
|
||||
|
@ -61,6 +62,8 @@ class Rotation:
|
|||
|
||||
def __repr__(self):
|
||||
"""Orientation displayed as unit quaternion, rotation matrix, and Bunge-Euler angles."""
|
||||
if self.quaternion.shape != (4,):
|
||||
raise NotImplementedError
|
||||
return '\n'.join([
|
||||
'Quaternion: (real={:.3f}, imag=<{:+.3f}, {:+.3f}, {:+.3f}>)'.format(*(self.quaternion)),
|
||||
'Matrix:\n{}'.format(self.asMatrix()),
|
||||
|
@ -116,7 +119,7 @@ class Rotation:
|
|||
|
||||
def inverse(self):
|
||||
"""In-place inverse rotation/backward rotation."""
|
||||
self.quaternion[1:] *= -1
|
||||
self.quaternion[...,1:] *= -1
|
||||
return self
|
||||
|
||||
def inversed(self):
|
||||
|
@ -125,12 +128,12 @@ class Rotation:
|
|||
|
||||
|
||||
def standardize(self):
|
||||
"""In-place quaternion representation with positive q."""
|
||||
if self.quaternion[0] < 0.0: self.quaternion*=-1
|
||||
"""In-place quaternion representation with positive real part."""
|
||||
self.quaternion[self.quaternion[...,0] < 0.0] *= -1
|
||||
return self
|
||||
|
||||
def standardized(self):
|
||||
"""Quaternion representation with positive q."""
|
||||
"""Quaternion representation with positive real."""
|
||||
return self.copy().standardize()
|
||||
|
||||
|
||||
|
@ -165,7 +168,7 @@ class Rotation:
|
|||
|
||||
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].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
@ -251,107 +254,106 @@ class Rotation:
|
|||
# static constructors. The input data needs to follow the convention, options allow to
|
||||
# relax these convections
|
||||
@staticmethod
|
||||
def fromQuaternion(quaternion,
|
||||
acceptHomomorph = False,
|
||||
P = -1):
|
||||
def from_quaternion(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: {}.'.format(qu[0]))
|
||||
if not np.isclose(np.linalg.norm(qu), 1.0):
|
||||
raise ValueError('Quaternion is not of unit length: {} {} {} {}.'.format(*qu))
|
||||
qu = np.array(quaternion,dtype=float)
|
||||
|
||||
if P > 0: qu[...,1:4] *= -1 # convert from P=1 to P=-1
|
||||
if acceptHomomorph:
|
||||
qu[qu[...,0] < 0.0] *= -1
|
||||
else:
|
||||
if np.any(qu[...,0] < 0.0):
|
||||
raise ValueError('Quaternions need to have positive first(real) component.')
|
||||
if not np.all(np.isclose(np.linalg.norm(qu,axis=-1), 1.0)):
|
||||
raise ValueError('Quaternions need to have unit length.')
|
||||
|
||||
return Rotation(qu)
|
||||
|
||||
@staticmethod
|
||||
def fromEulers(eulers,
|
||||
degrees = False):
|
||||
def from_Eulers(eulers,
|
||||
degrees = False):
|
||||
|
||||
eu = np.array(eulers,dtype=float)
|
||||
|
||||
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π]: {} {} {}.'.format(*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 need to be in [0..2π],[0..π],[0..2π].')
|
||||
|
||||
return Rotation(Rotation.eu2qu(eu))
|
||||
|
||||
@staticmethod
|
||||
def fromAxisAngle(angleAxis,
|
||||
degrees = False,
|
||||
normalise = False,
|
||||
P = -1):
|
||||
def from_axis_angle(axis_angle,
|
||||
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..π]: {}.'.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: {} {} {}.'.format(*ax[0:3]))
|
||||
ax = np.array(axis_angle,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],axis=-1)
|
||||
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)):
|
||||
raise ValueError('Axis angle rotation axis is not of unit length.')
|
||||
|
||||
return Rotation(Rotation.ax2qu(ax))
|
||||
|
||||
@staticmethod
|
||||
def fromBasis(basis,
|
||||
orthonormal = True,
|
||||
reciprocal = False,
|
||||
):
|
||||
def from_basis(basis,
|
||||
orthonormal = True,
|
||||
reciprocal = False):
|
||||
|
||||
om = np.array(basis,dtype=float)
|
||||
|
||||
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
|
||||
om = np.linalg.inv(mechanics.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.dot(U,Vh)
|
||||
if not np.isclose(np.linalg.det(om),1.0):
|
||||
om = np.einsum('...ij,...jl->...il',U,Vh)
|
||||
if not np.all(np.isclose(np.linalg.det(om),1.0)):
|
||||
raise ValueError('matrix is not a proper rotation: {}.'.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: {}.'.format(om))
|
||||
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('matrix is not orthogonal.')
|
||||
|
||||
return Rotation(Rotation.om2qu(om))
|
||||
|
||||
@staticmethod
|
||||
def fromMatrix(om,
|
||||
):
|
||||
def from_matrix(om):
|
||||
|
||||
return Rotation.fromBasis(om)
|
||||
return Rotation.from_basis(om)
|
||||
|
||||
@staticmethod
|
||||
def fromRodrigues(rodrigues,
|
||||
normalise = False,
|
||||
P = -1):
|
||||
def from_Rodrigues(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: {} {} {}.'.format(*ro[0:3]))
|
||||
if ro[3] < 0.0:
|
||||
raise ValueError('Rodrigues rotation angle not positive: {}.'.format(ro[3]))
|
||||
ro = 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],axis=-1)
|
||||
if np.any(ro[...,3] < 0.0):
|
||||
raise ValueError('Rodrigues rotation angle not positive.')
|
||||
if not np.all(np.isclose(np.linalg.norm(ro[...,0:3],axis=-1), 1.0)):
|
||||
raise ValueError('Rodrigues rotation axis is not of unit length.')
|
||||
|
||||
return Rotation(Rotation.ro2qu(ro))
|
||||
|
||||
@staticmethod
|
||||
def fromHomochoric(homochoric,
|
||||
P = -1):
|
||||
def from_homochoric(homochoric,
|
||||
P = -1):
|
||||
|
||||
ho = np.array(homochoric,dtype=float)
|
||||
|
||||
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
|
||||
|
||||
if np.linalg.norm(ho) > (3.*np.pi/4.)**(1./3.)+1e-9:
|
||||
raise ValueError('Coordinate outside of the sphere: {} {} {}.'.format(ho))
|
||||
if np.any(np.linalg.norm(ho,axis=-1) > (3.*np.pi/4.)**(1./3.)+1e-9):
|
||||
raise ValueError('Coordinate outside of the sphere.')
|
||||
|
||||
return Rotation(Rotation.ho2qu(ho))
|
||||
|
||||
|
@ -359,8 +361,7 @@ class Rotation:
|
|||
def fromCubochoric(cubochoric,
|
||||
P = -1):
|
||||
|
||||
cu = cubochoric if isinstance(cubochoric, np.ndarray) and cubochoric.dtype == np.dtype(float) \
|
||||
else np.array(cubochoric,dtype=float)
|
||||
cu = np.array(cubochoric,dtype=float)
|
||||
|
||||
if np.abs(np.max(cu))>np.pi**(2./3.) * 0.5+1e-9:
|
||||
raise ValueError('Coordinate outside of the cube: {} {} {}.'.format(*cu))
|
||||
|
@ -403,17 +404,28 @@ class Rotation:
|
|||
|
||||
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()
|
||||
def from_random(shape=None):
|
||||
if shape is None:
|
||||
r = np.random.random(3)
|
||||
else:
|
||||
r = np.random.random(tuple(shape)+(3,))
|
||||
A = np.sqrt(r[...,2])
|
||||
B = np.sqrt(1.0-r[...,2])
|
||||
return Rotation(np.block([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()
|
||||
|
||||
# for compatibility (old names do not follow convention)
|
||||
fromQuaternion = from_quaternion
|
||||
fromEulers = from_Eulers
|
||||
fromAxisAngle = from_axis_angle
|
||||
fromBasis = from_basis
|
||||
fromMatrix = from_matrix
|
||||
fromRodrigues = from_Rodrigues
|
||||
fromHomochoric = from_homochoric
|
||||
fromRandom = from_random
|
||||
|
||||
####################################################################################################
|
||||
# Code below available according to the following conditions on https://github.com/MarDiehl/3Drotations
|
||||
|
|
|
@ -135,16 +135,16 @@ def PK2(P,F):
|
|||
|
||||
Parameters
|
||||
----------
|
||||
P : numpy.ndarray of shape (:,3,3) or (3,3)
|
||||
P : numpy.ndarray of shape (...,3,3) or (3,3)
|
||||
First Piola-Kirchhoff stress.
|
||||
F : numpy.ndarray of shape (:,3,3) or (3,3)
|
||||
F : numpy.ndarray of shape (...,3,3) or (3,3)
|
||||
Deformation gradient.
|
||||
|
||||
"""
|
||||
if np.shape(F) == np.shape(P) == (3,3):
|
||||
S = np.dot(np.linalg.inv(F),P)
|
||||
else:
|
||||
S = np.einsum('ijk,ikl->ijl',np.linalg.inv(F),P)
|
||||
S = np.einsum('...jk,...kl->...jl',np.linalg.inv(F),P)
|
||||
return symmetric(S)
|
||||
|
||||
|
||||
|
@ -241,7 +241,7 @@ def symmetric(T):
|
|||
|
||||
Parameters
|
||||
----------
|
||||
T : numpy.ndarray of shape (:,3,3) or (3,3)
|
||||
T : numpy.ndarray of shape (...,3,3) or (3,3)
|
||||
Tensor of which the symmetrized values are computed.
|
||||
|
||||
"""
|
||||
|
@ -254,12 +254,12 @@ def transpose(T):
|
|||
|
||||
Parameters
|
||||
----------
|
||||
T : numpy.ndarray of shape (:,3,3) or (3,3)
|
||||
T : numpy.ndarray of shape (...,3,3) or (3,3)
|
||||
Tensor of which the transpose is computed.
|
||||
|
||||
"""
|
||||
return T.T if np.shape(T) == (3,3) else \
|
||||
np.transpose(T,(0,2,1))
|
||||
np.swapaxes(T,axis2=-2,axis1=-1)
|
||||
|
||||
|
||||
def _polar_decomposition(T,requested):
|
||||
|
|
|
@ -157,6 +157,19 @@ class TestRotation:
|
|||
print(m,o,rot.asQuaternion())
|
||||
assert ok and o.max() < np.pi**(2./3.)*0.5+1.e-9
|
||||
|
||||
@pytest.mark.parametrize('function,invalid',[(Rotation.from_quaternion, np.array([-1,0,0,0])),
|
||||
(Rotation.from_quaternion, np.array([1,1,1,0])),
|
||||
(Rotation.from_Eulers, np.array([1,4,0])),
|
||||
(Rotation.from_axis_angle, np.array([1,0,0,4])),
|
||||
(Rotation.from_axis_angle, np.array([1,1,0,1])),
|
||||
(Rotation.from_matrix, np.random.rand(3,3)),
|
||||
(Rotation.from_Rodrigues, np.array([1,0,0,-1])),
|
||||
(Rotation.from_Rodrigues, np.array([1,1,0,1])),
|
||||
(Rotation.from_homochoric, np.array([2,2,2])) ])
|
||||
def test_invalid(self,function,invalid):
|
||||
with pytest.raises(ValueError):
|
||||
function(invalid)
|
||||
|
||||
@pytest.mark.parametrize('conversion',[Rotation.qu2om,
|
||||
Rotation.qu2eu,
|
||||
Rotation.qu2ax,
|
||||
|
|
Loading…
Reference in New Issue