Source code for spharpy.transforms.rotations

"""
Rotation/Translation operations for data in the spherical harmonic domains
"""

import numpy as np
import spharpy
from scipy.special import eval_jacobi, factorial
from scipy.spatial.transform import Rotation


[docs]class RotationSH(Rotation): """Class for rotations of coordinates and data. """
[docs] def __init__(self, quat, n_max=0, *args, **kwargs): """Initialize Parameters ---------- quat : array_like, shape (N, 4) or (4,) Each row is a (possibly non-unit norm) quaternion in scalar-last (x, y, z, w) format. Each quaternion will be normalized to unit norm. n_max : int The spherical harmonic order Returns ------- RotationSH The rotation object with spherical harmonic order n_max. Note ---- Initializing using the constructor is not advised. Always use the respective ``from_quat`` method. """ super().__init__(quat, *args, **kwargs) if n_max < 0: raise ValueError("The order needs to be a positive value.") self._n_max = int(n_max)
[docs] @classmethod def from_rotvec(cls, n_max, rotvec, degrees=False, *args, **kwargs): """Initialize from rotation vectors. A rotation vector is a 3 dimensional vector which is co-directional to the axis of rotation and whose norm gives the angle of rotation [#]_. Parameters ---------- n_max : int Spherical harmonic order rotvec : array_like, float, shape (L, 3) or (3,) A single vector or a stack of vectors, where rot_vec[i] gives the ith rotation vector. degrees : bool, optional Specify if rotation angles are defined in degrees instead of radians, by default False. Returns ------- RotationSH Object containing the rotations represented by input rotation vectors. References ---------- .. [#] https://en.wikipedia.org/wiki/Axis%E2%80%93angle_representation\ #Rotation_vector Examples -------- >>> from spharpy.transforms import Rotation as R >>> rot = R.from_rotvec(np.pi/2 * np.array([0, 0, 1])) >>> rot.as_spherical_harmonic() """ if degrees: rotvec = np.deg2rad(rotvec) cls = super(RotationSH, cls).from_rotvec(rotvec, *args, **kwargs) cls.n_max = n_max return cls
[docs] @classmethod def from_euler(cls, n_max, seq, angles, degrees=False, **kwargs): """Initialize from Euler angles. Rotations in 3-D can be represented by a sequence of 3 rotations around a sequence of axes. In theory, any three axes spanning the 3-D Euclidean space are enough. In practice, the axes of rotation are chosen to be the basis vectors. The three rotations can either be in a global frame of reference (extrinsic) or in a body centred frame of reference (intrinsic), which is attached to, and moves with, the object under rotation [#]_. Parameters ---------- n_max : int Spherical harmonic order. seq : str Specifies sequence of axes for rotations. Up to 3 characters belonging to the set {‘X’, ‘Y’, ‘Z’} for intrinsic rotations, or {‘x’, ‘y’, ‘z’} for extrinsic rotations. Extrinsic and intrinsic rotations cannot be mixed in one function call. angles : (float or array_like, shape (N,) or (N, [1 or 2 or 3])) Euler angles specified in radians (degrees is False) or degrees (degrees is True). For a single character seq, angles can be: - a single value - array_like with shape (N,), where each ``angle[i]`` corresponds to a single rotation - array_like with shape (N, 1), where each ``angle[i, 0]`` corresponds to a single rotation For 2- and 3-character wide seq, angles can be: - array_like with shape (W,) where W is the width of ``seq``, which corresponds to a single rotation with W axes - array_like with shape (N, W) where each ``angle[i]`` corresponds to a sequence of Euler angles describing a single rotation degrees : bool, optional If True, then the given angles are assumed to be in degrees. Default is False. Returns ------- RotationSH Object containing the rotation represented by the sequence of rotations around given axes with given angles. References ---------- .. [#] https://en.wikipedia.org/wiki/Euler_angles Examples -------- >>> from spharpy.transforms import Rotation as R >>> rot = R.from_euler('z', 90, degrees=True) >>> rot.as_spherical_harmonic() """ cls = super(RotationSH, cls).from_euler( seq, angles, degrees=degrees, **kwargs) cls.n_max = n_max return cls
[docs] @classmethod def from_quat(cls, n_max, quat, **kwargs): """Initialize from quaternions. 3D rotations can be represented using unit-norm quaternions [#]_. Parameters ---------- n_max : int Spherical harmonic order quat : (array_like, shape (N, 4) or (4,)) Each row is a (possibly non-unit norm) quaternion in scalar-last (x, y, z, w) format. Each quaternion will be normalized to unit norm. Returns ------- RotationSH Object containing the rotations represented by input quaternions. References ---------- .. [#] https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation Examples -------- >>> from spharpy.transforms import Rotation as R >>> rot = R.from_quat([1, 0, 0, 0]) >>> rot.as_spherical_harmonic() """ cls = super(RotationSH, cls).from_quat(quat, **kwargs) cls.n_max = n_max return cls
[docs] @classmethod def from_matrix(cls, n_max, matrix, **kwargs): """Initialize from rotation matrix. Rotations in 3 dimensions can be represented with 3 x 3 proper orthogonal matrices [1]_. If the input is not proper orthogonal, an approximation is created using the method described in [2]_. Parameters ---------- n_max : int Spherical harmonic order matrix : (array_like, shape (N, 3, 3) or (3, 3)) A single matrix or a stack of matrices, where ``matrix[i]`` is the i-th matrix. Returns ------- RotationSH Object containing the rotations represented by the rotation matrices. References ---------- .. [1] https://en.wikipedia.org/wiki/Rotation_matrix .. [2] F. Landis Markley, “Unit Quaternion from Rotation Matrix”, Journal of guidance, control, and dynamics vol. 31.2, pp. 440-442, 2008. Examples -------- >>> from spharpy.transforms import Rotation as R >>> rot = R.from_matrix([ ... [0, -1, 0], ... [1, 0, 0], ... [0, 0, 1]]) >>> rot.as_spherical_harmonic() """ cls = super(RotationSH, cls).from_matrix(matrix, **kwargs) cls.n_max = n_max return cls
@property def n_max(self): """The spherical harmonic order used for spherical harmonic rotation matrices. """ return self._n_max @n_max.setter def n_max(self, value): """Set the spherical harmonic order Parameters ---------- value : int The spherical harmonic order used for spherical harmonic rotation matrices. """ if value < 0: raise ValueError("The order needs to be a positive value.") self._n_max = value
[docs] def as_spherical_harmonic(self, type='real'): """Export the rotation operations as a spherical harmonic rotation matrices. Supports complex and real-valued spherical harmonics. Parameters ---------- real : string, optional Spherical harmonic definition. Can either be 'complex' or 'real', by default 'real' is used. Returns ------- array, complex or float Stack of block-diagonal rotation matrices. """ euler_angles = np.atleast_2d(self.as_euler('zyz')) n_matrices = euler_angles.shape[0] n_sh = (self.n_max+1)**2 if type == 'real': dtype = float rot_func = wigner_d_rotation_real elif type == 'complex': dtype = complex rot_func = wigner_d_rotation else: raise ValueError("Invalid spherical harmonic type {}".format(type)) D = np.zeros((n_matrices, n_sh, n_sh), dtype=dtype) for idx, angles in enumerate(euler_angles): D[idx, :, :] = rot_func( self.n_max, angles[0], angles[1], angles[2]) return np.squeeze(D)
[docs] def apply(self, coefficients, type='real'): """Apply the rotation to L sets of spherical harmonic coefficients Parameters ---------- coefficients : array, complex, shape :math:`((n_max+1)^2, L)` L sets of spherical harmonic coefficients with a respective order :math:`((n_max+1)^2` Returns ------- array, complex The rotated data """ D = self.as_spherical_harmonic(type=type) if D.ndim > 2: M = np.diag(np.ones((self.n_max+1)**2)) for d in D: M = M @ d else: M = D return M @ coefficients
[docs]def rotation_z_axis(n_max, angle): """Rotation matrix for complex spherical harmonics around the z-axis by a given angle. The rotation is performed such that positive angles result in a counter clockwise rotation of the data [#]_. .. math:: c_{nm}(\\theta, \\phi + \\xi) = e^{-im\\xi} c_{nm}(\\theta, \\phi) Parameters ---------- n_max : integer Spherical harmonic order angle : number Rotation angle in radians `[0, 2 \\pi]` Returns ------- array_like, complex, shape :math:`((n_{max}+1)^2, (n_{max}+1)^2)` Diagonal rotation matrix evaluated for the specified angle References ---------- .. [#] N. A. Gumerov and R. Duraiswami, “Recursions for the computation of multipole translation and rotation coefficients for the 3-d helmholtz equation,” vol. 25, no. 4, pp. 1344–1381, 2003. Examples -------- >>> import numpy as np >>> import spharpy >>> n_max = 1 >>> sh_vec = np.array([0, 1, 0, 0]) >>> rotMat = spharpy.transforms.rotation_z_axis(n_max, np.pi/2) >>> sh_vec_rotated = rotMat @ sh_vec """ acn = np.arange(0, (n_max+1)**2) m = spharpy.spherical.acn2nm(acn)[1] rotation_phi = np.exp(-1j*angle*m) return np.diag(rotation_phi)
[docs]def rotation_z_axis_real(n_max, angle): """Rotation matrix for real-valued spherical harmonics around the z-axis by a given angle. The rotation is performed such that positive angles result in a counter clockwise rotation of the data [#]_. Parameters ---------- n_max : integer Spherical harmonic order angle : number Rotation angle in radians `[0, 2 \\pi]` Returns ------- array_like, float, shape :math:`((n_max+1)^2, (n_max+1)^2)` Block-diagonal Rotation matrix evaluated for the specified angle. References ---------- .. [#] M. Kronlachner, “Spatial Transformations for the Alteration of Ambisonic Recordings,” Master Thesis, University of Music and Performing Arts, Graz, 2014. Examples -------- >>> import numpy as np >>> import spharpy >>> n_max = 1 >>> sh_vec = np.array([0, 1, 0, 0]) >>> rotMat = spharpy.transforms.rotation_z_axis_real(n_max, np.pi/2) >>> sh_vec_rotated = rotMat @ sh_vec """ acn = np.arange(0, (n_max + 1) ** 2) n, m = spharpy.spherical.acn2nm(acn) acn_reverse_degree = n ** 2 + n - m rotation_phi = np.zeros(((n_max + 1) ** 2, (n_max + 1) ** 2)) mask = m == 0 rotation_phi[acn[mask], acn[mask]] = 1.0 mask_pos = m > 0 mask_neg = m < 0 rotation_phi[acn[mask_pos], acn[mask_pos]] = np.cos( np.abs(m[mask_pos]) * angle) rotation_phi[acn[mask_neg], acn[mask_neg]] = np.cos( np.abs(m[mask_neg]) * angle) # non diagonal rotation_phi[acn[mask_pos], acn_reverse_degree[mask_pos]] = -np.sin( np.abs(m[mask_pos]) * angle) rotation_phi[acn[mask_neg], acn_reverse_degree[mask_neg]] = np.sin( np.abs(m[mask_neg]) * angle) return rotation_phi
[docs]def wigner_d_rotation(n_max, alpha, beta, gamma): r"""Wigner-D rotation matrix for Euler rotations by angles (\alpha, \beta, \gamma) around the (z,y,z)-axes. The implementation follows [#]_. and rotation is performed such that positive angles result in a counter clockwise rotation of the data. .. math:: D_{m^\prime,m}^n(\alpha, \beta, \gamma) = e^{-im^\prime\alpha} d_{m^\prime,m}^n(\beta) e^{-im\gamma} Parameters ---------- n_max : int Spherical harmonic order alpha : float First z-axis rotation angle beta : float Y-axis rotation angle gamma : float Second z-axis rotation angle Returns ------- array_like, complex,:math:`((n_max+1)^2, (n_max+1)^2)` Block diagonal rotation matrix Examples -------- >>> import numpy as np >>> import spharpy >>> n_max = 1 >>> sh_vec = np.array([0, 0, 1, 0]) >>> rotMat = spharpy.transforms.wigner_d_rotation(n_max, 0, np.pi/4, 0) >>> sh_vec_rotated = rotMat @ sh_vec References ---------- .. [#] B. Rafaely, Fundamentals of Spherical Array Processing, 1st ed., vol. 8. Springer-Verlag GmbH Berlin Heidelberg, 2015. """ n_sh = (n_max+1)**2 R = np.zeros((n_sh, n_sh), dtype=complex) for row in np.arange(0, (n_max+1)**2): n_dash, m_dash = spharpy.spherical.acn2nm(row) for column in np.arange(0, (n_max+1)**2): n, m = spharpy.spherical.acn2nm(column) if n == n_dash: rot_alpha = np.exp(-1j*m_dash*alpha) rot_beta = wigner_d_function(n, m_dash, m, beta) rot_gamma = np.exp(-1j*m*gamma) R[row, column] = rot_alpha * rot_beta * rot_gamma return R
[docs]def wigner_d_rotation_real(n_max, alpha, beta, gamma): r"""Wigner-D rotation matrix for Euler rotations for real-valued spherical harmonics by angles (\alpha, \beta, \gamma) around the (z,y,z)-axes. The implementation follows [#]_ and the rotation is performed such that positive angles result in a counter clockwise rotation of the data. Parameters ---------- n_max : int Spherical harmonic order alpha : float First z-axis rotation angle beta : float Y-axis rotation angle gamma : float Second z-axis rotation angle Returns ------- array_like, float, :math:`((n_max+1)^2, (n_max+1)^2)` Block diagonal rotation matrix Examples -------- >>> import numpy as np >>> import spharpy >>> n_max = 1 >>> sh_vec = np.array([0, 0, 1, 0]) >>> rotMat = spharpy.transforms.wigner_d_rotation_real( >>> n_max, 0, np.pi/4, 0) >>> sh_vec_rotated = rotMat @ sh_vec References ---------- .. [#] M. A. Blanco, M. Flórez, and M. Bermejo, “Evaluation of the rotation matrices in the basis of real spherical harmonics,” Journal of Molecular Structure: THEOCHEM, vol. 419, no. 1–3, pp. 19–27, Dec. 1997, doi: 10.1016/S0166-1280(97)00185-1. """ n_sh = (n_max+1)**2 R = np.zeros((n_sh, n_sh), dtype=float) for row_acn in np.arange(0, (n_max+1)**2): for col_acn in np.arange(0, (n_max+1)**2): n, m = spharpy.spherical.acn2nm(col_acn) n_dash, m_dash = spharpy.spherical.acn2nm(row_acn) if n == n_dash: # minus beta opposite rotation direction d_l_1 = wigner_d_function(n, np.abs(m_dash), np.abs(m), -beta) d_l_2 = wigner_d_function(n, np.abs(m), -np.abs(m_dash), -beta) R[row_acn, col_acn] = \ _sign(m_dash) * _Phi(m, alpha) * _Phi(m_dash, gamma) * \ (d_l_1 + (-1)**int(m) * d_l_2)/2 \ - _sign(m) * _Phi(-m, alpha) * _Phi(-m_dash, gamma) * \ (d_l_1 - (-1)**int(m) * d_l_2)/2 return R
def _sign(x): """ Returns sign of x, differs from numpy definition for x=0 """ if x < 0: sign = -1 else: sign = 1 return sign def _Phi(m, angle): """ Rotation Matrix around z-axis for real Spherical Harmonics as defined in Blanco et al., Evaluation of the rotation matrices in the basis of real spherical harmonics, eq.(8) """ if m > 0: phi = np.sqrt(2)*np.cos(m*angle) elif m == 0: phi = 1 elif m < 0: # minus due to differing phase convention phi = -np.sqrt(2)*np.sin(np.abs(m)*angle) return phi
[docs]def wigner_d_function(n, m_dash, m, beta): r"""Wigner-d function for rotations around the y-axis. Convention as defined in [#]_. Parameters ---------- n : int order m_dash : int degree m : int degree beta : float Rotation angle Returns ------- float Wigner-d symbol References ---------- .. [#] B. Rafaely, Fundamentals of Spherical Array Processing, 1st ed., vol. 8. Springer-Verlag GmbH Berlin Heidelberg, 2015. """ if m >= m_dash: sign = 1 else: sign = (-1)**int(m-m_dash) mu = np.abs(m_dash - m) nu = np.abs(m_dash + m) s = n - (mu + nu)/2 norm = (factorial(s)*factorial(s+mu+nu))/(factorial(s+mu)*factorial(s+nu)) P = eval_jacobi(s, mu, nu, np.cos(beta)) d = sign * np.sqrt(norm) * np.sin(beta/2)**mu * np.cos(beta/2)**nu * P return d