Source code for blueprints.utils.geometry

"""
Geometry contains primitive functions for geometric operations on MoveableThings 
position and orientation. Orientations are currently only supported for (improper)
Euler angles and angles are currently assumed to be in radian. To convert angles the 
predefined constants ``DEGREES_TO_RADIANS`` and ``RADIANS_TO_DEGREES`` can be used.

Attributes
----------
TAU : float
	Tau is the circumference of a unit circle. :math:`\\pi \\times 2 = \\tau`.
PI : float
	Pi is the half circumference of a unit circle. :math:`\\pi \\times 2 = \\tau`.
DEGREES_TO_RADIANS : float
	To convert an angle in degrees to radian it can be multiplied with this constant.
RADIANS_TO_DEGREES : float
	To convert an angle in radian to degrees it can be multiplied with this constant.
"""
import numpy as np
import blueprints as blue



TAU = 6.2831853071795864769252867664
PI  = 3.1415926535897932384626433832

DEGREES_TO_RADIANS = TAU / 360
RADIANS_TO_DEGREES = 360 / TAU



[docs] class Rotation: """ This class implements multiple functions that operate on a MoveableThings orientation. Attributes ---------- X : np.ndarray The unit vector :math:`\\vec{\\text{id}_x}` representing the ``X``-axis. Y : np.ndarray The unit vector :math:`\\vec{\\text{id}_y}` representing the ``Y``-axis. Z : np.ndarray The unit vector :math:`\\vec{\\text{id}_z}` representing the ``Z``-axis. """ X = np.array([1, 0, 0]) Y = np.array([0, 1, 0]) Z = np.array([0, 0, 1])
[docs] @classmethod def euler(cls, vector, alpha, beta, gamma): """ Rotates a vector according to improper euler angles. Parameters ---------- vector : np.ndarray The vector to be rotated. alpha : float The angle along which the vector is rotated on the ``X``-axis. beta : float The angle along which the vector is rotated on the ``Y``-axis. gamma : float The angle along which the vector is rotated on the ``Z``-axis. Returns ------- np.ndarray The rotated vector. """ R = cls.E_rot(alpha, beta, gamma) return R @ vector
[docs] @classmethod def rotate_around_axis(cls, vector, axis, angle): """ Rotates a vector around an axis for a given angles. Parameters ---------- vector : np.ndarray The vector to be rotated axis : np.ndarray The axis around which the vector is rotated according to the right hand rule. angle : float The angle by which the vector is rotated. Returns ------- np.ndarray The rotated vector. """ axis = Vector.normalize(axis) return vector * np.cos(angle) + \ np.cross(axis, vector) * np.sin(angle) + \ axis * np.dot(axis, vector) * (1 - np.cos(angle))
[docs] @classmethod def global_orientation(cls, node): """ This method calculates the global orientation of a Thing (typically a node in a kinematic tree). Parameters ---------- node : Thing A Thing (typically a node in a kinematic tree) for which the global orientation is calculated w.r.t it's root. Returns ------- (float, float, float) The improper euler angles representing the global orientation. """ R = np.eye(3) while node is not None and isinstance(node, blue.MoveableThingType): M = cls.E_rot(node.alpha, node.beta, node.gamma) R = M @ R node = node.parent alpha, beta, gamma = cls.reference_frame_to_euler(R) return alpha, beta, gamma
[docs] @classmethod def global_rotation_matrix(cls, node): """ This method calculates the rotation matrix for the global orientation of a Thing (typically a node in e kinematic tree) w.r.t. to its root. Parameters ---------- node : Thing A Thing (typically a node in a kinematic tree) for which the global orientation is calculated w.r.t it's root. Returns ------- np.ndarray The global orientation matrix for the node. """ alpha, beta, gamma = cls.global_orientation(node) return cls.E_rot(alpha, beta, gamma)
[docs] @classmethod def euler_to_reference_frame(cls, alpha: float, beta: float, gamma: float): """ This method constructs a reference frame (i. e. the unit vectors for the axis of the local coordinate system) for (improper) euler angles. Parameters ---------- alpha : float The angle by which the ``X``-axis is rotated to reach the reference frame. beta : float The angle by which the ``Y``-axis is rotated to reach the reference frame. gamma : float The angle by which the ``Z``-axis is rotated to reach the reference frame. Returns ------- (np.ndarray, np.ndarray, np.ndarray) A tuple of the three unit vectors for the given orientation representing the reference frame. """ M = cls.M_rot(alpha, beta, gamma) R_x, R_y, R_z = np.rollaxis(M, axis=1) return R_x, R_y, R_z
[docs] @classmethod def reference_frame_to_euler(cls, R): """ Computes the (improper) euler angles that lead to the given reference frame. Parameters ---------- R : np.ndarray A matrix representing the reference frame as a concatenation of the unit coulmn vectors of the orientations coordinate system. Returns ------- (float, float, float) The (improper) euler angles for the given reference frame. Raises ------ ArithmeticError If the reference frame does not contain orthogonal vectors that do not adhere to the right hand rule convention am ArithmeticError is raised. """ X = np.array([1, 0, 0]) Y = np.array([0, 1, 0]) Z = np.array([0, 0, 1]) R_x, R_y, R_z = np.rollaxis(R, axis=1) try: assert Vector.equal(np.cross(R_x, R_y), R_z) except AssertionError: raise ArithmeticError('Referenceframe to euler convertion only supports rotation and not reflections but the given reference frame contains a reflected axis.') R_x = Vector.normalize(R_x) R_y = Vector.normalize(R_y) R_z = Vector.normalize(R_z) R_x1, R_x2, R_x3 = R_x R_y1, R_y2, R_y3 = R_y R_z1, R_z2, R_z3 = R_z if R_y1 != 0: Y_bar = np.array([0, -R_z3/R_y1, R_z2/R_y1]) Y_bar = Vector.normalize(Y_bar) X_bar = np.cross(Y_bar, R_z) else: X_bar = R_x Y_bar = R_y Z_bar = np.cross(X, Y_bar) alpha = cls.angle(Z, Z_bar, axis=X) beta = cls.angle(X, X_bar, axis=Y_bar) gamma = cls.angle(Y_bar, R_y, axis=R_z) return alpha, beta, gamma
[docs] @classmethod def angle(cls, a, b, axis=None) -> float: """ Computes the angle between two vectors. If axis is set, the angle is measured according to a rotation around axis from a to b, otherwise the shortest angle is returned. Parameters ---------- a : np.ndarray A vector for which the angle is computed w.r.t. b. b : np.ndarray A vector for which the angle is computed w.r.t. a. axis : None|np.ndarray, optional The axis of rotation can be specified optionally, if this is not done, the resulting angles will be in the range [0, PI). Returns ------- float The resulting angle will be in the range [0, PI) if axis is not specified, otherwise the angle will be in the range [0, TAU). """ a = Vector.normalize(a) b = Vector.normalize(b) c = np.cross(a, b) axis = Vector.normalize(axis) if axis is not None else c sign = np.sign(axis @ c) sign = sign if sign != 0 else 1 # DOT PRODUCT CLIPPING FOR NUMERICAL STABILITY angle = np.arccos(np.clip(a @ b, -1, 1)) return sign * angle
[docs] @classmethod def N_rot(cls, node=None) -> np.ndarray: """ This method returns the rotation matrix for a given Thing. If the Thing does not inherit from MoveableThing the identity metrix is returned. Parameters ---------- node : None, ThingType A Thing, typically a node from a kinematic tree. Returns ------- np.ndarray A rotation matrix from the Things orientation. """ if node and isinstance(node, blue.MoveableThingType): alpha, beta, gamma = node.alpha, node.beta, node.gamma else: alpha, beta, gamma = 0, 0, 0 return cls.E_rot(alpha, beta, gamma)
[docs] @staticmethod def X_rot(alpha) -> np.ndarray: """ Constructs the rotation matrix around the ``X``-axis for a given angle. Parameters ---------- alpha : float The angle of rotation. Returns ------- np.ndarray The rotation matrix for the given angle. """ return np.array([[ 1, 0, 0], [ 0, np.cos(alpha),-np.sin(alpha)], [ 0, np.sin(alpha), np.cos(alpha)]])
[docs] @staticmethod def Y_rot(beta) -> np.ndarray: """ Constructs the rotation matrix around the ``Y``-axis for a given angle. Parameters ---------- beta : float The angle of rotation. Returns ------- np.ndarray The rotation matrix for the given angle. """ return np.array([[ np.cos(beta) , 0, np.sin(beta) ], [ 0, 1, 0], [-np.sin(beta) , 0, np.cos(beta) ]])
[docs] @staticmethod def Z_rot(gamma) -> np.ndarray: """ Constructs the rotation matrix around the ``Z``-axis for a given angle. Parameters ---------- gamma : float The angle of rotation. Returns ------- np.ndarray The rotation matrix for the given angle. """ return np.array([[ np.cos(gamma) ,-np.sin(gamma), 0], [ np.sin(gamma) , np.cos(gamma), 0], [ 0, 0, 1]])
[docs] @classmethod def E_rot(cls, alpha = None, beta = None, gamma = None) -> np.ndarray: """ Constructs the rotation matrix for improper euler angles. Parameters ---------- alpha : float The angle around which the ``X``-axis is rotated. beta : float The angle around which the ``Y``-axis is rotated. gamma : float The angle around which the ``Z``-axis is rotated. Returns ------- np.ndarray The rotation matrix for the improper euler angles. """ X = cls.X_rot(alpha or 0) Y = cls.Y_rot(beta or 0) Z = cls.Z_rot(gamma or 0) R = X @ Y @ Z return R
[docs] @classmethod def Axis_rot(cls, R) -> np.ndarray: """ Returns ------- np.ndarray The axis of rotation for a given rotation matrix or reference frame """ S = 0.5 * (R - R.T) return np.array([S[2, 1], S[0, 2], S[1, 0]])
[docs] @classmethod def quat_to_euler(cls, R, I, J, K) -> tuple[float]: """ Parameters ---------- R : float The real rotation angle component of the quaternion. I : float The ``X``-axis component of the quaternion. J : float The ``Y``-axis component of the quaternion. K : float The ``Z``-axis component of the quaternion. Returns ------- tuple[float] The improper euler angles for a given quaternion. """ R = np.array([[1 - 2*(J**2 + K**2), 2*(I*J - R*K), 2*(R*J + I*K)], [ 2*(I*J + R*K), 1 - 2*(I**2 + K**2), 2*(J*K - R*I)], [ 2*(I*K - R*J), 2*(R*I + J*K), 1 - 2*(I**2 + J**2)]]) return cls.reference_frame_to_euler(R)
[docs] @classmethod def euler_to_quat(cls, alpha, beta, gamma) -> tuple[float]: """ Parameters ---------- alpha : float The angle around which the ``X``-axis is rotated. beta : float The angle around which the ``Y``-axis is rotated. gamma : float The angle around which the ``Z``-axis is rotated. Returns ------- tuple[float] The quaternion components for improper euler angles. """ R = cls.E_rot(alpha, beta, gamma) axis = cls.Axis_rot(R) angle = np.arccos(0.5 * (R[0, 0] + R[1, 1] + R[2, 2] - 1)) alpha_prime = cls.angle(axis, cls.X) beta_prime = cls.angle(axis, cls.Y) gamma_prime = cls.angle(axis, cls.Z) R = np.cos(angle/2) I = np.sin(angle/2) * np.cos(alpha_prime) J = np.sin(angle/2) * np.cos(beta_prime) K = np.sin(angle/2) * np.cos(gamma_prime) return R, I, J, K
[docs] class Vector: """ This class implements multiple functions to modify vectors and orientations of MoveableThings. """
[docs] @classmethod def get_axis(cls, axis: str|list[int|float]|np.ndarray): """ This method returns a vector that represents the axis which is either given as a str or as a 1D array like object. >>> Vector.get_axis("x") array([1., 0., 0.]) >>> Vector.get_axis([0., 1., 0.]) array([0., 1., 0.]) >>> Vector.get_axis(np.array([0., 0., 1.])) array([0., 0., 1.]) Parameters ---------- axis : str | list[int | float] | np.ndarray The axis argument may be either a 1D array like object or a string ('x', 'y', 'z'). Returns ------- np.ndarray The specified axis converted to a numpy array. Raises ------ ValueError If the axis argument is a string and neither 'x', 'y' or 'z' a ValueError is raise. """ if isinstance(axis, str): axis = axis.strip().lower() if axis not in ('x', 'y', 'z'): raise ValueError(f"Attribute axis must be 'x', 'y' or 'z', got '{axis}' instead.") if axis == 'x': return np.array([1., 0., 0.], dtype=np.float32) elif axis == 'y': return np.array([0., 1., 0.], dtype=np.float32) elif axis == 'z': return np.array([0., 0., 1.], dtype=np.float32) else: return np.array(axis, dtype=np.float32)
[docs] @staticmethod def equal(a: np.ndarray, b: np.ndarray, sensitivity: int = 8): """ Compares whether two vectors are equal within a margin of error as given by sensitivity. >>> a = np.array([3.2, 0., 0.]) >>> b = np.array([3.8, 0., 0.]) >>> c = np.array([3.2, 5., 0.]) >>> Vector.equal(a, b, sensitivity=0) True >>> Vector.equal(a, b, sensitivity=1) False >>> Vector.equal(a, c, sensitivity=0) False >>> Vector.equal(a, c, sensitivity=-1) True Parameters ---------- a : np.ndarray A vector to be compared. b : np.ndarray A vector to be compared. sensitivity : int, optional The floating point decimal cutoff. Returns ------- bool A boolean indicating whether the two vectors are equal within the specified margin of error. """ return np.all(np.round(a - b, sensitivity) == 0)
[docs] @classmethod def global_position(cls, node, pos=None): """ This method converts the local position of a Thing (typically a node in a kinematic tree) to its global position relative to the nodes root. Parameters ---------- node : Thing A Thing (typically a node in a kinematic tree). pos : None, optional An initial position that is assumed to be located relative to the node. Returns ------- np.ndarray The global position relative to root of the given node, or the initial position relative to the node is pos was specified. """ pos = pos or np.zeros(3) while node is not None: if hasattr(node, 'pos'): pos = node.pos + node.rotation_matrix @ pos node = node.parent return pos
[docs] @staticmethod def normalize(vector: np.ndarray) -> np.ndarray: """ This method normalizes a vector to a unit lenth of 1. Parameters ---------- vector : np.ndarray A vector to be normalized. Returns ------- np.ndarray The normalized vector. """ norm = np.linalg.norm(vector) assert norm > 0, 'The vector norm must be positive for normalization!' return vector/norm
[docs] @staticmethod def distance(a: np.ndarray|blue.MoveableThingType, b: np.ndarray|blue.MoveableThingType) -> float: """ This method returns the distance between two vectors or MoveableThings. Parameters ---------- a : np.ndarray | blue.MoveableThingType A vector or MoveableThings global position for which the distance is computed w.r.t. b. b : np.ndarray | blue.MoveableThingType A vector or MoveableThings global position for which the distance is computed w.r.t. a. Returns ------- float The distance betweent the two vectors or MoveableThings. """ if not isinstance(a, blue.MoveableThingType): a = a.global_pos if not isinstance(b, blue.MoveableThingType): b = b.global_pos return np.linalg.norm(b - a)
#class Distance: # # """ # This class implements methods to determin the distance between Things. # # Attributes # ---------- # BOX : tuple # A collection of Things that are equivalent in form but inherit from different classes (BaseGeom and BaseSite). # CAPSULE : tuple # A collection of Things that are equivalent in form but inherit from different classes (BaseGeom and BaseSite). # CYLINDER : tuple # A collection of Things that are equivalent in form but inherit from different classes (BaseGeom and BaseSite). # ELLIPSOID : tuple # A collection of Things that are equivalent in form but inherit from different classes (BaseGeom and BaseSite). # PLANE : tuple # A collection of Things that are equivalent in form but inherit from different classes (BaseGeom and BaseSite). # SPHERE : tuple # A collection of Things that are equivalent in form but inherit from different classes (BaseGeom and BaseSite). # TYPES : dict # A dictionary of all forms, containing the other collections. # """ # # CAPSULE = (blue.geoms.Capsule, blue.sites.Capsule) # CYLINDER = (blue.geoms.Cylinder, blue.sites.Cylinder) # SPHERE = (blue.geoms.Sphere, blue.sites.Sphere) # ELLIPSOID = (blue.geoms.Ellipsoid, blue.sites.Ellipsoid) # BOX = (blue.geoms.Box, blue.sites.Box) # PLANE = (blue.geoms.Plane,) # TYPES = ('CAPSULE', 'CYLINDER', 'PLANE', 'SPHERE', 'ELLIPSOID', 'BOX') # @classmethod # def type(cls, thing): # """ # Returns the form type of an object for a given Thing. # # Parameters # ---------- # thing : Thing # A Thing either geom or site. # # Returns # ------- # tuple # The collection of classes that implement the form of the given Thing. # # Raises # ------ # NotImplemented # If the Things type is not implemented, NotImplemented is raised. # """ # for TYPE in cls.TYPES: # if isinstance(thing, cls.__getattribute__(cls, TYPE)): # return TYPE # raise NotImplemented # # # def __getattr__(self, attr): # """ # This method is used to make min distance calls symetric. # # Parameters # ---------- # attr : str # A string of the methods name, for example "_min_CAPSULE_CYLINDER" # # Returns # ------- # method # The distance method reflected in arguments. # # Raises # ------ # AttributeError # If the attribute does not adhere to the distance function naming conventions an AttributeError is raised. # """ # if attr.startswith('_min_'): # type_a, type_b = attr.replace('_min_', '').split('_') # attr = f'_min_{type_b}_{type_a}' # method = self.__getattribute__(attr) # def wrapper(self, thing_a, thing_b): # return method(thing_b, thing_a) # return wrapper # raise AttributeError # # # def center(thing_a, thing_b): # """ # A method that returns the distance between the centers of two Things. # # Parameters # ---------- # thing_a : ThingType # Description # thing_b : ThingType # Description # # Returns # ------- # TYPE # Description # """ # return Vector.distance(thing_a.pos, thing_b.pos) # # # @classmethod # def minimum(cls, thing_a, thing_b): # """ # Calculates the minimum distance betweeen two Things analytically. # Currently the min Distance is approximated with an upper bound, precise # distance calculations will be unrolled over time. # # Parameters # ---------- # thing_a : ThingType # A Thing for which the minimum distance is calculated w.r.t. thing_b. # thing_b : ThingType # A Thing for which the minimum distance is calculated w.r.t. thing_a. # # Returns # ------- # float # The (currently approximated) minimum Distance between the two Things. # """ # #### FAST HACK # pos_a = Vector.global_position(thing_a) # pos_b = Vector.global_position(thing_b) # size_a = np.max(thing_a.size) # size_b = np.max(thing_b.size) # distance = Vector.distance(thing_a, thing_b) # return distance - size_a - size_b # #### FUTURE # assert isinstance(thing_a, (blue.geoms.Geom, blue.sites.Site)) and isinstance(thing_b, (blue.geoms.Geom, blue.sites.Site)) # type_a, type_b = cls.type(thing_a), cls.type(thing_b) # method_name = f'_min_{type_a}_{type_b}' # method = getattr(cls, method_name) # return method(thing_a, thing_b) # # # @classmethod # def _min_SPHERE_SPHERE(cls, thing_a, thing_b): # """ # Calculates the distance between two Spheres analytically # # Parameters # ---------- # thing_a : Sphere # A Sphere for which the minimum distance is calculated analytically w.r.t. thing_b. # thing_b : Sphere # A Sphere for which the minimum distance is calculated analytically w.r.t. thing_a. # # Returns # ------- # float # The minimum distance between two Spheres. # """ # return cls.center(thing_a, thing_b) - thing_a.radius - thing_b.radius if __name__ == '__bluen__': R = np.stack([Rotation.Y, Rotation.Z, Rotation.X], axis=1) alpha, beta, gamma = Rotation.reference_frame_to_euler(R) print(alpha, beta, gamma) S1 = blue.geoms.Sphere(radius=1.5) S2 = blue.geoms.Sphere(pos=[3.5, 0, 0]) print(Distance.minimum(S1, S2))