10

What is the best way of implementing an efficient Vector / Point class (or even better: is there one already), that can be used both in Python 2.7+ and 3.x?

I've found the blender-mathutils, but they seem to only support Python 3.x. Then there's this Vector class, that uses numpy, but it's only a 3D vector. Using a list for a Vector like kivy's vector class (sourcecode) that has static attributes (x and y) seems weird too. (There are all these list-methods.)

At the moment I'm using a class that extends namedtuple (as you can see below), but this has the disadvantage of not being able to change the coordinates. I think this can become a performance problem, when thousands of objects are moving and a new (vector) tuple is created everytime. (right?)

class Vector2D(namedtuple('Vector2D', ('x', 'y'))):
    __slots__ = ()

    def __abs__(self):
        return type(self)(abs(self.x), abs(self.y))

    def __int__(self):
        return type(self)(int(self.x), int(self.y))

    def __add__(self, other):
        return type(self)(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return type(self)(self.x - other.x, self.y - other.y)

    def __mul__(self, other):
        return type(self)(self.x * other, self.y * other)

    def __div__(self, other):
        return type(self)(self.x / other, self.y / other)

    def dot_product(self, other):
        return self.x * other.x + self.y * other.y

    def distance_to(self, other):
        """ uses the Euclidean norm to calculate the distance """
        return hypot((self.x - other.x), (self.y - other.y))

Edit: I did some testing and it seems that using numpy.array or numpy.ndarray as a vector is too slow. (For example getting an item takes almost twice as long, not to mention creating an array. I think it's more optimized for doing calculations on a large number of items.)

So, I'm looking more for a lightweight vector class with a fixed number of fields (in my case just x and y) that can be used for games. (I don't want to re-invent the wheel if there's already a well-tested one.)

Joschua
  • 5,816
  • 5
  • 33
  • 44
  • 1
    Related: http://stackoverflow.com/questions/1076778/good-geometry-library-in-python/1077458#1077458 – Cody Piersall Oct 18 '13 at 20:50
  • I think if you're worried so much about performance, you're using the wrong language (don't get me wrong, python's awesome, but CPython is slow; you could try PyPy if you're not relying on third party packages). – Jonas Schäfer Oct 19 '13 at 15:19
  • You can try PyPy even if you are relying on (some) third-party packages. :) In particular, pure-Python packages should work, and some non pure-Python packages have been ported, or are being ported (at least partially). – Eric O. Lebigot Oct 20 '13 at 09:58
  • Good point, about the speed comparison between `namedtuple` and `ndarray`. I obtain similar results for the sum and multiplication by a number (NumPy is slower on 2 coordinates). – Eric O. Lebigot Oct 20 '13 at 10:03
  • 1
    I checked [another pure-Python implementation of a 2D vector](http://www.pygame.org/wiki/2DVectorClass), but it is slower than what you propose, for coordinate access. It is also more general… I have never seen a 2D or 3D optimized vector implementation for Python: I would guess that implementing it yourself is the best option. You can even publish the result on the Python Package Index. :) – Eric O. Lebigot Oct 20 '13 at 10:12
  • If you do end up using your implementation, don't use `np.hypot` since it casts your tuple into an `np.ndarray` before calculation. – askewchan Oct 20 '13 at 20:15
  • Thanks Jonas Wielicki and askewchan. Thank you EOL, I'll accept your answer for now, but I may change it later if I've implemented my own or something new comes up. – Joschua Oct 22 '13 at 12:37
  • 1
    Did you consider using complex numbers? they give you v1 + v2, v1 - v2, real x * v, v / x, abs(v) and abs(pt1 - pt2). Multiplication by a unit vector is rotation and a * pt + b is any affine transformation. If you import exp and log from complex you get rotation by an angle, arc tangent of a vector or a difference, and conversion between polar and rectangular. Because it's awkward to say .real and .imag instead of .x and .y, I wrote a subclass of complex to rename the attributes. That makes my Points larger and i'm sure slower than raw complexes. – FutureNerd Sep 03 '19 at 01:01
  • @FutureNerd Thanks, I wasn't aware of the fact that complex numbers behave like vectors in most cases. Really cool solution. – Joschua Sep 03 '19 at 15:08

3 Answers3

16

Yeah, there is a vector class: it's in the de facto standard NumPy module. You create vectors like so:

>>> v = numpy.array([1, 10, 123])
>>> 2*v
array([  2,  20, 246])
>>> u = numpy.array([1, 1, 1])
>>> v-u
array([  0,   9, 122])

NumPy is very rich and gives you access to fast array operations: dot product (numpy.dot()), norm (numpy.linalg.norm()), etc.

Eric O. Lebigot
  • 91,433
  • 48
  • 218
  • 260
  • Thanks! Do you know a clean wrapper class that uses numpy for 2D vectors? (numpy.array is full of other methods that aren't needed for vectors) – Joschua Oct 18 '13 at 20:34
  • 1
    There is a subclass of `np.ndarray` called `np.matrix`. It's not _cleaner_, but if you have two `matrix`es, `A*B` provides the matrix product of the two (as in, `np.dot(A,B)` if they are dual vectors) – askewchan Oct 18 '13 at 20:48
  • The only downside to this is you can not access `x`, and `y` attributes like `v.x` or `v.y` – DollarAkshay Apr 22 '22 at 00:28
  • You can, by using NumPy's record arrays. – Eric O. Lebigot Apr 23 '22 at 05:38
7

The vector class in numpy in terms of linear algebra would probably be the numpy.matrix which is a subclass of numpy.ndarray. It's not cleaner per se but it makes your code cleaner because algebraic operations are assumed instead of elementwise.

In [77]: a = np.array([1,2])

In [78]: b = np.array([3,3])

In [79]: a*b
Out[79]: array([3, 6])

In [80]: np.dot(a,b)
Out[80]: 9

In [81]: np.outer(a,b)
Out[81]: 
array([[3, 3],
       [6, 6]])

In [82]: a = np.matrix(a).T

In [83]: b = np.matrix(b)

In [84]: b*a
Out[84]: matrix([[9]])

In [85]: a*b
Out[85]: 
matrix([[3, 3],
        [6, 6]])

If you want to create your own, base it on one of these, for example:

class v2d(np.ndarray):
    def __abs__(self):
        return np.linalg.norm(self)
    def dist(self,other):
        return np.linalg.norm(self-other)
    def dot(self, other):
        return np.dot(self, other)
    # and so on

Which in the simplest case you can just make by viewing an ndarray as your new class:

In [63]: a = np.array([1,2]).view(v2d)

In [64]: b = np.array([3,3]).view(v2d)

In [65]: a
Out[65]: v2d([1, 2])

In [66]: abs(b)
Out[66]: 4.2426406871192848

In [67]: a - b
Out[67]: v2d([-2, -1])

In [68]: a*b
Out[68]: v2d([3, 6])

In [69]: a*3
Out[69]: v2d([3, 6]) 

In [70]: a.dist(b)
Out[70]: 2.2360679774997898

In [71]: b.dist(a)
Out[71]: 2.2360679774997898

In [72]: a.dot(b)
Out[72]: 9

Here is more information on subclassing the ndarray.

askewchan
  • 45,161
  • 17
  • 118
  • 134
1

I needed a quick solution as well so I just wrapped numpy's array into my own. You'll notice some design decisions which can be changed to fit your own needs (like defaults). If you'd like to use it: https://gist.github.com/eigencoder/c029d7557e1f0828aec5

import numpy as np


class Point(np.ndarray):
    """
    n-dimensional point used for locations.
    inherits +, -, * (as dot-product)
    > p1 = Point([1, 2])
    > p2 = Point([4, 5])
    > p1 + p2
    Point([5, 7])
    See ``test()`` for more usage.
    """
    def __new__(cls, input_array=(0, 0)):
        """
        :param cls:
        :param input_array: Defaults to 2d origin
        """
        obj = np.asarray(input_array).view(cls)
        return obj

    @property
    def x(self):
        return self[0]

    @property
    def y(self):
        return self[1]

    @property
    def z(self):
        """
        :return: 3rd dimension element. 0 if not defined
        """
        try:
            return self[2]
        except IndexError:
            return 0

    def __eq__(self, other):
        return np.array_equal(self, other)

    def __ne__(self, other):
        return not np.array_equal(self, other)

    def __iter__(self):
        for x in np.nditer(self):
            yield x.item()


    def dist(self, other):
        """
        Both points must have the same dimensions
        :return: Euclidean distance
        """
        return np.linalg.norm(self - other)


def test():
    v1 = Point([1, 2, 3])
    v2 = Point([4, 5, 7])
    v3 = Point([4, ])
    sum12 = Point([5, 7, 10])
    dot12 = Point([4, 10, 21])

    # Access
    assert v2.x == 4
    assert v2.y == 5
    assert v2.z == 7
    assert v3.z == 0
    assert Point().x == 0
    assert v2[0] == 4
    assert v1[-1] == 3  # Not needed but inherited
    assert [x for x in v2] == [4, 5, 7], "Iteration should return all elements"

    # Operations
    assert v1 + v2 == sum12
    assert v1 * v2 == dot12
    assert v1.dist(v2) ** 2 == 34
    assert v1 != v2
    assert v2.size == 3, "v2 should be a 3d point"

    print "pass"


if __name__ == "__main__":
    test()