Source code for mbuild.coordinate_transform

from warnings import warn, simplefilter
simplefilter('always', DeprecationWarning)

import numpy as np
from numpy.linalg import norm, svd, inv
from mbuild.utils.decorators import deprecated


__all__ = ['rotate', 'rotate_around_x', 'rotate_around_y', 'rotate_around_z',
           'spin', 'spin_x', 'spin_y', 'spin_z',
           'force_overlap', 'translate', 'translate_to',
           'x_axis_transform', 'y_axis_transform', 'z_axis_transform',

           # Deprecated
           'equivalence_transform']


[docs]def force_overlap(move_this, from_positions, to_positions, add_bond=True): """Computes an affine transformation that maps the from_positions to the respective to_positions, and applies this transformation to the compound. Parameters ---------- move_this : mb.Compound The Compound to be moved. from_positions : np.ndarray, shape=(n, 3), dtype=float Original positions. to_positions : np.ndarray, shape=(n, 3), dtype=float New positions. add_bond : bool, optional, default=True If `from_positions` and `to_positions` are `Ports`, create a bond between the two anchor atoms. """ from mbuild.port import Port T = None if isinstance(from_positions, (list, tuple)) and isinstance(to_positions, (list, tuple)): equivalence_pairs = zip(from_positions, to_positions) elif isinstance(from_positions, Port) and isinstance(to_positions, Port): equivalence_pairs, T = _choose_correct_port(from_positions, to_positions) from_positions.used = True to_positions.used = True else: equivalence_pairs = [(from_positions, to_positions)] if not T: T = _create_equivalence_transform(equivalence_pairs) atom_positions = move_this.xyz_with_ports atom_positions = T.apply_to(atom_positions) move_this.xyz_with_ports = atom_positions if add_bond: if isinstance(from_positions, Port) and isinstance(to_positions, Port): if not from_positions.anchor or not to_positions.anchor: warn("Attempting to form bond from port that has no anchor") else: from_positions.anchor.parent.add_bond((from_positions.anchor, to_positions.anchor)) to_positions.anchor.parent.add_bond((from_positions.anchor, to_positions.anchor)) from_positions.anchor.parent.remove(from_positions) to_positions.anchor.parent.remove(to_positions)
class CoordinateTransform(object): """ """ def __init__(self, T=None): if T is None: T = np.eye(4) self.T = T self.Tinv = inv(T) def apply_to(self, A): """Apply the coordinate transformation to points in A. """ if A.ndim == 1: A = np.expand_dims(A, axis=0) rows, cols = A.shape A_new = np.hstack([A, np.ones((rows, 1))]) A_new = np.transpose(self.T.dot(np.transpose(A_new))) return A_new[:, 0:cols] class Translation(CoordinateTransform): """Cartesian translation. """ def __init__(self, P): T = np.eye(4) T[0, 3] = P[0] T[1, 3] = P[1] T[2, 3] = P[2] super(Translation, self).__init__(T) class RotationAroundZ(CoordinateTransform): """Rotation around the z-axis. """ def __init__(self, theta): T = np.eye(4) T[0, 0] = np.cos(theta) T[0, 1] = -np.sin(theta) T[1, 0] = np.sin(theta) T[1, 1] = np.cos(theta) super(RotationAroundZ, self).__init__(T) class RotationAroundY(CoordinateTransform): """Rotation around the y-axis. """ def __init__(self, theta): T = np.eye(4) T[0, 0] = np.cos(theta) T[0, 2] = np.sin(theta) T[2, 0] = -np.sin(theta) T[2, 2] = np.cos(theta) super(RotationAroundY, self).__init__(T) class RotationAroundX(CoordinateTransform): """Rotation around the x-axis. """ def __init__(self, theta): T = np.eye(4) T[1, 1] = np.cos(theta) T[1, 2] = -np.sin(theta) T[2, 1] = np.sin(theta) T[2, 2] = np.cos(theta) super(RotationAroundX, self).__init__(T) class Rotation(CoordinateTransform): """Rotation around vector by angle theta. """ def __init__(self, theta, around): assert around.size == 3 T = np.eye(4) s = np.sin(theta) c = np.cos(theta) t = 1 - c n = around / norm(around) x = n[0] y = n[1] z = n[2] m = np.array([ [t * x * x + c, t * x * y - s * z, t * x * z + s * y], [t * x * y + s * z, t * y * y + c, t * y * z - s * x], [t * x * z - s * y, t * y * z + s * x, t * z * z + c]]) T[0:3, 0:3] = m super(Rotation, self).__init__(T) class ChangeOfBasis(CoordinateTransform): """Convert the basis of coordinates to another basis""" def __init__(self, basis, origin=None): assert np.shape(basis) == (3, 3) if origin is None: origin = np.array([0.0, 0.0, 0.0]) T = np.eye(4) T[0:3, 0:3] = basis T = inv(T) T[0:3, 3:4] = -np.array([origin]).transpose() super(ChangeOfBasis, self).__init__(T) class AxisTransform(CoordinateTransform): """ """ def __init__(self, new_origin=None, point_on_x_axis=None, point_on_xy_plane=None): if new_origin is None: new_origin = np.array([0.0, 0.0, 0.0]) if point_on_x_axis is None: point_on_x_axis = np.array([1.0, 0.0, 0.0]) if point_on_xy_plane is None: point_on_xy_plane = np.array([1.0, 1.0, 0.0]) # Change the basis such that p1 is the origin, p2 is on the x axis and # p3 is in the xy plane. p1 = new_origin p2 = point_on_x_axis # positive x axis p3 = point_on_xy_plane # positive y part of the x axis # The direction vector of our new x axis. newx = unit_vector(p2 - p1) p3_u = unit_vector(p3 - p1) newz = unit_vector(np.cross(newx, p3_u)) newy = np.cross(newz, newx) # Translation that moves new_origin to the origin. T_tr = np.eye(4) T_tr[0:3, 3:4] = -np.array([p1]).transpose() # Rotation that moves newx to the x axis, newy to the y axis, newz to # the z axis. B = np.eye(4) B[0:3, 0:3] = np.vstack((newx, newy, newz)) # The concatentaion of translation and rotation. B_tr = np.dot(B, T_tr) super(AxisTransform, self).__init__(B_tr) class RigidTransform(CoordinateTransform): """Computes the rigid transformation that maps points A to points B. See http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.173.2196&rep=rep1&type=pdf """ def __init__(self, A, B): """ Parameters ---------- A : np.ndarray, shape=(n, 3), dtype=float Points in source coordinate system. B : np.ndarray, shape=(n, 3), dtype=float Points in destination coordinate system. """ rows, _ = np.shape(A) centroid_A = np.mean(A, axis=0) centroid_B = np.mean(B, axis=0) centroid_A.shape = (1, 3) centroid_B.shape = (1, 3) H = np.zeros((3, 3), dtype=float) for i in range(rows): H = H + np.transpose(A[i, :] - centroid_A).dot((B[i, :] - centroid_B)) U, _, V = svd(H) V = np.transpose(V) R = V.dot(np.transpose(U)) C_A = np.eye(3) C_A = np.vstack([np.hstack([C_A, np.transpose(centroid_A) * -1.0]), np.array([0, 0, 0, 1])]) R_new = np.vstack([np.hstack([R, np.array([[0], [0], [0]])]), np.array([0, 0, 0, 1])]) C_B = np.eye(3) C_B = np.vstack([np.hstack([C_B, np.transpose(centroid_B)]), np.array([0, 0, 0, 1])]) T = C_B.dot(R_new).dot(C_A) super(RigidTransform, self).__init__(T) def unit_vector(v): """Returns the unit vector of the vector. """ return v / norm(v) def angle(u, v, w=None): """Returns the angle in radians between two vectors. """ if w is not None: u = u - v v = w - v c = np.dot(u, v) / norm(u) / norm(v) return np.arccos(np.clip(c, -1, 1)) def _create_equivalence_transform(equiv): """Compute an equivalence transformation that transforms this compound to another compound's coordinate system. Parameters ---------- equiv : np.ndarray, shape=(n, 3), dtype=float Array of equivalent points. Returns ------- T : CoordinateTransform Transform that maps this point cloud to the other point cloud's coordinates system. """ from mbuild.compound import Compound self_points = np.array([]) self_points.shape = (0, 3) other_points = np.array([]) other_points.shape = (0, 3) for pair in equiv: if not isinstance(pair, tuple) or len(pair) != 2: raise ValueError('Equivalence pair not a 2-tuple') if not (isinstance(pair[0], Compound) and isinstance(pair[1], Compound)): raise ValueError('Equivalence pair type mismatch: pair[0] is a {0} ' 'and pair[1] is a {1}'.format(type(pair[0]), type(pair[1]))) # TODO: vstack is slow, replace with list concatenation if not pair[0].children: self_points = np.vstack([self_points, pair[0].pos]) other_points = np.vstack([other_points, pair[1].pos]) else: for atom0 in pair[0]._particles(include_ports=True): self_points = np.vstack([self_points, atom0.pos]) for atom1 in pair[1]._particles(include_ports=True): other_points = np.vstack([other_points, atom1.pos]) T = RigidTransform(self_points, other_points) return T def equivalence_transform(compound, from_positions, to_positions, add_bond=True): """Computes an affine transformation that maps the from_positions to the respective to_positions, and applies this transformation to the compound. Parameters ---------- compound : mb.Compound The Compound to be transformed. from_positions : np.ndarray, shape=(n, 3), dtype=float Original positions. to_positions : np.ndarray, shape=(n, 3), dtype=float New positions. """ warn('The `equivalence_transform` function is being phased out in favor of' ' `force_overlap`.', DeprecationWarning) from mbuild.port import Port T = None if isinstance(from_positions, (list, tuple)) and isinstance(to_positions, (list, tuple)): equivalence_pairs = zip(from_positions, to_positions) elif isinstance(from_positions, Port) and isinstance(to_positions, Port): equivalence_pairs, T = _choose_correct_port(from_positions, to_positions) from_positions.used = True to_positions.used = True else: equivalence_pairs = [(from_positions, to_positions)] if not T: T = _create_equivalence_transform(equivalence_pairs) atom_positions = compound.xyz_with_ports atom_positions = T.apply_to(atom_positions) compound.xyz_with_ports = atom_positions if add_bond: if isinstance(from_positions, Port) and isinstance(to_positions, Port): if not from_positions.anchor or not to_positions.anchor: # TODO: I think warnings is undefined here warn("Attempting to form bond from port that has no anchor") else: from_positions.anchor.parent.add_bond((from_positions.anchor, to_positions.anchor)) to_positions.anchor.parent.add_bond((from_positions.anchor, to_positions.anchor)) def _choose_correct_port(from_port, to_port): """Chooses the direction when using an equivalence transform on two Ports. Each Port object actually contains 2 sets of 4 atoms, either of which can be used to make a connection with an equivalence transform. This function chooses the set of 4 atoms that makes the anchor atoms not overlap which is the intended behavior for most use-cases. TODO: -Increase robustness for cases where the anchors are a different distance from their respective ports. -Provide options in `force_overlap` to override this behavior. Parameters ---------- from_port : mb.Port to_port : mb.Port Returns ------- equivalence_pairs : tuple of Ports, shape=(2,) Technically, a tuple of the Ports' sub-Compounds ('up' or 'down') that are used to make the correct connection between components. """ # First we try matching the two 'up' ports. T1 = _create_equivalence_transform([(from_port['up'], to_port['up'])]) new_position = T1.apply_to(np.array(from_port.anchor.pos, ndmin=2)) dist_between_anchors_up_up = norm(new_position[0] - to_port.anchor.pos) # Then matching a 'down' with an 'up' port. T2 = _create_equivalence_transform([(from_port['down'], to_port['up'])]) new_position = T2.apply_to(np.array(from_port.anchor.pos, ndmin=2)) # Determine which transform places the anchors further away from each other. dist_between_anchors_down_up = norm(new_position[0] - to_port.anchor.pos) difference_between_distances = dist_between_anchors_down_up - dist_between_anchors_up_up if difference_between_distances > 0: correct_port = from_port['down'] T = T2 else: correct_port = from_port['up'] T = T1 return [(correct_port, to_port['up'])], T warning_message = 'Please use Compound.translate()' @deprecated(warning_message) def translate(compound, pos): """Translate a compound by a vector. Parameters ---------- compound : mb.Compound The compound being translated. pos : np.ndarray, shape=(3,), dtype=float The vector to translate the compound by. """ atom_positions = compound.xyz_with_ports atom_positions = Translation(pos).apply_to(atom_positions) compound.xyz_with_ports = atom_positions warning_message = 'Please use Compound.translate_to()' @deprecated(warning_message) def translate_to(compound, pos): """Translate a compound to a coordinate. Parameters ---------- compound : mb.Compound The compound being translated. pos : np.ndarray, shape=(3,), dtype=float The coordinate to translate the compound to. """ atom_positions = compound.xyz_with_ports atom_positions -= compound.center atom_positions = Translation(pos).apply_to(atom_positions) compound.xyz_with_ports = atom_positions def _translate(coordinates, by): """Translate a set of coordinates by a vector. Parameters ---------- coordinates : np.ndarray, shape=(n,3), dtype=float The coordinates being translated. by : np.ndarray, shape=(3,), dtype=float The vector to translate the coordinates by. """ return Translation(by).apply_to(coordinates) def _translate_to(coordinates, to): """Translate a set of coordinates to a location. Parameters ---------- coordinates : np.ndarray, shape=(n,3), dtype=float The coordinates being translated. to : np.ndarray, shape=(3,), dtype=float The new average position of the coordinates. """ coordinates -= np.mean(coordinates, axis=0) return Translation(to).apply_to(coordinates) def _rotate(coordinates, theta, around): """Rotate a set of coordinates around an arbitrary vector. Parameters ---------- coordinates : np.ndarray, shape=(n,3), dtype=float The coordinates being rotated. theta : float The angle by which to rotate the coordinates, in radians. around : np.ndarray, shape=(3,), dtype=float The vector about which to rotate the coordinates. """ around = np.asarray(around).reshape(3) if np.array_equal(around, np.zeros(3)): raise ValueError('Cannot rotate around a zero vector') return Rotation(theta, around).apply_to(coordinates) warning_message = 'Please use Compound.rotate()' @deprecated(warning_message) def rotate(compound, theta, around): """Rotate a compound around an arbitrary vector. Parameters ---------- compound : mb.Compound The compound being rotated. theta : float The angle by which to rotate the compound, in radians. around : np.ndarray, shape=(3,), dtype=float The vector about which to rotate the compound. """ around = np.asarray(around).reshape(3) if np.array_equal(around, np.zeros(3)): raise ValueError('Cannot rotate around a zero vector') atom_positions = compound.xyz_with_ports atom_positions = Rotation(theta, around).apply_to(atom_positions) compound.xyz_with_ports = atom_positions warning_message = 'Please use rotate(compound, theta, around=np.asarray([1, 0, 0]))' @deprecated(warning_message) def rotate_around_x(compound, theta): """Rotate a compound around the x axis. Parameters ---------- compound : mb.Compound The compound being rotated. theta : float The angle by which to rotate the compound. """ rotate(compound, theta, np.asarray([1, 0, 0])) warning_message = 'Please use rotate(compound, theta, around=np.asarray([0, 1, 0]))' @deprecated(warning_message) def rotate_around_y(compound, theta): """Rotate a compound around the y axis. Parameters ---------- compound : mb.Compound The compound being rotated. theta : float The angle by which to rotate the compound. """ rotate(compound, theta, np.asarray([0, 1, 0])) warning_message = 'Please use rotate(compound, theta, around=np.asarray([0, 0, 1]))' @deprecated(warning_message) def rotate_around_z(compound, theta): """Rotate a compound around the z axis. Parameters ---------- compound : mb.Compound The compound being rotated. theta : float The angle by which to rotate the compound. """ rotate(compound, theta, np.asarray([0, 0, 1])) warning_message = 'Please use Compound.spin()' @deprecated(warning_message) def spin(compound, theta, around): """Rotate a compound in place around an arbitrary vector. Parameters ---------- compound : mb.Compound The compound being rotated. theta : float The angle by which to rotate the compound, in radians. around : np.ndarray, shape=(3,), dtype=float The axis about which to spin the compound. """ around = np.asarray(around).reshape(3) if np.array_equal(around, np.zeros(3)): raise ValueError('Cannot spin around a zero vector') center_pos = compound.center translate(compound, -center_pos) rotate(compound, theta, around) translate(compound, center_pos) def _spin(coordinates, theta, around): """Rotate a set of coordinates in place around an arbitrary vector. Parameters ---------- coordinates : np.ndarray, shape=(n,3), dtype=float The coordinates being spun. theta : float The angle by which to spin the coordinates, in radians. around : np.ndarray, shape=(3,), dtype=float The axis about which to spin the coordinates. """ around = np.asarray(around).reshape(3) if np.array_equal(around, np.zeros(3)): raise ValueError('Cannot spin around a zero vector') center_pos = np.mean(coordinates, axis=0) coordinates -= center_pos coordinates = _rotate(coordinates, theta, around) coordinates += center_pos return coordinates warning_message = 'Please use spin(compound, theta, around=np.asarray([1, 0, 0]))' @deprecated(warning_message) def spin_x(compound, theta): """Rotate a compound in place around the x axis. Parameters ---------- compound : mb.Compound The compound being rotated. theta : float The angle by which to rotate the compound. """ spin(compound, theta, np.asarray([1.0, 0.0, 0.0])) warning_message = 'Please use spin(compound, theta, around=np.asarray([0, 1, 0]))' @deprecated(warning_message) def spin_y(compound, theta): """Rotate a compound in place around the y axis. Parameters ---------- compound : mb.Compound The compound being rotated. theta : float The angle by which to rotate the compound. """ spin(compound, theta, np.asarray([0.0, 1.0, 0.0])) warning_message = 'Please use spin(compound, theta, around=np.asarray([0, 0, 1]))' @deprecated(warning_message) def spin_z(compound, theta): """Rotate a compound in place around the z axis. Parameters ---------- compound : mb.Compound The compound being rotated. theta : float The angle by which to rotate the compound. """ spin(compound, theta, np.asarray([0.0, 0.0, 1.0]))
[docs]def x_axis_transform(compound, new_origin=None, point_on_x_axis=None, point_on_xy_plane=None): """Move a compound such that the x-axis lies on specified points. Parameters ---------- compound : mb.Compound The compound to move. new_origin : mb.Compound or list-like of size 3, optional, default=[0.0, 0.0, 0.0] Where to place the new origin of the coordinate system. point_on_x_axis : mb.Compound or list-like of size 3, optional, default=[1.0, 0.0, 0.0] A point on the new x-axis. point_on_xy_plane : mb.Compound, or list-like of size 3, optional, default=[1.0, 0.0, 0.0] A point on the new xy-plane. """ import mbuild as mb if new_origin is None: new_origin = np.array([0, 0, 0]) elif isinstance(new_origin, mb.Compound): new_origin = new_origin.pos elif isinstance(new_origin, (tuple, list,np.ndarray)): new_origin = np.asarray(new_origin) else: raise TypeError('x_axis_transform, y_axis_transform, and z_axis_transform only accept' ' mb.Compounds, list-like of length 3 or None for the new_origin' ' parameter. User passed type: {}.'.format(type(new_origin))) if point_on_x_axis is None: point_on_x_axis = np.array([1.0, 0.0, 0.0]) elif isinstance(point_on_x_axis, mb.Compound): point_on_x_axis = point_on_x_axis.pos elif isinstance(point_on_x_axis, (list, tuple, np.ndarray)): point_on_x_axis = np.asarray(point_on_x_axis) else: raise TypeError('x_axis_transform, y_axis_transform, and z_axis_transform only accept' ' mb.Compounds, list-like of size 3, or None for the point_on_x_axis' ' parameter. User passed type: {}.'.format(type(point_on_x_axis))) if point_on_xy_plane is None: point_on_xy_plane = np.array([1.0, 1.0, 0.0]) elif isinstance(point_on_xy_plane, mb.Compound): point_on_xy_plane = point_on_xy_plane.pos elif isinstance(point_on_xy_plane, (list, tuple, np.ndarray)): point_on_xy_plane = np.asarray(point_on_xy_plane) else: raise TypeError('x_axis_transform, y_axis_transform, and z_axis_transform only accept' ' mb.Compounds, list-like of size 3, or None for the point_on_xy_plane' ' parameter. User passed type: {}.'.format(type(point_on_xy_plane))) atom_positions = compound.xyz_with_ports transform = AxisTransform(new_origin=new_origin, point_on_x_axis=point_on_x_axis, point_on_xy_plane=point_on_xy_plane) atom_positions = transform.apply_to(atom_positions) compound.xyz_with_ports = atom_positions
[docs]def y_axis_transform(compound, new_origin=None, point_on_y_axis=None, point_on_xy_plane=None): """Move a compound such that the y-axis lies on specified points. Parameters ---------- compound : mb.Compound The compound to move. new_origin : mb.Compound or like-like of size 3, optional, default=[0.0, 0.0, 0.0] Where to place the new origin of the coordinate system. point_on_y_axis : mb.Compound or list-like of size 3, optional, default=[0.0, 1.0, 0.0] A point on the new y-axis. point_on_xy_plane : mb.Compound or list-like of size 3, optional, default=[0.0, 1.0, 0.0] A point on the new xy-plane. """ x_axis_transform(compound, new_origin=new_origin, point_on_x_axis=point_on_y_axis, point_on_xy_plane=point_on_xy_plane) rotate_around_z(compound, np.pi / 2)
[docs]def z_axis_transform(compound, new_origin=None, point_on_z_axis=None, point_on_zx_plane=None): """Move a compound such that the z-axis lies on specified points. Parameters ---------- compound : mb.Compound The compound to move. new_origin : mb.Compound or list-like of size 3, optional, default=[0.0, 0.0, 0.0] Where to place the new origin of the coordinate system. point_on_z_axis : mb.Compound or list-like of size 3, optional, default=[0.0, 0.0, 1.0] A point on the new z-axis. point_on_zx_plane : mb.Compound or list-like of size 3, optional, default=[0.0, 0.0, 1.0] A point on the new xz-plane. """ x_axis_transform(compound, new_origin=new_origin, point_on_x_axis=point_on_z_axis, point_on_xy_plane=point_on_zx_plane) rotate_around_y(compound, np.pi * 3 / 2)