{"action":"created","comment":{"url":"https://api.github.com/repos/uf-mil/Sub8/pulls/comments/44207216","id":44207216,"diff_hunk":"@@ -0,0 +1,557 @@\n+\"\"\"Functions for manipulating SO3 quantities (rigid body rotations).\"\"\"\n+\n+# --- SUMMARY\n+# This library is effectively an extension of Christoph Gohlke's\n+# transformations.py. It contains extra functions specifically\n+# for handling SO3 quantities.\n+# Version 1.1\n+\n+# --- HIGHLIGHTS\n+# The functions are generally representation independent; you can pass\n+# in a quaternion or rotation matrix or whatever valid SO3 quantity you\n+# want and high level operations are carried out correctly regardless.\n+\n+# SO3 quantity typing is not object-oriented. This means you can keep your\n+# quantities as simple arrays; you don't have to make all sorts of wacky\n+# class function calls to work on a state with SO3 quantities. For example,\n+# you could fit your favorite quaternion q into a state vector as easy as\n+# [blah,q] and all numpy array operations will still be usable.\n+\n+# Conversions to and from axles (Euler vectors) are available. An axle\n+# is the product of the minimum angle of rotation (radians, 0 to pi)\n+# and axis of rotation (unit 3-vector). The set of all axles is the\n+# tangent space of the SO3 manifold; it is a lie algebra. When expressed\n+# as a skew-symmetric matrix, the matrix exponential of the axle is equal\n+# to its associated rotation matrix. Note: it is a mathematical truth\n+# that \"axle\" is a cooler name than \"Euler vector\" because it cleverly\n+# combines the words \"axis\" and \"angle.\"\n+\n+# \"Addition and subtraction\" for SO3 quantities is also available.\n+# For example, minus(a, b) will return an SO3 quantity representing\n+# the minimal rotation from orientation b to orientation a. These\n+# are simply abstractions of composition and inverse composition.\n+\n+# --- USAGE\n+# Inputted array quantities should be numpy ndarrays, but may also be lists or tuples.\n+# Outputted array quantities will be numpy ndarrays.\n+# Inputted angles must be in radians, any value.\n+# Outputted angles will be in radians, between 0 and pi.\n+# Quaternions are ordered as [w,xi,yj,zk] where w is the real part.\n+\n+# --- REFERENCE\n+# https://github.com/uf-mil/software-common/blob/master/uf_common/src/uf_common/orientation_helpers.py\n+# https://github.com/krishauser/RobotSim/blob/master/Python/robot/py\n+# https://en.wikipedia.org/wiki/Axis%E2%80%93angle_representation\n+# http://arxiv.org/pdf/1107.1119.pdf\n+\n+# --- AUTHOR\n+# Jason Nezvadovitz\n+\n+################################################# IMPORTS\n+\n+# standard\n+from __future__ import division\n+# 3rd party\n+import numpy as np\n+import numpy.linalg as npl\n+import transformations as trns\n+\n+################################################# MAIN\n+\n+def rep(x):\n+    \"\"\"Returns a string naming the SO3 representation of the given\n+    array x by examining its shape and testing for orthonormality.\n+    If the quantity is not an axle, unit quaternion, rotation matrix,\n+    or homogeneous transformation matrix, the string 'notSO3' is returned.\n+\n+    Output formats: 'axle', 'quaternion', 'rotmat', 'tfmat', or 'notSO3'.\n+\n+    Note that this function is also useful for checking (as one example) if\n+    a quaternion is remaining unit-norm during operations that accrue error.\n+\n+    >>> M = trns.random_rotation_matrix()\n+    >>> print(rep(M))\n+    tfmat\n+    >>> print(rep(M[:3,:3]))\n+    rotmat\n+    >>> M[-1,-1] = 2  # breaks homogeneity\n+    >>> print(rep(M))\n+    notSO3\n+    >>> print(rep([5,1,0]))  # arbitrary vector\n+    notSO3\n+\n+    \"\"\"\n+    x = np.array(x)\n+    shape = x.shape\n+    if shape == (3, ):\n+        if (npl.norm(x) >= 0) and (npl.norm(x) <= np.pi):\n+            return 'axle'  # norm of a proper axle is always between 0 and pi (inclusive)\n+    elif shape == (4, ):\n+        if np.isclose(npl.norm(x), 1):\n+            return 'quaternion'  # here, quaternion refers specifically to unit quaternions (versors)\n+    elif shape == (3, 3):\n+        if np.isclose(npl.det(x), 1) and np.allclose(x.dot(x.T), np.eye(3)):\n+            return 'rotmat'  # a proper orthonormal matrix must have a determinant of +1 and an inverse equal to its transpose\n+    elif shape == (4, 4):\n+        if np.isclose(npl.det(x[:3, :3]), 1) and np.allclose(x[:3, :3].dot(x[:3, :3].T), np.eye(3)) and np.allclose(x[3, :], [0, 0, 0, 1]):\n+            return 'tfmat'  # both orthonormality and homogeneity (bottom row of [0,0,0,1]) are required\n+    return 'notSO3'\n+\n+\n+def apply(x, P, t=None):\n+    \"\"\"Applies the rotation given by the SO3 quantity x to a set of\n+    points P, and returns the newly transformed set of points. The points\n+    must be in 3-space and fitted into P as columns:\n+\n+        [x1 x2 x3 ...]\n+    P = [y1 y2 y3 ...]\n+        [z1 z2 z3 ...]\n+\n+    If t is set to a 3 element vector, then a translation by that specific vector\n+    will also be applied. Even if you pass in an affine transform (tfmat), only\n+    the rotation part will be carried out. However, if you intend to pass in\n+    a tfmat, do  apply(x, P, 'tfmat')  instead of  apply(x, P, x[:3,3])  and\n+    the extraction of x[:3,3] will be inherent.\n+\n+    >>> P = [[1,0,1],\n+    ...      [0,1,1],\n+    ...      [0,0,1]]\n+    >>> r = [0,0,np.pi/2]  # rotation of 90 deg about +z axis\n+    >>> P2 = apply(r,P)\n+    >>> print(np.round(P2,2))\n+    [[ 0. -1. -1.]\n+     [ 1.  0.  1.]\n+     [ 0.  0.  1.]]\n+    >>> P3 = apply(r,P,[1,2,3])\n+    >>> print(np.round(P3,2))\n+    [[ 1.  0.  0.]\n+     [ 3.  2.  3.]\n+     [ 3.  3.  4.]]\n+    >>> M = affine(matrix_from_axle(r), [1,2,3])\n+    >>> P4 = apply(M,P)\n+    >>> P5 = apply(M,P,'tfmat')\n+    >>> print(np.allclose(P2,P4))\n+    True\n+    >>> print(np.allclose(P3,P5))\n+    True\n+    >>> A = np.array([[-1 , -0.7 ,  1],\n+    ...               [ 4 ,   2  ,  4],\n+    ...               [ 5 ,  0.3 , -8]])  # some constant matrix in body frame\n+    >>> q = trns.random_quaternion()  # quaternion representing body_to_world conversion\n+    >>> A_world_ex1 = trns.quaternion_matrix(q)[:3,:3].dot(A).dot(trns.quaternion_matrix(q)[:3,:3].T)\n+    >>> A_world_ex2 = apply(q, apply(q, A.T).T)  # note that apply(q,A.T).T == A*inv(q)\n+    >>> print(np.allclose(A_world_ex1, A_world_ex2))\n+    True\n+\n+    \"\"\"\n+    # Check shape of P:\n+    P = np.array(P)\n+    shape = P.shape\n+    if shape[0] == 3:\n+        # Check representation of x:\n+        xrep = rep(x)\n+        # Get rotation matrix if not rotmat:\n+        if xrep == 'axle':\n+            x = matrix_from_axle(x)\n+        elif xrep == 'quaternion':\n+            x = trns.quaternion_matrix(x)[:3, :3]\n+        elif xrep == 'tfmat':\n+            if t == 'tfmat':\n+                t = x[:3, 3]\n+            x = x[:3, :3]\n+        # Apply rotation matrix to each point:\n+        newP = x.dot(P)\n+        # Apply translation as desired:\n+        if t is not None:\n+            # Allow the user to have used a 1D array for a single point:\n+            if shape == (3, ):\n+                newP = newP + t\n+            else:\n+                newP = newP + np.array([t]).T\n+        # Finally:\n+        return newP\n+    else:\n+        raise ValueError(\"P must be 3 by n.\")\n+\n+\n+def plus(x1, x2):\n+    \"\"\"Returns the SO3 quantity that represents first rotating\n+    by x1 and then by x2 (i.e. the composition of x2 on x1, call\n+    it \"x1+x2\"). The output will be in the same representation as the\n+    inputs, but the inputs must be in the same representation as each other.\n+    Note that in the case of using tfmats, the resulting tfmat's translation\n+    will, as expected, be the sum of the two constituent translations.\n+\n+    >>> R1 = trns.random_rotation_matrix()[:3,:3]\n+    >>> R2 = trns.random_rotation_matrix()[:3,:3]\n+    >>> R3 = R2.dot(R1)  # rotating vector v by R3 is R2*R1*v\n+    >>> R = plus(R1,R2)\n+    >>> print(np.allclose(R,R3))\n+    True\n+    >>> M1 = affine(R1, [4,4,4])\n+    >>> M2 = affine(R2, [5,5,5])\n+    >>> M3 = affine(R2.dot(R1), [9,9,9])\n+    >>> M = plus(M1,M2)\n+    >>> print(np.allclose(M,M3))\n+    True\n+    >>> print(np.allclose(M[:3,:3],R))\n+    True\n+    >>> q1 = trns.quaternion_from_matrix(R1)\n+    >>> q2 = trns.quaternion_from_matrix(R2)\n+    >>> q3 = trns.quaternion_multiply(q2,q1)\n+    >>> q = plus(q1,q2)  # rotating vector v by q3 is imag(q2*q1*qv)\n+    >>> print(np.allclose(q,q3))\n+    True\n+    >>> print(np.allclose(trns.quaternion_matrix(q)[:3,:3],R))\n+    True\n+    >>> R1plusR2plusR3 = reduce(plus,[R1,R2,R3])  # using plus in succession\n+\n+    \"\"\"\n+    xrep = rep(x1)\n+    if xrep == 'quaternion':\n+        return trns.unit_vector(trns.quaternion_multiply(x2, x1))\n+    elif xrep == 'rotmat':\n+        return x2.dot(x1)\n+    elif xrep == 'tfmat':\n+        return affine(x2[:3, :3].dot(x1[:3, :3]), x1[:3, 3] + x2[:3, 3])\n+    elif xrep == 'axle':\n+        # Adding axles is only valid for small perturbations...\n+        # Perform operation on actual SO3 manifold instead:\n+        x1 = matrix_from_axle(x1)\n+        x2 = matrix_from_axle(x2)\n+        return get_axle(x2.dot(x1))\n+    else:\n+        raise ValueError('One or both values to \"add\" are not SO3.')\n+\n+\n+def minus(x1, x2):\n+    \"\"\"Returns the SO3 quantity representing the minimal rotation from\n+    orientation x1 to orientation x2 (i.e. the inverse composition of x2 on x1,\n+    call it \"x2-x1\"). The output will be in the same representation as the\n+    inputs, but the inputs must be in the same representation as each other.\n+    Note that in the case of using tfmats, the resulting tfmat's translation\n+    will, as expected, be the translation of x2 minus the translation of x1.\n+    That is, the translation from the x1 origin to the x2 origin.\n+\n+    >>> A = trns.rotation_matrix(.2,[0,1,0])  # rotation of 0.2 rad about +y axis\n+    >>> B = trns.rotation_matrix(-.2,[0,1,0])  # rotation of -0.2 rad about +y axis\n+    >>> AtoB = minus(A,B)  # this is \"B - A\", the rotation from A to B\n+    >>> angle, axis = angle_axis(AtoB)  # should be a rotation of (-0.2)-(0.2) = -0.4 rad about +y axis\n+    >>> print(angle, axis)\n+    (0.39999999999999997, array([-0., -1., -0.]))\n+    >>> # The above is 0.4 rad about -y axis, which is equivalent to the expected -0.4 rad about the +y axis.\n+    >>> # As usual, the angle is always returned between 0 and pi, with the accordingly correct axis.\n+    >>> # The rotations need not be about the same axis of course:\n+    >>> NtoA = trns.random_quaternion()  # some rotation from frame N to frame A\n+    >>> NtoB = trns.random_quaternion()  # some rotation from frame N to frame B\n+    >>> AtoB = minus(NtoA, NtoB)  # We say \"AtoB = NtoB - NtoA\"\n+    >>> NtoAtoB = plus(NtoA, AtoB)  # NtoAtoB == NtoB and we say \"NtoAtoB = NtoA + AtoB\"\n+    >>> print(np.allclose(NtoAtoB, NtoB))\n+    True\n+    >>> # Evidently, plus and minus are inverse operations.\n+    >>> # \"x1 + (x2 - x1) = x2\"  reads as \"(N to x1) plus (x1 to x2) = (N to x2)\"\n+\n+    \"\"\"\n+    xrep = rep(x1)\n+    if xrep == 'quaternion':\n+        return trns.quaternion_multiply(x2, trns.quaternion_inverse(x1))\n+    elif xrep == 'rotmat':\n+        return x2.dot(x1.T)\n+    elif xrep == 'tfmat':\n+        return affine(x2[:3, :3].dot(x1.T[:3, :3]), x2[:3, 3] - x1[:3, 3])\n+    elif xrep == 'axle':\n+        # Subtracting axles is only valid for small perturbations...\n+        # Perform operation on actual SO3 manifold instead:\n+        x1 = matrix_from_axle(x1)\n+        x2 = matrix_from_axle(x2)\n+        return get_axle(x2.dot(x1.T))\n+    else:\n+        raise ValueError('One or both values to \"subtract\" are not SO3.')\n+\n+\n+def error(current,target):\n+    \"\"\"Returns a vector representing the difference in orientation between target\n+    and current. Picture the SO3 manifold as some surface, and now put two points\n+    on it; one called target and one called current. Draw a curve along the manifold\n+    from current to target. That curve is so3.minus(current,target). At the curve's\n+    midpoint, draw the tangent vector to the curve. Make that tangent vector's\n+    magnitude equal to the arclength of the curve. This vector is the axle. If current\n+    and target are very close to each other, the axle becomes equivalent to the curve.\n+    However, even for long distances, the axle is still a good vector representation\n+    of error from current to target. It tells you instantaneously which way to rotate\n+    to get from current to target, and tells you approximately by how much. As current\n+    gets closer to target, these approximations become exact, hence why axles work.\n+\n+    >>> current = trns.random_quaternion()\n+    >>> axle_current = get_axle(current)\n+    >>>\n+    >>> current_to_target_small = trns.quaternion_about_axis(0.001, [1,-2,3])  # a small rotation of 0.001 rad about some axis\n+    >>> current_to_target_large = trns.quaternion_about_axis(3, [1,-2,3])  # a large rotation of 3 rad about the same axis\n+    >>>\n+    >>> target_near = plus(current, current_to_target_small)  # current + small change = target_near\n+    >>> axle_target_near = get_axle(target_near)  # axle form of target_near\n+    >>>\n+    >>> target_far = plus(current, current_to_target_large)  # current + large change = target_far\n+    >>> axle_target_far = get_axle(target_far)  # axle form of target_far\n+    >>>\n+    >>> err_near = error(current, target_near)  # axle form of current_to_target_small\n+    >>> err_far = error(current, target_far)  # axle form of current_to_target_large\n+    >>>\n+    >>> # In the limit, the error becomes exactly the difference between the two axles:\n+    >>> print(np.allclose(err_near, axle_target_near - axle_current, atol=1e-02))\n+    True\n+    >>> print(np.allclose(err_far, axle_target_far - axle_current, atol=1e-02))\n+    False\n+\n+    \"\"\"\n+    return get_axle(minus(current, target))\n+\n+\n+def slerp(x1, x2, fraction):\n+    \"\"\"Spherical Linear intERPolation. Returns an SO3 quantity representing an orientation\n+    between x1 and x2. The fraction of the path from x1 to x2 is the fraction input, and it\n+    must be between 0 and 1. The output will be in the same representation as the inputs, but\n+    the inputs must be in the same representation as each other. In the case of using tfmats, \n+    the resulting tfmat's translation will point to a location along the line from the x1\n+    origin to the x2 origin, with that point of course dictated by fraction.\n+\n+    >>> x1 = affine(trns.random_rotation_matrix()[:3,:3], [1,1,1])\n+    >>> x2 = affine(trns.random_rotation_matrix()[:3,:3], [2,2,2])\n+    >>> nogo = slerp(x1, x2, 0)\n+    >>> print(np.allclose(nogo, x1))\n+    True\n+    >>> allgo = slerp(x1, x2, 1)\n+    >>> print(np.allclose(allgo, x2))\n+    True\n+    >>> fourth_between = slerp(x1, x2, 0.25)  # orientation 1/4th the way from orientation x1 to orientation x2\n+    >>> threefourth_between = slerp(x1, x2, 0.75)  # orientation 3/4th the way from orientation x1 to orientation x2\n+    >>> fourth_rot = minus(threefourth_between, x2)  # a fourth the rotation from x1 to x2\n+    >>> print(np.allclose(fourth_between, plus(x1, fourth_rot)))  # x1 + (1-0.75)*(x2-x1)\n+    True\n+    >>> print(fourth_between[:3,3])\n+    [ 1.25  1.25  1.25]\n+\n+    \"\"\"\n+    xrep = rep(x1)\n+    if xrep == 'quaternion':\n+        r12 = get_axle(trns.quaternion_multiply(x2, trns.quaternion_inverse(x1)))\n+        x12 = quaternion_from_axle(fraction * r12)\n+    elif xrep == 'rotmat':\n+        r12 = get_axle(x2.dot(x1.T))\n+        x12 = matrix_from_axle(fraction * r12)\n+    elif xrep == 'tfmat':\n+        r12 = get_axle(x2[:3, :3].dot(x1.T[:3, :3]))\n+        x12 = affine(matrix_from_axle(fraction * r12), fraction * (x2[:3,3] - x1[:3,3]))\n+    elif xrep == 'axle':\n+        R1 = matrix_from_axle(x1)\n+        R2 = matrix_from_axle(x2)\n+        x12 = fraction * get_axle(R2.dot(R1.T))\n+    else:\n+        raise ValueError('One or both values to slerp between are not SO3.')\n+    return plus(x1, x12)\n+\n+\n+def get_axle(x):\n+    \"\"\"Returns the axle equivalent to the given SO3 quantity x.\n+\n+    >>> q = trns.random_quaternion()\n+    >>> r = get_axle(q)\n+    >>> M = trns.rotation_matrix(npl.norm(r), r/npl.norm(r))\n+    >>> print(trns.is_same_transform(M, trns.quaternion_matrix(q)))\n+    True\n+\n+    \"\"\"\n+    # sorry Mario, but the function you are looking for is in another castle\n+    angle, axis = angle_axis(x)\n+    return angle * axis\n+\n+\n+def angle_axis(x):\n+    \"\"\"Returns the extracted angle and axis from a valid SO3 quantity x.\n+    This is equivalent to separating the axle form of x into its\n+    magnitude and direction. The angle will always be between 0 and pi,\n+    inclusive, and the axis will always be a unit vector.\n+\n+    >>> yourAngle, yourAxis = -1337, [-2,0,7]\n+    >>> yourMat = trns.rotation_matrix(yourAngle, yourAxis)\n+    >>> myAngle, myAxis = angle_axis(yourMat)\n+    >>> print(np.isclose(myAngle, np.mod(yourAngle, 2*np.pi)))\n+    True\n+    >>> print(np.allclose(myAxis, trns.unit_vector(yourAxis)))\n+    True\n+    >>> myAngle2, myAxis2 = angle_axis(trns.quaternion_from_matrix(yourMat))\n+    >>> print(np.allclose(myAngle, myAngle2), np.allclose(myAxis, myAxis2))\n+    (True, True)\n+\n+    \"\"\"\n+    xrep = rep(x)\n+    # In the matrix case, transformations.py already has a nice implementation:\n+    if xrep in ['rotmat', 'tfmat']:\n+        if xrep == 'rotmat':\n+            x = affine(x)\n+        angle, axis, point = trns.rotation_from_matrix(x)\n+        # But to be consistent with axles, we only use positive angles:\n+        if angle < 0:\n+            angle, axis = -angle, -axis\n+        # And we map the identity rotation to an axis of [0,0,0]:\n+        elif np.isclose(angle, 0):\n+            axis = np.array([0, 0, 0])\n+    # In the quaternion case, we carry out the following routine:\n+    elif xrep == 'quaternion':\n+        # Renormalize for accuracy:\n+        x = trns.unit_vector(x)\n+        # If \"amount of rotation\" is negative, flip quaternion:\n+        if x[0] < 0:\n+            x = -x\n+        # Extract axis:\n+        imMax = max(map(abs, x[1:]))\n+        if not np.isclose(imMax, 0):\n+            axis = trns.unit_vector(x[1:] / (imMax * npl.norm(x[1:])))\n+        else:\n+            axis = np.array([0, 0, 0])\n+        # Extract angle:\n+        angle = 2 * np.arccos(x[0])\n+    # In the axle case, the routine is trivial:\n+    elif xrep == 'axle':\n+        angle = npl.norm(x)\n+        if not np.isclose(angle, 0):\n+            axis = x / angle\n+        else:\n+            axis = np.array([0, 0, 0])\n+    # If not SO3:\n+    else:\n+        raise ValueError('Quantity to extract angle and axis from must be on SO3')\n+    # Finally:\n+    return (angle, axis)\n+\n+\n+def quaternion_from_axle(r):\n+    \"\"\"Returns the quaternion equivalent to the given axle r.\n+\n+    >>> myAxle = np.pi * np.array([0,1,0])\n+    >>> q = quaternion_from_axle(myAxle)\n+    >>> M1 = trns.quaternion_matrix(q)\n+    >>> M2 = trns.rotation_matrix(np.pi, [0,1,0])\n+    >>> print(trns.is_same_transform(M1, M2))\n+    True\n+\n+    \"\"\"\n+    angle = np.mod(npl.norm(r), 2 * np.pi)\n+    if not np.isclose(angle, 0):\n+        return trns.quaternion_about_axis(angle, r / angle)\n+    else:\n+        return np.array([1, 0, 0, 0])  # unit real number is identity quaternion\n+\n+\n+def matrix_from_axle(r):\n+    \"\"\"Returns the rotation matrix equivalent to the given axle r.\n+\n+    >>> angle, axis = 45*(np.pi/180), trns.unit_vector([0,0,1])\n+    >>> myAxle = angle*axis\n+    >>> myMat = matrix_from_axle(myAxle)\n+    >>> print(np.round(myMat, decimals=3))\n+    [[ 0.707 -0.707  0.   ]\n+     [ 0.707  0.707  0.   ]\n+     [ 0.     0.     1.   ]]\n+\n+    \"\"\"\n+    angle = np.mod(npl.norm(r), 2 * np.pi)\n+    if not np.isclose(angle, 0):\n+        axlemat = crossmat(r / angle)\n+        return np.eye(3) + (np.sin(angle) * axlemat) + ((1 - np.cos(angle)) * axlemat.dot(axlemat))\n+    else:\n+        return np.eye(3)\n+\n+\n+def affine(x, t=None):","path":"utils/sub8_ros_tools/so3tools/so3tools.py","position":466,"original_position":466,"commit_id":"ff68e40b6345f95b55e4388b9981bb591c538386","original_commit_id":"ff68e40b6345f95b55e4388b9981bb591c538386","user":{"login":"jpanikulam","id":6531468,"avatar_url":"https://avatars.githubusercontent.com/u/6531468?v=3","gravatar_id":"","url":"https://api.github.com/users/jpanikulam","html_url":"https://github.com/jpanikulam","followers_url":"https://api.github.com/users/jpanikulam/followers","following_url":"https://api.github.com/users/jpanikulam/following{/other_user}","gists_url":"https://api.github.com/users/jpanikulam/gists{/gist_id}","starred_url":"https://api.github.com/users/jpanikulam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpanikulam/subscriptions","organizations_url":"https://api.github.com/users/jpanikulam/orgs","repos_url":"https://api.github.com/users/jpanikulam/repos","events_url":"https://api.github.com/users/jpanikulam/events{/privacy}","received_events_url":"https://api.github.com/users/jpanikulam/received_events","type":"User","site_admin":false},"body":"This should use a more descriptive name\r\n\r\nSuggestion: make_homogeneous\r\n","created_at":"2015-11-07T03:27:15Z","updated_at":"2015-11-07T03:27:15Z","html_url":"https://github.com/uf-mil/Sub8/pull/31#discussion_r44207216","pull_request_url":"https://api.github.com/repos/uf-mil/Sub8/pulls/31","_links":{"self":{"href":"https://api.github.com/repos/uf-mil/Sub8/pulls/comments/44207216"},"html":{"href":"https://github.com/uf-mil/Sub8/pull/31#discussion_r44207216"},"pull_request":{"href":"https://api.github.com/repos/uf-mil/Sub8/pulls/31"}}},"pull_request":{"url":"https://api.github.com/repos/uf-mil/Sub8/pulls/31","id":49936265,"html_url":"https://github.com/uf-mil/Sub8/pull/31","diff_url":"https://github.com/uf-mil/Sub8/pull/31.diff","patch_url":"https://github.com/uf-mil/Sub8/pull/31.patch","issue_url":"https://api.github.com/repos/uf-mil/Sub8/issues/31","number":31,"state":"open","locked":false,"title":"SO3TOOLS: Introducing so3tools, PEP8 Conformed","user":{"login":"jnez71","id":15154558,"avatar_url":"https://avatars.githubusercontent.com/u/15154558?v=3","gravatar_id":"","url":"https://api.github.com/users/jnez71","html_url":"https://github.com/jnez71","followers_url":"https://api.github.com/users/jnez71/followers","following_url":"https://api.github.com/users/jnez71/following{/other_user}","gists_url":"https://api.github.com/users/jnez71/gists{/gist_id}","starred_url":"https://api.github.com/users/jnez71/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jnez71/subscriptions","organizations_url":"https://api.github.com/users/jnez71/orgs","repos_url":"https://api.github.com/users/jnez71/repos","events_url":"https://api.github.com/users/jnez71/events{/privacy}","received_events_url":"https://api.github.com/users/jnez71/received_events","type":"User","site_admin":false},"body":"","created_at":"2015-11-06T10:13:46Z","updated_at":"2015-11-07T03:27:15Z","closed_at":null,"merged_at":null,"merge_commit_sha":"7e6df5f90ed75692b1c57b4c0d11a24f6b9b5f67","assignee":{"login":"jpanikulam","id":6531468,"avatar_url":"https://avatars.githubusercontent.com/u/6531468?v=3","gravatar_id":"","url":"https://api.github.com/users/jpanikulam","html_url":"https://github.com/jpanikulam","followers_url":"https://api.github.com/users/jpanikulam/followers","following_url":"https://api.github.com/users/jpanikulam/following{/other_user}","gists_url":"https://api.github.com/users/jpanikulam/gists{/gist_id}","starred_url":"https://api.github.com/users/jpanikulam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpanikulam/subscriptions","organizations_url":"https://api.github.com/users/jpanikulam/orgs","repos_url":"https://api.github.com/users/jpanikulam/repos","events_url":"https://api.github.com/users/jpanikulam/events{/privacy}","received_events_url":"https://api.github.com/users/jpanikulam/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/uf-mil/Sub8/pulls/31/commits","review_comments_url":"https://api.github.com/repos/uf-mil/Sub8/pulls/31/comments","review_comment_url":"https://api.github.com/repos/uf-mil/Sub8/pulls/comments{/number}","comments_url":"https://api.github.com/repos/uf-mil/Sub8/issues/31/comments","statuses_url":"https://api.github.com/repos/uf-mil/Sub8/statuses/ff68e40b6345f95b55e4388b9981bb591c538386","head":{"label":"jnez71:master","ref":"master","sha":"ff68e40b6345f95b55e4388b9981bb591c538386","user":{"login":"jnez71","id":15154558,"avatar_url":"https://avatars.githubusercontent.com/u/15154558?v=3","gravatar_id":"","url":"https://api.github.com/users/jnez71","html_url":"https://github.com/jnez71","followers_url":"https://api.github.com/users/jnez71/followers","following_url":"https://api.github.com/users/jnez71/following{/other_user}","gists_url":"https://api.github.com/users/jnez71/gists{/gist_id}","starred_url":"https://api.github.com/users/jnez71/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jnez71/subscriptions","organizations_url":"https://api.github.com/users/jnez71/orgs","repos_url":"https://api.github.com/users/jnez71/repos","events_url":"https://api.github.com/users/jnez71/events{/privacy}","received_events_url":"https://api.github.com/users/jnez71/received_events","type":"User","site_admin":false},"repo":{"id":45663730,"name":"Sub8","full_name":"jnez71/Sub8","owner":{"login":"jnez71","id":15154558,"avatar_url":"https://avatars.githubusercontent.com/u/15154558?v=3","gravatar_id":"","url":"https://api.github.com/users/jnez71","html_url":"https://github.com/jnez71","followers_url":"https://api.github.com/users/jnez71/followers","following_url":"https://api.github.com/users/jnez71/following{/other_user}","gists_url":"https://api.github.com/users/jnez71/gists{/gist_id}","starred_url":"https://api.github.com/users/jnez71/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jnez71/subscriptions","organizations_url":"https://api.github.com/users/jnez71/orgs","repos_url":"https://api.github.com/users/jnez71/repos","events_url":"https://api.github.com/users/jnez71/events{/privacy}","received_events_url":"https://api.github.com/users/jnez71/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jnez71/Sub8","description":"Subjugator 8 On-board software","fork":true,"url":"https://api.github.com/repos/jnez71/Sub8","forks_url":"https://api.github.com/repos/jnez71/Sub8/forks","keys_url":"https://api.github.com/repos/jnez71/Sub8/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jnez71/Sub8/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jnez71/Sub8/teams","hooks_url":"https://api.github.com/repos/jnez71/Sub8/hooks","issue_events_url":"https://api.github.com/repos/jnez71/Sub8/issues/events{/number}","events_url":"https://api.github.com/repos/jnez71/Sub8/events","assignees_url":"https://api.github.com/repos/jnez71/Sub8/assignees{/user}","branches_url":"https://api.github.com/repos/jnez71/Sub8/branches{/branch}","tags_url":"https://api.github.com/repos/jnez71/Sub8/tags","blobs_url":"https://api.github.com/repos/jnez71/Sub8/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jnez71/Sub8/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jnez71/Sub8/git/refs{/sha}","trees_url":"https://api.github.com/repos/jnez71/Sub8/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jnez71/Sub8/statuses/{sha}","languages_url":"https://api.github.com/repos/jnez71/Sub8/languages","stargazers_url":"https://api.github.com/repos/jnez71/Sub8/stargazers","contributors_url":"https://api.github.com/repos/jnez71/Sub8/contributors","subscribers_url":"https://api.github.com/repos/jnez71/Sub8/subscribers","subscription_url":"https://api.github.com/repos/jnez71/Sub8/subscription","commits_url":"https://api.github.com/repos/jnez71/Sub8/commits{/sha}","git_commits_url":"https://api.github.com/repos/jnez71/Sub8/git/commits{/sha}","comments_url":"https://api.github.com/repos/jnez71/Sub8/comments{/number}","issue_comment_url":"https://api.github.com/repos/jnez71/Sub8/issues/comments{/number}","contents_url":"https://api.github.com/repos/jnez71/Sub8/contents/{+path}","compare_url":"https://api.github.com/repos/jnez71/Sub8/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jnez71/Sub8/merges","archive_url":"https://api.github.com/repos/jnez71/Sub8/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jnez71/Sub8/downloads","issues_url":"https://api.github.com/repos/jnez71/Sub8/issues{/number}","pulls_url":"https://api.github.com/repos/jnez71/Sub8/pulls{/number}","milestones_url":"https://api.github.com/repos/jnez71/Sub8/milestones{/number}","notifications_url":"https://api.github.com/repos/jnez71/Sub8/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jnez71/Sub8/labels{/name}","releases_url":"https://api.github.com/repos/jnez71/Sub8/releases{/id}","created_at":"2015-11-06T06:28:07Z","updated_at":"2015-11-06T06:28:08Z","pushed_at":"2015-11-06T10:06:40Z","git_url":"git://github.com/jnez71/Sub8.git","ssh_url":"git@github.com:jnez71/Sub8.git","clone_url":"https://github.com/jnez71/Sub8.git","svn_url":"https://github.com/jnez71/Sub8","homepage":null,"size":987,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"uf-mil:master","ref":"master","sha":"35e78aac20612c16070835373d136bd070f0eb26","user":{"login":"uf-mil","id":6162945,"avatar_url":"https://avatars.githubusercontent.com/u/6162945?v=3","gravatar_id":"","url":"https://api.github.com/users/uf-mil","html_url":"https://github.com/uf-mil","followers_url":"https://api.github.com/users/uf-mil/followers","following_url":"https://api.github.com/users/uf-mil/following{/other_user}","gists_url":"https://api.github.com/users/uf-mil/gists{/gist_id}","starred_url":"https://api.github.com/users/uf-mil/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/uf-mil/subscriptions","organizations_url":"https://api.github.com/users/uf-mil/orgs","repos_url":"https://api.github.com/users/uf-mil/repos","events_url":"https://api.github.com/users/uf-mil/events{/privacy}","received_events_url":"https://api.github.com/users/uf-mil/received_events","type":"Organization","site_admin":false},"repo":{"id":41452384,"name":"Sub8","full_name":"uf-mil/Sub8","owner":{"login":"uf-mil","id":6162945,"avatar_url":"https://avatars.githubusercontent.com/u/6162945?v=3","gravatar_id":"","url":"https://api.github.com/users/uf-mil","html_url":"https://github.com/uf-mil","followers_url":"https://api.github.com/users/uf-mil/followers","following_url":"https://api.github.com/users/uf-mil/following{/other_user}","gists_url":"https://api.github.com/users/uf-mil/gists{/gist_id}","starred_url":"https://api.github.com/users/uf-mil/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/uf-mil/subscriptions","organizations_url":"https://api.github.com/users/uf-mil/orgs","repos_url":"https://api.github.com/users/uf-mil/repos","events_url":"https://api.github.com/users/uf-mil/events{/privacy}","received_events_url":"https://api.github.com/users/uf-mil/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/uf-mil/Sub8","description":"Subjugator 8 On-board software","fork":false,"url":"https://api.github.com/repos/uf-mil/Sub8","forks_url":"https://api.github.com/repos/uf-mil/Sub8/forks","keys_url":"https://api.github.com/repos/uf-mil/Sub8/keys{/key_id}","collaborators_url":"https://api.github.com/repos/uf-mil/Sub8/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/uf-mil/Sub8/teams","hooks_url":"https://api.github.com/repos/uf-mil/Sub8/hooks","issue_events_url":"https://api.github.com/repos/uf-mil/Sub8/issues/events{/number}","events_url":"https://api.github.com/repos/uf-mil/Sub8/events","assignees_url":"https://api.github.com/repos/uf-mil/Sub8/assignees{/user}","branches_url":"https://api.github.com/repos/uf-mil/Sub8/branches{/branch}","tags_url":"https://api.github.com/repos/uf-mil/Sub8/tags","blobs_url":"https://api.github.com/repos/uf-mil/Sub8/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/uf-mil/Sub8/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/uf-mil/Sub8/git/refs{/sha}","trees_url":"https://api.github.com/repos/uf-mil/Sub8/git/trees{/sha}","statuses_url":"https://api.github.com/repos/uf-mil/Sub8/statuses/{sha}","languages_url":"https://api.github.com/repos/uf-mil/Sub8/languages","stargazers_url":"https://api.github.com/repos/uf-mil/Sub8/stargazers","contributors_url":"https://api.github.com/repos/uf-mil/Sub8/contributors","subscribers_url":"https://api.github.com/repos/uf-mil/Sub8/subscribers","subscription_url":"https://api.github.com/repos/uf-mil/Sub8/subscription","commits_url":"https://api.github.com/repos/uf-mil/Sub8/commits{/sha}","git_commits_url":"https://api.github.com/repos/uf-mil/Sub8/git/commits{/sha}","comments_url":"https://api.github.com/repos/uf-mil/Sub8/comments{/number}","issue_comment_url":"https://api.github.com/repos/uf-mil/Sub8/issues/comments{/number}","contents_url":"https://api.github.com/repos/uf-mil/Sub8/contents/{+path}","compare_url":"https://api.github.com/repos/uf-mil/Sub8/compare/{base}...{head}","merges_url":"https://api.github.com/repos/uf-mil/Sub8/merges","archive_url":"https://api.github.com/repos/uf-mil/Sub8/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/uf-mil/Sub8/downloads","issues_url":"https://api.github.com/repos/uf-mil/Sub8/issues{/number}","pulls_url":"https://api.github.com/repos/uf-mil/Sub8/pulls{/number}","milestones_url":"https://api.github.com/repos/uf-mil/Sub8/milestones{/number}","notifications_url":"https://api.github.com/repos/uf-mil/Sub8/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/uf-mil/Sub8/labels{/name}","releases_url":"https://api.github.com/repos/uf-mil/Sub8/releases{/id}","created_at":"2015-08-26T22:11:51Z","updated_at":"2015-11-06T20:36:15Z","pushed_at":"2015-11-06T10:13:46Z","git_url":"git://github.com/uf-mil/Sub8.git","ssh_url":"git@github.com:uf-mil/Sub8.git","clone_url":"https://github.com/uf-mil/Sub8.git","svn_url":"https://github.com/uf-mil/Sub8","homepage":null,"size":987,"stargazers_count":3,"watchers_count":3,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":9,"mirror_url":null,"open_issues_count":9,"forks":9,"open_issues":9,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/uf-mil/Sub8/pulls/31"},"html":{"href":"https://github.com/uf-mil/Sub8/pull/31"},"issue":{"href":"https://api.github.com/repos/uf-mil/Sub8/issues/31"},"comments":{"href":"https://api.github.com/repos/uf-mil/Sub8/issues/31/comments"},"review_comments":{"href":"https://api.github.com/repos/uf-mil/Sub8/pulls/31/comments"},"review_comment":{"href":"https://api.github.com/repos/uf-mil/Sub8/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/uf-mil/Sub8/pulls/31/commits"},"statuses":{"href":"https://api.github.com/repos/uf-mil/Sub8/statuses/ff68e40b6345f95b55e4388b9981bb591c538386"}}}}