#!/usr/bin/python #import timeit, sys import mirge.forgi.threedee.utilities.cytvec as ftuc import mirge.forgi.utilities.debug as fud import numpy as np import math as m import numpy.linalg as nl import numpy.testing as nt import random as rand #import scipy as sp null_array = np.array([0., 0., 0.]) x_array = np.array([1., 0., 0.]) y_array = np.array([0., 1., 0.]) z_array = np.array([0., 0., 1.]) # identity matrix identity_matrix = np.array([x_array, y_array, z_array]) standard_basis = np.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) tau = 2 * m.pi def get_inter_distances(vecs): ''' Calculate all of the distances between the points of vecs. ''' distances = [] for i in range(len(vecs)): for j in range(i+1, len(vecs)): distances += [vec_distance(vecs[i], vecs[j])] return distances def get_random_vector(mult=1.): return np.array([mult * rand.uniform(-1, 1), mult * rand.uniform(-1, 1), mult * rand.uniform(-1, 1)]) def get_orthogonal_unit_vector(vec): ''' Return a vector orthogonal to vec. ''' vec2 = get_non_colinear_unit_vector(vec) vec3 = np.cross(vec, vec2) return normalize(vec3) def get_random_vector_pair(angle=rand.uniform(0, m.pi)): vec1 = get_random_vector() vec2 = get_non_colinear_unit_vector(vec1) rot_vec = np.cross(vec1, vec2) rotmat = rotation_matrix(rot_vec, angle) vec2 = np.dot(rotmat, vec1) return (vec1, vec2) def get_double_alignment_matrix(vp1, vp2): ''' Align two sets of two vectors onto each other. @param vp1: A pair of two vectors. @param vp2: Another pair of two vectors. ''' angle1 = vec_angle(vp1[0], vp1[1]) angle2 = vec_angle(vp2[0], vp2[1]) nt.assert_allclose(angle1, angle2, rtol=1e-7, atol=1e-7) # Align the first two segments mat1 = get_alignment_matrix(vp1[0], vp2[0]) # See where the second segment of the second set ends up # after the first alignment new_vp2_1 = np.dot(mat1, vp2[1]) comp1 = np.cross(vp1[1], vp1[0]) #comp1 = np.cross(vp1[0], vp1[1]) comp2 = np.cross(vp1[0], comp1) # should be along the plane of vp1[0] and vp1[1] basis1 = create_orthonormal_basis(normalize(vp1[0]), normalize(comp2)) rej2 = change_basis(new_vp2_1, basis1, standard_basis) angle = m.atan2(rej2[2], rej2[1]) mat2 = rotation_matrix(vp1[0], angle) #return np.dot(mat1, mat2) return np.dot(mat2, mat1) def get_alignment_matrix(vec1, vec2): ''' Return a rotation matrix that will align vec1 along vec2. @param vec1: The target vector. @param vec2: The vector to be aligned. ''' comp = np.cross(vec1, vec2) angle = vec_angle(vec1, vec2) return rotation_matrix(comp, angle) def get_non_colinear_unit_vector(vec): ''' Get a unit vector that does not lie on the line defined by vec. This is done by creating a vector along the least represented axis in vec. @param vec: The vector under consideration. @return: A vector along an axis. ''' absvec = [abs(v) for v in vec] m = min(absvec) ind = absvec.index(m) unit = [0., 0., 0.] unit[ind] = 1. return np.array(unit) def create_orthonormal_basis1(vec1, vec2=None, vec3=None): ''' Create an orthonormal basis using the provided vectors. If more than one is provided, it must be orthogonal to the others. ''' if vec2 == None: vec2 = get_non_colinear_unit_vector(vec1) #else: # nt.assert_allclose(np.dot(vec2, vec1), 0., rtol=1e-7, atol=1e-7) vec1 = normalize(np.array(vec1)) vec2 = normalize(np.array(vec2)) if vec3 == None: vec3 = np.cross(vec1, vec2) vec3 = normalize(vec3) return np.array([vec1, vec2, vec3]) def create_orthonormal_basis(vec1, vec2=None, vec3=None): ''' Create an orthonormal basis using the provided vectors. If more than one is provided, it must be orthogonal to the others. ''' if vec2 == None: vec2 = get_non_colinear_unit_vector(vec1) vec2 = np.cross(vec1, vec2) #else: # nt.assert_allclose(np.dot(vec2, vec1), 0., rtol=1e-7, atol=1e-7) vec1 /= magnitude(vec1) vec2 /= magnitude(vec2) if vec3 == None: vec3 = np.cross(vec1, vec2) vec3 /= magnitude(vec3) return np.array([vec1, vec2, vec3]) def time_cob1(): vec1 = get_random_vector() vec2 = get_random_vector() basis = create_orthonormal_basis1(vec1, vec2) def time_cob2(): vec1 = get_random_vector() vec2 = get_random_vector() basis = create_orthonormal_basis(vec1, vec2) def time_cob(): t1 = timeit.Timer("time_cob1()", "from forgi.utilities.vector import time_cob1") t2 = timeit.Timer("time_cob2()", "from forgi.utilities.vector import time_cob2") print t1.repeat(number=10000) print t2.repeat(number=10000) def spherical_cartesian_to_polar(vec): ''' Return a parameterization of a vector of 3 coordinates: x = r sin u cos v y = r sin u sin v z = r cos u 0 <= u <= pi -pi <= v <= pi Where u is the polar angle and v is the azimuth angle. @param vec: A vector of 3 cartesian coordinates. @return: (r, u, v) ''' r = magnitude(vec) u = m.acos(vec[2] / r) v = m.atan2(vec[1], vec[0]) nt.assert_allclose(vec[0], r * m.sin(u) * m.cos(v), rtol=1e-7, atol=1e-7) return (r, u, v) def spherical_polar_to_cartesian(vec): ''' Convert spherical polar coordinates to cartesian coordinates: See the definition of spherical_cartesian_to_polar. @param vec: A vector of the 3 polar coordinates (r, u, v) @return: (x, y, z) ''' (r, u, v) = vec x = r * m.sin(u) * m.cos(v) y = r * m.sin(u) * m.sin(v) z = r * m.cos(u) return [x, y, z] def get_standard_basis(dim): ''' Get a standard basis for the given dimension. For 2D, this equals [[1.,0.],[0.,1.]] @param dim: The dimension of the vector space. @return: A vector of vectors that constitute the standard basis. ''' standard_basis = [[0. for j in range(dim)] for i in range(dim)] for i in range(dim): standard_basis[i][i] = 1. standard_basis = np.array(standard_basis) return standard_basis def change_basis(coords, new_basis, old_basis): ''' Change the basis of coordinates to a new basis. In a regular structure we have the coordinates in the regular cartesian coordinate system. For helix-helix orientations, however, we want to express the coordinates in a coordinate system defined by the first helix. The new basis will consist of the axis of the first helix, one of its twist elements (the one closest to the second vector) and a third vector orthogonal to the previous two. # http://tutorial.math.lamar.edu/Classes/LinAlg/ChangeOfBasis.aspx @param coords: The coordinates to transform (array of n elements). @param new_basis: The new basis vectors (n x n matrix) @param old_basis: The old basis for the coordinates(n x n matrix) @return: The new coordinates according to the new basis ''' #assert(len(coords) == len(new_basis)) #assert(len(new_basis) == len(old_basis)) dim = len(coords) #print "coords:", coords standard_coords = np.dot(old_basis.transpose(), coords) ''' #print "standard_coords:", standard_coords standard_to_new = inv(new_basis.transpose()) #print "standard_to_new:", standard_to_new new_coords = np.dot(standard_to_new, standard_coords) print "new_coords:", new_coords ''' new_coords = nl.solve(new_basis.transpose(), standard_coords) #print "new_coords1:", new_coords1 return new_coords def change_basis1(coords, new_basis, old_basis): ''' ''' dim = len(coords) standard_coords = np.dot(old_basis.transpose(), coords) standard_to_new = nl.inv(new_basis.transpose()) new_coords = np.dot(standard_to_new, standard_coords) return new_coords def change_basis2(coords, new_basis, old_basis): ''' ''' dim = len(coords) standard_coords = np.dot(old_basis.T, coords) new_coords = nl.solve(new_basis.T, standard_coords) return new_coords def change_basis1_benchmark(): coords = get_random_vector(10.) basis1 = np.array([get_random_vector(10.) for i in range(3)]) basis2 = np.array([get_random_vector(10.) for i in range(3)]) nc = change_basis1(coords, basis1, basis2) def change_basis2_benchmark(): coords = get_random_vector(10.) basis1 = np.array([get_random_vector(10.) for i in range(3)]) basis2 = np.array([get_random_vector(10.) for i in range(3)]) nc = change_basis2(coords, basis1, basis2) def time_basis1(): t1 = timeit.Timer("change_basis1_benchmark()","from forgi.utilities.vector import change_basis1_benchmark") print t1.repeat(number=10000) def time_basis2(): t2 = timeit.Timer("change_basis2_benchmark()","from forgi.utilities.vector import change_basis2_benchmark") print t2.repeat(number=10000) def vector_rejection(a, b): ''' Return the vector rejection of a from b. In other words, return the orthogonal projection of a onto the plane orthogonal to b. @param a: The vector to be projected. @param b: The vector defining the normal of the plane. @return: The rejection of the vector a from b. (a - (np.dot(a, b) / np.dot(b, b)) * b) ''' n = np.dot(a, b) d = np.dot(b, b) return a - (n / d) * b def rotation_matrix_weave(axis, theta, mat = None): ''' Calculate the rotation matrix for a rotation of theta degrees around axis. Implemented in C++ using the weave module. Runs approximately 6x faster than the numpy version below if no mat is passed in and around 20x faster if mat is passed in. Thanks to unutbu on StackOverflow http://stackoverflow.com/questions/6802577/python-rotation-of-3d-vector @param axis: The axis around which to rotate @param theta: The angle of rotation @return: A matrix which can be used to perform the given rotation. The coordinates need only be multiplied by the matrix. ''' if mat == None: mat = np.eye(3,3) support = "#include <math.h>" code = """ double x = sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]); double a = cos(theta / 2.0); double b = -(axis[0] / x) * sin(theta / 2.0); double c = -(axis[1] / x) * sin(theta / 2.0); double d = -(axis[2] / x) * sin(theta / 2.0); mat[0] = a*a + b*b - c*c - d*d; mat[1] = 2 * (b*c - a*d); mat[2] = 2 * (b*d + a*c); mat[3*1 + 0] = 2*(b*c+a*d); mat[3*1 + 1] = a*a+c*c-b*b-d*d; mat[3*1 + 2] = 2*(c*d-a*b); mat[3*2 + 0] = 2*(b*d-a*c); mat[3*2 + 1] = 2*(c*d+a*b); mat[3*2 + 2] = a*a+d*d-b*b-c*c; """ import scipy as sp sp.weave.inline(code, ['axis', 'theta', 'mat'], support_code = support, libraries = ['m']) return mat def vector_set_rmsd(set1, set2): ''' Calculate the rmsd between two sets of vectors. @param set1: A matrix @param set2: Another matrix. @return: The rmsd between the rows of the matrix. ''' rmsd = 0 count = 0 for i in range(len(set1)): rmsd += magnitude(set2[i] - set1[i]) ** 2 count += 1 rmsd /= count return m.sqrt(rmsd) def rotation_matrix(axis, theta): ''' Calculate the rotation matrix for a rotation of theta degrees around axis. Thanks to unutbu on StackOverflow http://stackoverflow.com/questions/6802577/python-rotation-of-3d-vector @param axis: The axis around which to rotate @param theta: The angle of rotation @return: A matrix which can be used to perform the given rotation. The coordinates need only be multiplied by the matrix. ''' axis = axis/m.sqrt(np.dot(axis, axis)) a = m.cos(theta/2) b, c, d = -axis*m.sin(theta/2) return np.array([[a*a+b*b-c*c-d*d, 2*(b*c-a*d), 2*(b*d+a*c)], [2*(b*c+a*d), a*a+c*c-b*b-d*d, 2*(c*d-a*b)], [2*(b*d-a*c), 2*(c*d+a*b), a*a+d*d-b*b-c*c]]) def get_vector_centroid(crds1): ''' Find the centroid of a set of vectors. @param crds: A matrix containing all of the vectors. @return: The centroid of the rows of the matrix crds. ''' centroid1 = np.array([0., 0., 0.]) for i in range(len(crds1)): centroid1 += crds1[i] centroid1 /= float(len(crds1)) for i in centroid1: if m.isnan(i): raise Exception('nan encountered') return centroid1 def center_on_centroid(crds1): centroid1 = get_vector_centroid(crds1) crds = [] for i in range(len(crds1)): crds += [crds1[i] - centroid1] return np.array(crds) def magnitude(vec): ''' Return the magnitude of a vector (|V|). @param vec: The vector in question. @return: The magnitude of the vector. ''' return ftuc.magnitude(vec) #return m.sqrt(np.dot(vec, vec)) def time_mag1(): vec1 = get_random_vector() return m.sqrt(np.dot(vec1, vec1)) def time_mag2(): vec1 = get_random_vector() return m.sqrt(np.dot(vec1, vec1)) def time_mag(): t1 = timeit.Timer("time_mag1()", "from forgi.utilities.vector import time_mag1") t2 = timeit.Timer("time_mag2()", "from forgi.utilities.vector import time_mag2") print t1.repeat(number=10000) print t2.repeat(number=10000) def normalize(vec): ''' Normalize a vector so that its magnitude becomes 1.0 while its direction remains the same. @param vec: The vector in question. @return: A normalized version of the vector. ''' return vec / magnitude(vec) def vec_angle(vec1, vec2): ''' Get the angle between two vectors using the identity: A * B = |A||B| cos t Where A and B are two vectors and t is the angle between them. @param vec1: The first vector (A) @param vec2: The second vector (B) @return: The angle between the two vectors. ''' vec1n = normalize(vec1) vec2n = normalize(vec2) d = np.dot(vec1n, vec2n) # this shouldn't happen, but sometimes it does, presumably because # of rounding errors if d >= 1.: d = 1. if d <= -1.: d = -1. angle = m.acos(d) return angle def vec_dot(a, b): return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] ''' def cross(a, b): c = [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]] return c ''' def vec_distance(vec1, vec2): return ftuc.vec_distance(vec1, vec2) #return m.sqrt(np.dot(vec2 - vec1, vec2 - vec1)) def line_segment_distance(s1_p0, s1_p1, s2_p0, s2_p1): ''' Calculate the two points on each of the segments that are closest to each other. The first segment is defined as p1->p2 and the second as p3->p4. Code shamelessly translated from: http://softsurfer.com/Archive/algorithm_0106/algorithm_0106.htm#dist3D_Segment_to_Segment @param s1_p0: The start of the first segment @param s1_p1: The end of the first segment @param s2_p0: The start of the second segment @param s2_p1: The end of the second segment @return: A tuple of points (i1,i2) containing the point i1 on s1 closest to the point i2 on segment s2 ''' u = s1_p1 - s1_p0 v = s2_p1 - s2_p0 w = s1_p0 - s2_p0 a = np.dot(u,u) # always >= 0 b = np.dot(u,v) c = np.dot(v,v) # always >= 0 d = np.dot(u,w) e = np.dot(v,w) D = a*c - b*b # always >= 0 sD = D # sc = sN / sD, default sD = D >= 0 tD = D # tc = tN / tD, default tD = D >= 0 SMALL_NUM = 0.000001 # compute the line parameters of the two closest points if (D < SMALL_NUM): # the lines are almost parallel sN = 0.0 # force using point P0 on segment S1 sD = 1.0 # to prevent possible division by 0.0 later tN = e tD = c else: # get the closest points on the infinite lines sN = (b*e - c*d) tN = (a*e - b*d) if (sN < 0.0): # sc < 0 => the s=0 edge is visible sN = 0.0 tN = e tD = c elif (sN > sD): # sc > 1 => the s=1 edge is visible sN = sD tN = e + b tD = c if (tN < 0.0): # tc < 0 => the t=0 edge is visible tN = 0.0 # recompute sc for this edge if (-d < 0.0): sN = 0.0 elif (-d > a): sN = sD else: sN = -d sD = a elif (tN > tD): # tc > 1 => the t=1 edge is visible tN = tD # recompute sc for this edge if ((-d + b) < 0.0): sN = 0 elif ((-d + b) > a): sN = sD else: sN = (-d + b) sD = a # finally do the division to get sc and tc sc = 0.0 if abs(sN) < SMALL_NUM else sN / sD tc = 0.0 if abs(tN) < SMALL_NUM else tN / tD # get the difference of the two closest points dP = w + (sc * u) - (tc * v) # = S1(sc) - S2(tc) return (s1_p0 + sc * u, s2_p0 + tc * v) def closest_point_on_seg(seg_a, seg_b, circ_pos): ''' Closest point between a line segment and a point. Lifted from: http://doswa.com/2009/07/13/circle-segment-intersectioncollision.html ''' seg_v = seg_b - seg_a pt_v = circ_pos - seg_a mag = m.sqrt(sum(seg_v * seg_v)) if m <= 0: raise ValueError, "Invalid segment length" seg_v_unit = seg_v / mag proj = pt_v.dot(seg_v_unit) if proj <= 0: return seg_a.copy() if proj >= mag: return seg_b.copy() proj_v = seg_v_unit * proj closest = proj_v + seg_a return closest def segment_circle(seg_a, seg_b, circ_pos, circ_rad): ''' Something. Lifted from: http://doswa.com/2009/07/13/circle-segment-intersectioncollision.html ''' closest = closest_point_on_seg(seg_a, seg_b, circ_pos) dist_v = circ_pos - closest mag = m.sqrt(sum(dist_v * dist_v)) if m > circ_rad: return vec(0, 0) if mag(dist_v) <= 0: raise ValueError, "Circle's center is exactly on segment" offset = dist_v / mag(dist_v) * (circ_rad - mag(dist_v)) return offset def cylinder_line_intersection(cyl, line, r): ''' Get the points of intersection between a line and a cylinder. If they do not intersect, return an empty list. If the line touches the cylinder, then return a 2 point list with two identical points. If the line crosses the cylinder, then return a list of 2 points. ''' cyl = np.array(cyl) line = np.array(line) cyl_vec = cyl[1] - cyl[0] line_vec = line[1] - line[0] cyl_basis = create_orthonormal_basis(cyl_vec) line_t = change_basis(line.T, cyl_basis, standard_basis).T cyl_t = change_basis(cyl.T, cyl_basis, standard_basis).T cyl_vec_t = cyl_t[1] - cyl_t[0] line_vec_t = line_t[1] - line_t[0] line_vec_t_normed = normalize(line_vec_t) # get the point on the line closest to the # center of the cylinder p = closest_point_on_seg(line_t[0][1:], line_t[1][1:], np.array(cyl_t[0][1:])) if line_vec_t[1] == 0: return [] # Figure out the x position by determining how far along # the y-coordinate of the segment the closest point is x = (line_t[0][0] + (line_vec_t[0] * (p[0] - line_t[0][1]) / line_vec_t[1])) v = p - cyl_t[0][1:] o = m.sqrt(sum(v * v)) p = [x, p[0], p[1]] if o > r: # no intersection return [] t = m.sqrt(r ** 2 - o ** 2) i1 = p - t * line_vec_t_normed i2 = p + t * line_vec_t_normed endpoints_t = [i1, i2] endpoints_t = sorted(endpoints_t, key=lambda x: x[0]) line_t = sorted(line_t, key=lambda x: x[0]) cyl_t = sorted(cyl_t, key=lambda x: x[0]) # the start point will be the highest of the top of the cylinder # the end of the line, and the point where the line intersects # the surface of the cylinder start_points = np.array([cyl_t[0][0], line_t[0][0], endpoints_t[0][0]]) # the end point will be the lowest of the... " " " end_points = np.array([cyl_t[1][0], line_t[1][0], endpoints_t[1][0]]) start_points = sorted(start_points) end_points = sorted(end_points) start_param = (start_points[-1] - p[0]) / line_vec_t_normed[0] end_param = (end_points[0] - p[0]) / line_vec_t_normed[0] endpoints_t[0] = p + start_param * line_vec_t_normed endpoints_t[1] = p + end_param * line_vec_t_normed ''' endpoints_t[0] = [start_points[-1], endpoints_t[ endpoints_t[1][0] = end_points[0] real_start = start_points[-1] real_end = end_points[0] ''' intersects_t = np.array(endpoints_t) if intersects_t[0][0] > intersects_t[1][0]: # degenerate case, the line is to the side of the cylinder return [] return change_basis(intersects_t.T, standard_basis, cyl_basis).T def pin_fits_two_cyl(cyl1, cyl2, cyl_width): ''' If we create a cone that starts at one end of cylinder1, does it enclose cylinder2? This function approximates an answer by projecting a circle on the plane normal to the axis of cylinder1. @param cyl1: The coordinates of cylinder1 @param cyl2: The coordinates of cylinder2 @param cyl_width: The widths of the two cylinders @return: True or False ''' basis = create_orthonormal_basis(cyl1[1] - cyl1[0]) cyl2 = np.array([cyl2[0] - cyl1[1], cyl2[1] - cyl1[1]]) cyl2_t = change_basis(cyl2.T, basis, standard_basis).T cone_width = cyl_width cyl1_len = magnitude(cyl1[1] - cyl1[0]) # the cone expands cone_width_cyl2_start = cyl_width * (cyl1_len + cyl2_t[0][0]) / cyl1_len cone_width_cyl2_end = cyl_width * (cyl1_len + cyl2_t[1][0]) / cyl1_len cyl2_start_offset = m.sqrt(cyl2_t[0][1] ** 2 + cyl2_t[0][2] ** 2) cyl2_end_offset = m.sqrt(cyl2_t[1][1] ** 2 + cyl2_t[1][2] ** 2) if cyl2_start_offset > cone_width_cyl2_start: return False if cyl2_end_offset > cone_width_cyl2_end: return False return True def point_in_cylinder(pt1, pt2, r, testpt): ''' Determine if testpt is within a cylinder of radius r. Translated from: http://www.flipcode.com/archives/Fast_Point-In-Cylinder_Test.shtml @param pt1: The start of the cylinder axis. @param pt2: The end of the cylinder axis. @param r: The radius of the cylinder. @param testpt: The point we are testing. @return: True if the point is within the cylinder, False otherwise. ''' d = pt2 - pt1 lengthsq = np.dot(d, d) radius_sq = r * r pd = testpt - pt1 dot = np.dot(pd, d) if dot < 0. or dot > lengthsq: # beyond the edges of the cylinder return False else: dsq = np.dot(pd, pd) - (dot * dot) / lengthsq if (dsq > radius_sq): return False else: return True