87

I need to determine the angle(s) between two n-dimensional vectors in Python. For example, the input can be two lists like the following: [1,2,3,4] and [6,7,8,9].

Gabriel
  • 32,750
  • 58
  • 187
  • 337
Peter
  • 873
  • 1
  • 7
  • 4
  • 1
    This is the best answer is @MK83's as it is exactly the mathematical expression theta = atan2(u^v, u.v). even the case where u=[0 0] or v=[0 0] is covered because this is only time atan2 will produce the NaN in the other answers NaN will be produced by the / norm(u) or / norm(v) – PilouPili Sep 01 '18 at 10:38

11 Answers11

167

Note: all of the other answers here will fail if the two vectors have either the same direction (ex, (1, 0, 0), (1, 0, 0)) or opposite directions (ex, (-1, 0, 0), (1, 0, 0)).

Here is a function which will correctly handle these cases:

import numpy as np

def unit_vector(vector):
    """ Returns the unit vector of the vector.  """
    return vector / np.linalg.norm(vector)

def angle_between(v1, v2):
    """ Returns the angle in radians between vectors 'v1' and 'v2'::

            >>> angle_between((1, 0, 0), (0, 1, 0))
            1.5707963267948966
            >>> angle_between((1, 0, 0), (1, 0, 0))
            0.0
            >>> angle_between((1, 0, 0), (-1, 0, 0))
            3.141592653589793
    """
    v1_u = unit_vector(v1)
    v2_u = unit_vector(v2)
    return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
David Wolever
  • 130,273
  • 78
  • 311
  • 472
  • Wouldn't it be better to use `np.isnan` instead of the one from the math library? In theory they should be identical, but I'm not quite sure in practice. Either way I'd imagine it would be safer. – Hooked Jul 15 '13 at 15:52
  • The only difference is that `np.isnan` will do something sensible if the input is an array, which will never be the case here. However, using `np.isnan` would definitely be cleaner (not sure why I used `math.isnan`…), so I'll switch that up. – David Wolever Jul 15 '13 at 16:03
  • 2
    My numpy (version==1.12.1) can use `arccos` directly and safely. : In [140]: np.arccos(np.dot(np.array([1,0,0]),np.array([-1,0,0]) )) Out[140]: 3.1415926535897931 In [141]: np.arccos(np.dot(np.array([1,0,0]),np.array([1,0,0]) )) Out[141]: 0.0 – ene Aug 17 '17 at 08:04
  • 2
    The special case where at least one input vector is the zero vector is omitted, which is problematic for the division in `unit_vector`. One possibility is to just return the input vector in this function when this is the case. – kafman Oct 18 '17 at 12:06
  • If you apply this to an array of vectors that are all positive (including zero), the maximum angle between two vectors is 90 degrees. i've tried scaling my data from -1 to 1, but i'm still getting angles strictly between 0 and 90 (instead of 0 and 180) – DrTchocky May 23 '19 at 20:21
  • 3
    angle_between((0, 0, 0), (0, 1, 0)) will give nan as result, and not 90 – FabioSpaghetti Jun 04 '19 at 09:34
  • 2
    @kafman 0-vectors' angle is undefined (in math). So the fact it raises an error is good. – user Jun 05 '19 at 20:41
  • @ene it can still return `nan` if you use random unit vectors that happen to have a product of 1. Due to _floating-point precision_ `dot()` would return `1.0000000000000002`. This is true even for version 1.16.1. – user Jun 05 '19 at 20:50
67
import math

def dotproduct(v1, v2):
  return sum((a*b) for a, b in zip(v1, v2))

def length(v):
  return math.sqrt(dotproduct(v, v))

def angle(v1, v2):
  return math.acos(dotproduct(v1, v2) / (length(v1) * length(v2)))

Note: this will fail when the vectors have either the same or the opposite direction. The correct implementation is here: https://stackoverflow.com/a/13849249/71522

Community
  • 1
  • 1
Alex Martelli
  • 762,786
  • 156
  • 1,160
  • 1,345
  • 2
    Also, if you only need cos, sin, tan of angle, and not the angle itself, then you can skip the math.acos to get cosine, and use cross product to get sine. – mbeckish May 13 '10 at 14:17
  • 10
    Given that `math.sqrt(x)` is equivalent to `x**0.5` and `math.pow(x,y)` is equivalent to `x**y`, I'm surprised these survived the redundancy axe wielded during the Python 2.x->3.0 transition. In practice, I'm usually doing these kinds of numeric things as part of a larger compute-intensive process, and the interpreter's support for '**' going directly to the bytecode BINARY_POWER, vs. the lookup of 'math', the access to its attribute 'sqrt', and then the painfully slow bytecode CALL_FUNCTION, can make a measurable improvement in speed at no coding or readability cost. – PaulMcG May 14 '10 at 07:11
  • 5
    As in the answer with numpy: This can fail if the rounding error comes into play! This can happen for parallel and anti-parallel vectors! – BandGap Jan 27 '12 at 11:15
  • 2
    Note: **this will fail** if the vectors are identical (ex, `angle((1., 1., 1.), (1., 1., 1.))`). See my answer for a slightly more correct version. – David Wolever Dec 12 '12 at 21:41
  • (err, and "by identical" I mean "have the same or opposite directions") – David Wolever Dec 12 '12 at 21:53
  • @DavidWolever This tripped me up so I'm going to have to clarify. The only time this is undefined is when the magnitude of one of the vectors is 0. Any other time an the cauchy-schwarz inequality states that the input to acos will be between -1 and 1. – Pace Mar 12 '13 at 23:07
  • @Pace uuhh… I don't believe that's true… As the vectors `(1., 1., 1.)`, `(1., 1., 1.)` obviously have non-zero magnitudes, but the calculation results in `nan`… – David Wolever Mar 12 '13 at 23:29
  • norm(1,1,1)*norm(1,1,1) = 3. dot((1,1,1),(1,1,1)) = 3. acos(3/3) = 0. – Pace Mar 13 '13 at 00:10
  • 2
    If you're talking about the implementation above then it fails because of rounding errors, not because the vectors are parallel. – Pace Mar 13 '13 at 00:16
  • Simply use ***sklearn.metrics.pairwise import cosine_similarity*** and then acos – Priyank Pathak Mar 28 '18 at 21:01
  • you are genius, – Khalil Al Hooti May 10 '18 at 01:24
  • 1
    The most general answer was given by @MK83. Mathematically the angle is given by theta = atan2(u^v, u.v) is python terms angle = np.math.atan2(np.linalg.det([v0,v1]),np.dot(v0,v1)) – PilouPili Sep 01 '18 at 10:29
  • 1
    Also, it is best to use the approach given by PilouPili, since `acos` and `asin` are not numerically stable for some values. Why does nobody teach this anymore? – HackerBoss Feb 20 '19 at 18:16
46

Using numpy (highly recommended), you would do:

from numpy import (array, dot, arccos, clip)
from numpy.linalg import norm

u = array([1.,2,3,4])
v = ...
c = dot(u,v)/norm(u)/norm(v) # -> cosine of the angle
angle = arccos(clip(c, -1, 1)) # if you really want the angle
Paulo Scardine
  • 60,096
  • 9
  • 116
  • 138
Olivier Verdier
  • 41,410
  • 26
  • 94
  • 89
  • 3
    The last line can result in an error as I've found out because of rounding errors. Thus if you to dot(u,u)/norm(u)**2 it results in 1.0000000002 and the arccos then fails (also 'works' for antiparallel vectors) – BandGap Jan 27 '12 at 11:10
  • I've tested with u=[1,1,1]. u=[1,1,1,1] works fine but every dimension added returns slightly larger or smaler values than 1... – BandGap Jan 27 '12 at 11:20
  • 3
    Note: **this will fail** (yield `nan`) when the direction of the two vectors is either identical or opposite. See my answer for a more correct version. – David Wolever Dec 12 '12 at 21:52
  • 2
    adding neo's comment to this, the last line should be `angle = arccos(clip(c, -1, 1))` to avoid rounding issues. This solves @DavidWolever 's issue. – Tim Tisdall Dec 30 '14 at 15:40
  • 4
    For the folks using the code snippet above: `clip` should be added to the list of numpy imports. – Liam Deacon Jun 18 '15 at 16:11
34

The other possibility is using just numpy and it gives you the interior angle

import numpy as np

p0 = [3.5, 6.7]
p1 = [7.9, 8.4]
p2 = [10.8, 4.8]

''' 
compute angle (in degrees) for p0p1p2 corner
Inputs:
    p0,p1,p2 - points in the form of [x,y]
'''

v0 = np.array(p0) - np.array(p1)
v1 = np.array(p2) - np.array(p1)

angle = np.math.atan2(np.linalg.det([v0,v1]),np.dot(v0,v1))
print np.degrees(angle)

and here is the output:

In [2]: p0, p1, p2 = [3.5, 6.7], [7.9, 8.4], [10.8, 4.8]

In [3]: v0 = np.array(p0) - np.array(p1)

In [4]: v1 = np.array(p2) - np.array(p1)

In [5]: v0
Out[5]: array([-4.4, -1.7])

In [6]: v1
Out[6]: array([ 2.9, -3.6])

In [7]: angle = np.math.atan2(np.linalg.det([v0,v1]),np.dot(v0,v1))

In [8]: angle
Out[8]: 1.8802197318858924

In [9]: np.degrees(angle)
Out[9]: 107.72865519428085
MK83
  • 616
  • 6
  • 8
5

If you're working with 3D vectors, you can do this concisely using the toolbelt vg. It's a light layer on top of numpy.

import numpy as np
import vg

vec1 = np.array([1, 2, 3])
vec2 = np.array([7, 8, 9])

vg.angle(vec1, vec2)

You can also specify a viewing angle to compute the angle via projection:

vg.angle(vec1, vec2, look=vg.basis.z)

Or compute the signed angle via projection:

vg.signed_angle(vec1, vec2, look=vg.basis.z)

I created the library at my last startup, where it was motivated by uses like this: simple ideas which are verbose or opaque in NumPy.

paulmelnikow
  • 16,036
  • 6
  • 56
  • 110
3

David Wolever's solution is good, but

If you want to have signed angles you have to determine if a given pair is right or left handed (see wiki for further info).

My solution for this is:

def unit_vector(vector):
    """ Returns the unit vector of the vector"""
    return vector / np.linalg.norm(vector)

def angle(vector1, vector2):
    """ Returns the angle in radians between given vectors"""
    v1_u = unit_vector(vector1)
    v2_u = unit_vector(vector2)
    minor = np.linalg.det(
        np.stack((v1_u[-2:], v2_u[-2:]))
    )
    if minor == 0:
        raise NotImplementedError('Too odd vectors =(')
    return np.sign(minor) * np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))

It's not perfect because of this NotImplementedError but for my case it works well. This behaviour could be fixed (cause handness is determined for any given pair) but it takes more code that I want and have to write.

sgt pepper
  • 192
  • 2
  • 10
2

Building on sgt pepper's great answer and adding support for aligned vectors plus adding a speedup of over 2x using Numba

@njit(cache=True, nogil=True)
def angle(vector1, vector2):
    """ Returns the angle in radians between given vectors"""
    v1_u = unit_vector(vector1)
    v2_u = unit_vector(vector2)
    minor = np.linalg.det(
        np.stack((v1_u[-2:], v2_u[-2:]))
    )
    if minor == 0:
        sign = 1
    else:
        sign = -np.sign(minor)
    dot_p = np.dot(v1_u, v2_u)
    dot_p = min(max(dot_p, -1.0), 1.0)
    return sign * np.arccos(dot_p)

@njit(cache=True, nogil=True)
def unit_vector(vector):
    """ Returns the unit vector of the vector.  """
    return vector / np.linalg.norm(vector)

def test_angle():
    def npf(x):
        return np.array(x, dtype=float)
    assert np.isclose(angle(npf((1, 1)), npf((1,  0))),  pi / 4)
    assert np.isclose(angle(npf((1, 0)), npf((1,  1))), -pi / 4)
    assert np.isclose(angle(npf((0, 1)), npf((1,  0))),  pi / 2)
    assert np.isclose(angle(npf((1, 0)), npf((0,  1))), -pi / 2)
    assert np.isclose(angle(npf((1, 0)), npf((1,  0))),  0)
    assert np.isclose(angle(npf((1, 0)), npf((-1, 0))),  pi)

%%timeit results without Numba

  • 359 µs ± 2.86 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

And with

  • 151 µs ± 820 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
crizCraig
  • 7,169
  • 4
  • 46
  • 50
2

Easy way to find angle between two vectors(works for n-dimensional vector),

Python code:

import numpy as np

vector1 = [1,0,0]
vector2 = [0,1,0]

unit_vector1 = vector1 / np.linalg.norm(vector1)
unit_vector2 = vector2 / np.linalg.norm(vector2)

dot_product = np.dot(unit_vector1, unit_vector2)

angle = np.arccos(dot_product) #angle in radian
Kevin Patel
  • 321
  • 3
  • 8
1

Use some functions from numpy.

import numpy as np

def dot_product_angle(v1,v2):

    if np.linalg.norm(v1) == 0 or np.linalg.norm(v2) == 0:
        print("Zero magnitude vector!")
    else:
        vector_dot_product = np.dot(v1,v2)
        arccos = np.arccos(vector_dot_product / (np.linalg.norm(v1) * np.linalg.norm(v2)))
        angle = np.degrees(arccos)
        return angle
    return 0
Eugenio X
  • 11
  • 2
0

Using numpy and taking care of BandGap's rounding errors:

from numpy.linalg import norm
from numpy import dot
import math

def angle_between(a,b):
  arccosInput = dot(a,b)/norm(a)/norm(b)
  arccosInput = 1.0 if arccosInput > 1.0 else arccosInput
  arccosInput = -1.0 if arccosInput < -1.0 else arccosInput
  return math.acos(arccosInput)

Note, this function will throw an exception if one of the vectors has zero magnitude (divide by 0).

Pace
  • 33,215
  • 10
  • 99
  • 130
0

For the few who may have (due to SEO complications) ended here trying to calculate the angle between two lines in python, as in (x0, y0), (x1, y1) geometrical lines, there is the below minimal solution (uses the shapely module, but can be easily modified not to):

from shapely.geometry import LineString
import numpy as np

ninety_degrees_rad = 90.0 * np.pi / 180.0

def angle_between(line1, line2):
    coords_1 = line1.coords
    coords_2 = line2.coords

    line1_vertical = (coords_1[1][0] - coords_1[0][0]) == 0.0
    line2_vertical = (coords_2[1][0] - coords_2[0][0]) == 0.0

    # Vertical lines have undefined slope, but we know their angle in rads is = 90° * π/180
    if line1_vertical and line2_vertical:
        # Perpendicular vertical lines
        return 0.0
    if line1_vertical or line2_vertical:
        # 90° - angle of non-vertical line
        non_vertical_line = line2 if line1_vertical else line1
        return abs((90.0 * np.pi / 180.0) - np.arctan(slope(non_vertical_line)))

    m1 = slope(line1)
    m2 = slope(line2)

    return np.arctan((m1 - m2)/(1 + m1*m2))

def slope(line):
    # Assignments made purely for readability. One could opt to just one-line return them
    x0 = line.coords[0][0]
    y0 = line.coords[0][1]
    x1 = line.coords[1][0]
    y1 = line.coords[1][1]
    return (y1 - y0) / (x1 - x0)

And the use would be

>>> line1 = LineString([(0, 0), (0, 1)]) # vertical
>>> line2 = LineString([(0, 0), (1, 0)]) # horizontal
>>> angle_between(line1, line2)
1.5707963267948966
>>> np.degrees(angle_between(line1, line2))
90.0
Julio Cezar Silva
  • 1,332
  • 1
  • 11
  • 24