Spaces:
No application file
No application file
| # Copyright (C) 2004, Thomas Hamelryck (thamelry@binf.ku.dk) | |
| # | |
| # This file is part of the Biopython distribution and governed by your | |
| # choice of the "Biopython License Agreement" or the "BSD 3-Clause License". | |
| # Please see the LICENSE file that should have been included as part of this | |
| # package. | |
| """Vector class, including rotation-related functions.""" | |
| import numpy # type: ignore | |
| from typing import Tuple, Optional | |
| def m2rotaxis(m): | |
| """Return angles, axis pair that corresponds to rotation matrix m. | |
| The case where ``m`` is the identity matrix corresponds to a singularity | |
| where any rotation axis is valid. In that case, ``Vector([1, 0, 0])``, | |
| is returned. | |
| """ | |
| eps = 1e-5 | |
| # Check for singularities a la | |
| # http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToAngle/ # noqa | |
| if ( | |
| abs(m[0, 1] - m[1, 0]) < eps | |
| and abs(m[0, 2] - m[2, 0]) < eps | |
| and abs(m[1, 2] - m[2, 1]) < eps | |
| ): | |
| # Singularity encountered. Check if its 0 or 180 deg | |
| if ( | |
| abs(m[0, 1] + m[1, 0]) < eps | |
| and abs(m[0, 2] + m[2, 0]) < eps | |
| and abs(m[1, 2] + m[2, 1]) < eps | |
| and abs(m[0, 0] + m[1, 1] + m[2, 2] - 3) < eps | |
| ): | |
| angle = 0 | |
| else: | |
| angle = numpy.pi | |
| else: | |
| # Angle always between 0 and pi | |
| # Sense of rotation is defined by axis orientation | |
| t = 0.5 * (numpy.trace(m) - 1) | |
| t = max(-1, t) | |
| t = min(1, t) | |
| angle = numpy.arccos(t) | |
| if angle < 1e-15: | |
| # Angle is 0 | |
| return 0.0, Vector(1, 0, 0) | |
| elif angle < numpy.pi: | |
| # Angle is smaller than pi | |
| x = m[2, 1] - m[1, 2] | |
| y = m[0, 2] - m[2, 0] | |
| z = m[1, 0] - m[0, 1] | |
| axis = Vector(x, y, z) | |
| axis.normalize() | |
| return angle, axis | |
| else: | |
| # Angle is pi - special case! | |
| m00 = m[0, 0] | |
| m11 = m[1, 1] | |
| m22 = m[2, 2] | |
| if m00 > m11 and m00 > m22: | |
| x = numpy.sqrt(m00 - m11 - m22 + 0.5) | |
| y = m[0, 1] / (2 * x) | |
| z = m[0, 2] / (2 * x) | |
| elif m11 > m00 and m11 > m22: | |
| y = numpy.sqrt(m11 - m00 - m22 + 0.5) | |
| x = m[0, 1] / (2 * y) | |
| z = m[1, 2] / (2 * y) | |
| else: | |
| z = numpy.sqrt(m22 - m00 - m11 + 0.5) | |
| x = m[0, 2] / (2 * z) | |
| y = m[1, 2] / (2 * z) | |
| axis = Vector(x, y, z) | |
| axis.normalize() | |
| return numpy.pi, axis | |
| def vector_to_axis(line, point): | |
| """Vector to axis method. | |
| Return the vector between a point and | |
| the closest point on a line (ie. the perpendicular | |
| projection of the point on the line). | |
| :type line: L{Vector} | |
| :param line: vector defining a line | |
| :type point: L{Vector} | |
| :param point: vector defining the point | |
| """ | |
| line = line.normalized() | |
| np = point.norm() | |
| angle = line.angle(point) | |
| return point - line ** (np * numpy.cos(angle)) | |
| def rotaxis2m(theta, vector): | |
| """Calculate left multiplying rotation matrix. | |
| Calculate a left multiplying rotation matrix that rotates | |
| theta rad around vector. | |
| :type theta: float | |
| :param theta: the rotation angle | |
| :type vector: L{Vector} | |
| :param vector: the rotation axis | |
| :return: The rotation matrix, a 3x3 Numeric array. | |
| Examples | |
| -------- | |
| >>> from numpy import pi | |
| >>> from Bio.PDB.vectors import rotaxis2m | |
| >>> from Bio.PDB.vectors import Vector | |
| >>> m = rotaxis2m(pi, Vector(1, 0, 0)) | |
| >>> Vector(1, 2, 3).left_multiply(m) | |
| <Vector 1.00, -2.00, -3.00> | |
| """ | |
| vector = vector.normalized() | |
| c = numpy.cos(theta) | |
| s = numpy.sin(theta) | |
| t = 1 - c | |
| x, y, z = vector.get_array() | |
| rot = numpy.zeros((3, 3)) | |
| # 1st row | |
| rot[0, 0] = t * x * x + c | |
| rot[0, 1] = t * x * y - s * z | |
| rot[0, 2] = t * x * z + s * y | |
| # 2nd row | |
| rot[1, 0] = t * x * y + s * z | |
| rot[1, 1] = t * y * y + c | |
| rot[1, 2] = t * y * z - s * x | |
| # 3rd row | |
| rot[2, 0] = t * x * z - s * y | |
| rot[2, 1] = t * y * z + s * x | |
| rot[2, 2] = t * z * z + c | |
| return rot | |
| rotaxis = rotaxis2m | |
| def refmat(p, q): | |
| """Return a (left multiplying) matrix that mirrors p onto q. | |
| :type p,q: L{Vector} | |
| :return: The mirror operation, a 3x3 Numeric array. | |
| Examples | |
| -------- | |
| >>> from Bio.PDB.vectors import refmat | |
| >>> p, q = Vector(1, 2, 3), Vector(2, 3, 5) | |
| >>> mirror = refmat(p, q) | |
| >>> qq = p.left_multiply(mirror) | |
| >>> print(q) | |
| <Vector 2.00, 3.00, 5.00> | |
| >>> print(qq) | |
| <Vector 1.21, 1.82, 3.03> | |
| """ | |
| p = p.normalized() | |
| q = q.normalized() | |
| if (p - q).norm() < 1e-5: | |
| return numpy.identity(3) | |
| pq = p - q | |
| pq.normalize() | |
| b = pq.get_array() | |
| b.shape = (3, 1) | |
| i = numpy.identity(3) | |
| ref = i - 2 * numpy.dot(b, numpy.transpose(b)) | |
| return ref | |
| def rotmat(p, q): | |
| """Return a (left multiplying) matrix that rotates p onto q. | |
| :param p: moving vector | |
| :type p: L{Vector} | |
| :param q: fixed vector | |
| :type q: L{Vector} | |
| :return: rotation matrix that rotates p onto q | |
| :rtype: 3x3 Numeric array | |
| Examples | |
| -------- | |
| >>> from Bio.PDB.vectors import rotmat | |
| >>> p, q = Vector(1, 2, 3), Vector(2, 3, 5) | |
| >>> r = rotmat(p, q) | |
| >>> print(q) | |
| <Vector 2.00, 3.00, 5.00> | |
| >>> print(p) | |
| <Vector 1.00, 2.00, 3.00> | |
| >>> p.left_multiply(r) | |
| <Vector 1.21, 1.82, 3.03> | |
| """ | |
| rot = numpy.dot(refmat(q, -p), refmat(p, -p)) | |
| return rot | |
| def calc_angle(v1, v2, v3): | |
| """Calculate angle method. | |
| Calculate the angle between 3 vectors | |
| representing 3 connected points. | |
| :param v1, v2, v3: the tree points that define the angle | |
| :type v1, v2, v3: L{Vector} | |
| :return: angle | |
| :rtype: float | |
| """ | |
| v1 = v1 - v2 | |
| v3 = v3 - v2 | |
| return v1.angle(v3) | |
| def calc_dihedral(v1, v2, v3, v4): | |
| """Calculate dihedral angle method. | |
| Calculate the dihedral angle between 4 vectors | |
| representing 4 connected points. The angle is in | |
| ]-pi, pi]. | |
| :param v1, v2, v3, v4: the four points that define the dihedral angle | |
| :type v1, v2, v3, v4: L{Vector} | |
| """ | |
| ab = v1 - v2 | |
| cb = v3 - v2 | |
| db = v4 - v3 | |
| u = ab**cb | |
| v = db**cb | |
| w = u**v | |
| angle = u.angle(v) | |
| # Determine sign of angle | |
| try: | |
| if cb.angle(w) > 0.001: | |
| angle = -angle | |
| except ZeroDivisionError: | |
| # dihedral=pi | |
| pass | |
| return angle | |
| class Vector: | |
| """3D vector.""" | |
| def __init__(self, x, y=None, z=None): | |
| """Initialize the class.""" | |
| if y is None and z is None: | |
| # Array, list, tuple... | |
| if len(x) != 3: | |
| raise ValueError("Vector: x is not a list/tuple/array of 3 numbers") | |
| self._ar = numpy.array(x, "d") | |
| else: | |
| # Three numbers | |
| self._ar = numpy.array((x, y, z), "d") | |
| def __repr__(self): | |
| """Return vector 3D coordinates.""" | |
| x, y, z = self._ar | |
| return f"<Vector {x:.2f}, {y:.2f}, {z:.2f}>" | |
| def __neg__(self): | |
| """Return Vector(-x, -y, -z).""" | |
| a = -self._ar | |
| return Vector(a) | |
| def __add__(self, other): | |
| """Return Vector+other Vector or scalar.""" | |
| if isinstance(other, Vector): | |
| a = self._ar + other._ar | |
| else: | |
| a = self._ar + numpy.array(other) | |
| return Vector(a) | |
| def __sub__(self, other): | |
| """Return Vector-other Vector or scalar.""" | |
| if isinstance(other, Vector): | |
| a = self._ar - other._ar | |
| else: | |
| a = self._ar - numpy.array(other) | |
| return Vector(a) | |
| def __mul__(self, other): | |
| """Return Vector.Vector (dot product).""" | |
| return sum(self._ar * other._ar) | |
| def __truediv__(self, x): | |
| """Return Vector(coords/a).""" | |
| a = self._ar / numpy.array(x) | |
| return Vector(a) | |
| def __pow__(self, other): | |
| """Return VectorxVector (cross product) or Vectorxscalar.""" | |
| if isinstance(other, Vector): | |
| a, b, c = self._ar | |
| d, e, f = other._ar | |
| c1 = numpy.linalg.det(numpy.array(((b, c), (e, f)))) | |
| c2 = -numpy.linalg.det(numpy.array(((a, c), (d, f)))) | |
| c3 = numpy.linalg.det(numpy.array(((a, b), (d, e)))) | |
| return Vector(c1, c2, c3) | |
| else: | |
| a = self._ar * numpy.array(other) | |
| return Vector(a) | |
| def __getitem__(self, i): | |
| """Return value of array index i.""" | |
| return self._ar[i] | |
| def __setitem__(self, i, value): | |
| """Assign values to array index i.""" | |
| self._ar[i] = value | |
| def __contains__(self, i): | |
| """Validate if i is in array.""" | |
| return i in self._ar | |
| def norm(self): | |
| """Return vector norm.""" | |
| return numpy.sqrt(sum(self._ar * self._ar)) | |
| def normsq(self): | |
| """Return square of vector norm.""" | |
| return abs(sum(self._ar * self._ar)) | |
| def normalize(self): | |
| """Normalize the Vector object. | |
| Changes the state of ``self`` and doesn't return a value. | |
| If you need to chain function calls or create a new object | |
| use the ``normalized`` method. | |
| """ | |
| if self.norm(): | |
| self._ar = self._ar / self.norm() | |
| def normalized(self): | |
| """Return a normalized copy of the Vector. | |
| To avoid allocating new objects use the ``normalize`` method. | |
| """ | |
| v = self.copy() | |
| v.normalize() | |
| return v | |
| def angle(self, other): | |
| """Return angle between two vectors.""" | |
| n1 = self.norm() | |
| n2 = other.norm() | |
| c = (self * other) / (n1 * n2) | |
| # Take care of roundoff errors | |
| c = min(c, 1) | |
| c = max(-1, c) | |
| return numpy.arccos(c) | |
| def get_array(self): | |
| """Return (a copy of) the array of coordinates.""" | |
| return numpy.array(self._ar) | |
| def left_multiply(self, matrix): | |
| """Return Vector=Matrix x Vector.""" | |
| a = numpy.dot(matrix, self._ar) | |
| return Vector(a) | |
| def right_multiply(self, matrix): | |
| """Return Vector=Vector x Matrix.""" | |
| a = numpy.dot(self._ar, matrix) | |
| return Vector(a) | |
| def copy(self): | |
| """Return a deep copy of the Vector.""" | |
| return Vector(self._ar) | |
| """Homogeneous matrix geometry routines. | |
| Rotation, translation, scale, and coordinate transformations. | |
| Robert T. Miller 2019 | |
| """ | |
| def homog_rot_mtx(angle_rads: float, axis: str) -> numpy.array: | |
| """Generate a 4x4 single-axis numpy rotation matrix. | |
| :param float angle_rads: the desired rotation angle in radians | |
| :param char axis: character specifying the rotation axis | |
| """ | |
| cosang = numpy.cos(angle_rads) | |
| sinang = numpy.sin(angle_rads) | |
| if "z" == axis: | |
| return numpy.array( | |
| ( | |
| (cosang, -sinang, 0, 0), | |
| (sinang, cosang, 0, 0), | |
| (0, 0, 1, 0), | |
| (0, 0, 0, 1), | |
| ), | |
| dtype=numpy.float64, | |
| ) | |
| elif "y" == axis: | |
| return numpy.array( | |
| ( | |
| (cosang, 0, sinang, 0), | |
| (0, 1, 0, 0), | |
| (-sinang, 0, cosang, 0), | |
| (0, 0, 0, 1), | |
| ), | |
| dtype=numpy.float64, | |
| ) | |
| else: | |
| return numpy.array( | |
| ( | |
| (1, 0, 0, 0), | |
| (0, cosang, -sinang, 0), | |
| (0, sinang, cosang, 0), | |
| (0, 0, 0, 1), | |
| ), | |
| dtype=numpy.float64, | |
| ) | |
| def set_Z_homog_rot_mtx(angle_rads: float, mtx: numpy.ndarray): | |
| """Update existing Z rotation matrix to new angle.""" | |
| cosang = numpy.cos(angle_rads) | |
| sinang = numpy.sin(angle_rads) | |
| mtx[0][0] = mtx[1][1] = cosang | |
| mtx[1][0] = sinang | |
| mtx[0][1] = -sinang | |
| def set_Y_homog_rot_mtx(angle_rads: float, mtx: numpy.ndarray): | |
| """Update existing Y rotation matrix to new angle.""" | |
| cosang = numpy.cos(angle_rads) | |
| sinang = numpy.sin(angle_rads) | |
| mtx[0][0] = mtx[2][2] = cosang | |
| mtx[0][2] = sinang | |
| mtx[2][0] = -sinang | |
| def set_X_homog_rot_mtx(angle_rads: float, mtx: numpy.ndarray): | |
| """Update existing X rotation matrix to new angle.""" | |
| cosang = numpy.cos(angle_rads) | |
| sinang = numpy.sin(angle_rads) | |
| mtx[1][1] = mtx[2][2] = cosang | |
| mtx[2][1] = sinang | |
| mtx[1][2] = -sinang | |
| def homog_trans_mtx(x: float, y: float, z: float) -> numpy.array: | |
| """Generate a 4x4 numpy translation matrix. | |
| :param x, y, z: translation in each axis | |
| """ | |
| return numpy.array( | |
| ((1, 0, 0, x), (0, 1, 0, y), (0, 0, 1, z), (0, 0, 0, 1)), | |
| dtype=numpy.float64, | |
| ) | |
| def set_homog_trans_mtx(x: float, y: float, z: float, mtx: numpy.ndarray): | |
| """Update existing translation matrix to new values.""" | |
| mtx[0][3] = x | |
| mtx[1][3] = y | |
| mtx[2][3] = z | |
| def homog_scale_mtx(scale: float) -> numpy.array: | |
| """Generate a 4x4 numpy scaling matrix. | |
| :param float scale: scale multiplier | |
| """ | |
| return numpy.array( | |
| [[scale, 0, 0, 0], [0, scale, 0, 0], [0, 0, scale, 0], [0, 0, 0, 1]], | |
| dtype=numpy.float64, | |
| ) | |
| def _get_azimuth(x: float, y: float) -> float: | |
| sign_y = -1.0 if y < 0.0 else 1.0 | |
| sign_x = -1.0 if x < 0.0 else 1.0 | |
| return ( | |
| numpy.arctan2(y, x) | |
| if (0 != x and 0 != y) | |
| else (numpy.pi / 2.0 * sign_y) # +/-90 if X=0, Y!=0 | |
| if 0 != y | |
| else numpy.pi | |
| if sign_x < 0.0 # 180 if Y=0, X < 0 | |
| else 0.0 # 0 if Y=0, X >= 0 | |
| ) | |
| def get_spherical_coordinates(xyz: numpy.array) -> Tuple[float, float, float]: | |
| """Compute spherical coordinates (r, azimuth, polar_angle) for X,Y,Z point. | |
| :param array xyz: column vector (3 row x 1 column numpy array) | |
| :return: tuple of r, azimuth, polar_angle for input coordinate | |
| """ | |
| r = numpy.linalg.norm(xyz) | |
| if 0 == r: | |
| return (0, 0, 0) | |
| azimuth = _get_azimuth(xyz[0], xyz[1]) | |
| polar_angle = numpy.arccos(xyz[2] / r) | |
| return (r, azimuth, polar_angle) | |
| gtm = numpy.identity(4, dtype=numpy.float64) | |
| gmrz = numpy.identity(4, dtype=numpy.float64) | |
| gmry = numpy.identity(4, dtype=numpy.float64) | |
| gmrz2 = numpy.identity(4, dtype=numpy.float64) | |
| def coord_space( | |
| a0: numpy.ndarray, a1: numpy.ndarray, a2: numpy.ndarray, rev: bool = False | |
| ) -> Tuple[numpy.ndarray, Optional[numpy.ndarray]]: | |
| """Generate transformation matrix to coordinate space defined by 3 points. | |
| New coordinate space will have: | |
| acs[0] on XZ plane | |
| acs[1] origin | |
| acs[2] on +Z axis | |
| :param numpy column array x3 acs: X,Y,Z column input coordinates x3 | |
| :param bool rev: if True, also return reverse transformation matrix | |
| (to return from coord_space) | |
| :returns: 4x4 numpy array, x2 if rev=True | |
| """ | |
| # dbg = False | |
| # if dbg: | |
| # print(a0.transpose()) | |
| # print(a1.transpose()) | |
| # print(a2.transpose()) | |
| # a0 = acs[0] | |
| # a1 = acs[1] | |
| # a2 = acs[2] | |
| global gtm | |
| global gmry | |
| global gmrz, gmrz2 | |
| tm = gtm | |
| mry = gmry | |
| mrz = gmrz | |
| mrz2 = gmrz2 | |
| # tx acs[1] to origin | |
| # tm = homog_trans_mtx(-a1[0][0], -a1[1][0], -a1[2][0]) | |
| set_homog_trans_mtx(-a1[0], -a1[1], -a1[2], tm) | |
| # directly translate a2 using a1 | |
| p = a2 - a1 | |
| sc = get_spherical_coordinates(p) | |
| # if dbg: | |
| # print("p", p.transpose()) | |
| # print("sc", sc) | |
| # rotate translated a2 -azimuth about Z | |
| set_Z_homog_rot_mtx(-sc[1], mrz) | |
| # rotate translated a2 -polar_angle about Y | |
| set_Y_homog_rot_mtx(-sc[2], mry) | |
| # mt completes a1-a2 on Z-axis, still need to align a0 with XZ plane | |
| # mt = mry @ mrz @ tm # python 3.5 and later | |
| mt = gmry.dot(gmrz.dot(gtm)) | |
| # if dbg: | |
| # print("tm:\n", tm) | |
| # print("mrz:\n", mrz) | |
| # print("mry:\n", mry) | |
| # # print("mt ", mt) | |
| p = mt.dot(a0) | |
| # if dbg: | |
| # print("mt:\n", mt, "\na0:\n", a0, "\np:\n", p) | |
| # need azimuth of translated a0 | |
| # sc2 = get_spherical_coordinates(p) | |
| # print(sc2) | |
| azimuth2 = _get_azimuth(p[0], p[1]) | |
| # rotate a0 -azimuth2 about Z to align with X | |
| # mrz2 = homog_rot_mtx(-azimuth2, "z") | |
| set_Z_homog_rot_mtx(-azimuth2, mrz2) | |
| # mt = mrz2 @ mt | |
| mt = gmrz2.dot(mt) | |
| # if dbg: | |
| # print("mt:", mt, "\na0:", a0, "\np:", p) | |
| # # print(p, "\n", azimuth2, "\n", mrz2, "\n", mt) | |
| # if dbg: | |
| # print("mt:\n", mt) | |
| # print("<<<<<<==============================") | |
| if not rev: | |
| return mt, None | |
| # rev=True, so generate the reverse transformation | |
| # rotate a0 theta about Z, reversing alignment with X | |
| # mrz2 = homog_rot_mtx(azimuth2, "z") | |
| set_Z_homog_rot_mtx(azimuth2, mrz2) | |
| # rotate a2 phi about Y | |
| # mry = homog_rot_mtx(sc[2], "y") | |
| set_Y_homog_rot_mtx(sc[2], mry) | |
| # rotate a2 theta about Z | |
| # mrz = homog_rot_mtx(sc[1], "z") | |
| set_Z_homog_rot_mtx(sc[1], mrz) | |
| # translation matrix origin to a1 | |
| # tm = homog_trans_mtx(a1[0][0], a1[1][0], a1[2][0]) | |
| set_homog_trans_mtx(a1[0], a1[1], a1[2], tm) | |
| # mr = tm @ mrz @ mry @ mrz2 | |
| mr = gtm.dot(gmrz.dot(gmry.dot(gmrz2))) | |
| # mr = numpy.dot(tm, numpy.dot(mrz, numpy.dot(mry, mrz2))) | |
| return mt, mr | |
| def multi_rot_Z(angle_rads: numpy.ndarray) -> numpy.ndarray: | |
| """Create [entries] numpy Z rotation matrices for [entries] angles. | |
| :param entries: int number of matrices generated. | |
| :param angle_rads: numpy array of angles | |
| :returns: entries x 4 x 4 homogeneous rotation matrices | |
| """ | |
| rz = numpy.empty((angle_rads.shape[0], 4, 4)) | |
| rz[...] = numpy.identity(4) | |
| rz[:, 0, 0] = rz[:, 1, 1] = numpy.cos(angle_rads) | |
| rz[:, 1, 0] = numpy.sin(angle_rads) | |
| rz[:, 0, 1] = -rz[:, 1, 0] | |
| return rz | |
| def multi_rot_Y(angle_rads: numpy.ndarray) -> numpy.ndarray: | |
| """Create [entries] numpy Y rotation matrices for [entries] angles. | |
| :param entries: int number of matrices generated. | |
| :param angle_rads: numpy array of angles | |
| :returns: entries x 4 x 4 homogeneous rotation matrices | |
| """ | |
| ry = numpy.empty((angle_rads.shape[0], 4, 4)) | |
| ry[...] = numpy.identity(4) | |
| ry[:, 0, 0] = ry[:, 2, 2] = numpy.cos(angle_rads) | |
| ry[:, 0, 2] = numpy.sin(angle_rads) | |
| ry[:, 2, 0] = -ry[:, 0, 2] | |
| return ry | |
| def multi_coord_space(a3: numpy.ndarray, dLen: int, rev: bool = False) -> numpy.ndarray: | |
| """Generate [dLen] transform matrices to coord space defined by 3 points. | |
| New coordinate space will have: | |
| acs[0] on XZ plane | |
| acs[1] origin | |
| acs[2] on +Z axis | |
| :param numpy array [entries]x3x3 [entries] XYZ coords for 3 atoms | |
| :param bool rev: if True, also return reverse transformation matrix | |
| (to return from coord_space) | |
| :returns: [entries] 4x4 numpy arrays, x2 if rev=True | |
| """ | |
| # build tm translation matrix: atom1 to origin | |
| tm = numpy.empty((dLen, 4, 4)) | |
| tm[...] = numpy.identity(4) | |
| tm[:, 0:3, 3] = -a3[:, 1, 0:3] | |
| # directly translate a2 into new space using a1 | |
| p = a3[:, 2] - a3[:, 1] | |
| # get spherical coords of translated a2 (p) | |
| r = numpy.linalg.norm(p, axis=1) | |
| azimuth = numpy.arctan2(p[:, 1], p[:, 0]) | |
| polar_angle = numpy.arccos(numpy.divide(p[:, 2], r, where=r != 0)) | |
| # build rz rotation matrix: translated a2 -azimuth around Z | |
| # (enables next step rotating around Y to align with Z) | |
| rz = multi_rot_Z(-azimuth) | |
| # build ry rotation matrix: translated a2 -polar_angle around Y | |
| ry = multi_rot_Y(-polar_angle) | |
| # mt completes a1-a2 on Z-axis, still need to align a0 with XZ plane | |
| mt = numpy.matmul(ry, numpy.matmul(rz, tm)) | |
| # transform a0 to mt space | |
| p = numpy.matmul(mt, a3[:, 0].reshape(-1, 4, 1)).reshape(-1, 4) | |
| # print(f"mt[0]:\n{mt[0]}\na3[0][0] (a0):\n{a3[0][0]}\np[0]:\n{p[0]}") | |
| # get azimuth of translated a0 | |
| azimuth2 = numpy.arctan2(p[:, 1], p[:, 0]) | |
| # build rotation matrix rz2 to rotate a0 -azimuth about Z to align with X | |
| rz2 = multi_rot_Z(-azimuth2) | |
| # update mt to be complete transform into hedron coordinate space | |
| if not rev: | |
| return numpy.matmul(rz2, mt[:]) | |
| # rev=True, so generate the reverse transformation | |
| mt = numpy.matmul(rz2, mt[:]) | |
| # rotate a0 theta about Z, reversing alignment with X | |
| mrz2 = multi_rot_Z(azimuth2) | |
| # rotate a2 phi about Y | |
| mry = multi_rot_Y(polar_angle) | |
| # rotate a2 theta about Z | |
| mrz = multi_rot_Z(azimuth) | |
| # translation matrix origin to a1 | |
| tm[:, 0:3, 3] = a3[:, 1, 0:3] | |
| mr = tm @ mrz @ mry @ mrz2 # tm.dot(mrz.dot(mry.dot(mrz2))) | |
| return numpy.array([mt, mr]) | |