How to Use the Py5Vector Class

The Py5Vector class provides linear algebra functionality for 2D, 3D, and 4D vectors.

This class was designed to be consistent with conventions established by numpy, Processing, and general Python programming. It leverages Python’s and numpy’s strengths where it makes sense to do so.

Although the design of Py5Vector values the rigor of its calculations, be aware that this is not a scientific computing vector library. Other libraries will do a better job of providing that level of functionality. This class favors simplicity and usability over high performance computing. Nevertheless, Py5Vector works seamlessly with numpy arrays, so you should be able to easily write additional code to meet your needs.

Creating Py5Vectors

Let’s create our first Py5Vector.

v1 = Py5Vector(1, 2, 3)

v1
Py5Vector3D([1., 2., 3.])

Observe that we used the Py5Vector constructor and got an instance of Py5Vector3D back. What’s actually happening here is there are 3 vector classes: Py5Vector2D, Py5Vector3D, and Py5Vector4D. All of these inherit from Py5Vector and can be constructed with Py5Vector(). (This is done by implementing __new__ instead of __init__ in the parent class). You’ll really only need Py5Vector, except perhaps when you want to use isinstance.

print('is Py5Vector?', isinstance(v1, Py5Vector))
print('is Py5Vector2D?', isinstance(v1, Py5Vector2D))
print('is Py5Vector3D?', isinstance(v1, Py5Vector3D))
print('is Py5Vector4D?', isinstance(v1, Py5Vector4D))
is Py5Vector? True
is Py5Vector2D? False
is Py5Vector3D? True
is Py5Vector4D? False

The Py5Vector constructor is reasonably sophisticated in what inputs it will accept. It will figure out the appropriate dimensionality of the inputs and create an instance of the proper vector class. The other vector classes like Py5Vector3D are similar except they are specific to vectors with a certain dimension (i.e., only 3D vectors). Most of the time you’ll only need Py5Vector.

Py5Vector3D(10, 20, 30)
Py5Vector3D([10., 20., 30.])
try:
    Py5Vector2D(10, 20, 30)
except RuntimeError as e:
    print(e)
dim parameter is 2 but Py5Vector values imply dimension of 3
Py5Vector2D(10, 20)
Py5Vector2D([10., 20.])

You can create a vector using other inputs besides a sequence of numbers.

Py5Vector([2, 4, 6])
Py5Vector3D([2., 4., 6.])
import numpy as np

Py5Vector(np.arange(3))
Py5Vector3D([0., 1., 2.])

You can create a vector using another vector. In this example, we create a 4D vector from a 3D vector and an extra number (0) for the 4th dimension:

Py5Vector(v1, 0)
Py5Vector4D([1., 2., 3., 0.])

Sometimes you need a vector of zeros. Either of the following will work:

Py5Vector(dim=3)
Py5Vector3D([0., 0., 0.])
Py5Vector3D()
Py5Vector3D([0., 0., 0.])

There are class methods for creating random vectors. Random vectors will have a magnitude of 1.

Py5Vector.random(dim=4)
Py5Vector4D([ 0.38112563, -0.58298004, -0.39864144,  0.59662596])
Py5Vector4D.random()
Py5Vector4D([ 0.95687362,  0.02856102,  0.20063608, -0.20814009])

Also, a from_heading() class method, for creating vectors with a specific heading:

Py5Vector.from_heading(np.pi / 4)
Py5Vector2D([0.70710678, 0.70710678])

The 3D heading calculations are consistent with Wikipedia’s Spherical Coordinate System article, which is also consistent with this Coding Train video. Note that neither will give the same results as p5’s fromAngles() calculations for the same parameters because p5 measures the spherical coordinate system angles relative to different axes.

Py5Vector.from_heading(0.1, 0.2)
Py5Vector3D([0.0978434 , 0.01983384, 0.99500417])

The 4D heading calculations are consistent with Wikipedia’s N-Sphere article.

Py5Vector.from_heading(0.1, 0.2, 0.3)
Py5Vector4D([0.99500417, 0.0978434 , 0.01894799, 0.0058613 ])

The order of the from_heading() parameters are consistent with the set_heading() method’s parameters and the heading property values, both demonstrated later in this page.

Data Types

Like numpy arrays, Py5Vector instances have a data type (dtype).

v1
Py5Vector3D([1., 2., 3.])
v1.dtype
dtype('float64')

On 64 bit computers, the data type of vectors will default to 64 bit floating point numbers.

If you like, you can specify a different sized floating data type. Only floating types are allowed.

v2 = Py5Vector(1, 3, 5, dtype=np.float16)

v2
Py5Vector3D([1., 3., 5.], dtype=float16)

Much like numpy arrays, the data type will be propagated through math operations:

v3 = Py5Vector(0.1, 0.2, 0.3, dtype=np.float128)

v2 + v3
Py5Vector3D([1.1, 3.2, 5.3], dtype=float128)

Accessing Vector Data

You can access the vector’s data using array indexing or vector properties.

v1 = Py5Vector(1, 2, 3)

v1
Py5Vector3D([1., 2., 3.])
v1.x, v1.y, v1.z
(1.0, 2.0, 3.0)
v1[0], v1[1], v1[2]
(1.0, 2.0, 3.0)

A 2D vector does not have the third z attribute. A 4D vector has a fourth w attribute.

All of these support assignment, including in place operations.

v1.x = 10
v1[1] = 20
v1.z += 30

v1
Py5Vector3D([10., 20., 33.])

The vector has properties such as mag, mag_sq, and heading that work the same way:

v1.mag
39.8622628559895
v1.mag = 3

v1
Py5Vector3D([0.7525915 , 1.50518299, 2.48355193])
v1.mag
3.0
v1.heading
(0.5955311914005236, 1.1071487177940904)
v1.heading = np.pi / 4, np.pi / 4

v1
Py5Vector3D([1.5       , 1.5       , 2.12132034])
v1.heading
(0.7853981633974482, 0.7853981633974483)
v1.mag
3.0
v1.mag_sq
9.0
v1.mag_sq = 100

v1.mag
10.0
v1
Py5Vector3D([5.        , 5.        , 7.07106781])

There are also methods like set_mag(), set_mag_sq(), set_limit(), and set_heading() if you don’t want to use the properties. Each of these will modify the vector in place and will return the vector itself to support method chaining.

v1 = Py5Vector.random(dim=3)

v1.set_mag(5)
Py5Vector3D([ 1.24267889, -3.9502007 ,  2.80208201])
v1
Py5Vector3D([ 1.24267889, -3.9502007 ,  2.80208201])
v1.set_mag(2).set_heading(np.pi / 4, np.pi / 4)
Py5Vector3D([1.        , 1.        , 1.41421356])

The vector v1 was modified in place:

v1
Py5Vector3D([1.        , 1.        , 1.41421356])
v1.mag
2.0

Use normalize() to normalize the vector and modify it in place. This will set the vector magnitude to 1.

v1.normalize()

v1
Py5Vector3D([0.5       , 0.5       , 0.70710678])
v1.mag
1.0

Each Py5Vector stores its vector data in a small numpy array. To access that, use the data attribute.

v1.data
array([0.5       , 0.5       , 0.70710678])

You can also use the dim and dtype attributes to get the size and data type.

v1.dim
3
v1.dtype
dtype('float64')

Use the norm attribute to create a normalized copy of a vector:

v1 = Py5Vector(10, 20, 30)

v1.norm
Py5Vector3D([0.26726124, 0.53452248, 0.80178373])

Use the copy attribute to create an unmodified copy of the vector:

v2 = v1.copy

v2.x = 42

v2
Py5Vector3D([42., 20., 30.])

Observe that v1 is unchanged.

v1
Py5Vector3D([10., 20., 30.])

Swizzling

Vector Swizzling is a useful feature inspired by OpenGL’s vector class. The basic idea is you can compose new vectors by rearranging components of other vectors. For example:

v1 = Py5Vector(1, 2, 3)

v1
Py5Vector3D([1., 2., 3.])
v1.yx
Py5Vector2D([2., 1.])
v1.xyzz
Py5Vector4D([1., 2., 3., 3.])

Swizzles support item assignment. Possible assignments include constants as well as and properly sized numpy arrays, Py5Vectors, and iterables.

v1.xy = 10, 20

v1
Py5Vector3D([10., 20.,  3.])
v1.zx += 100

v1
Py5Vector3D([110.,  20., 103.])

You can use x, y, z, and w to refer to the first, second, third, and fourth components. A “swizzle” can be up to 4 components in length. Using the same component multiple times is allowed when accessing data but not for assignments.

Math Operations

You can do math operations on Py5Vectors. Operands can be constants, or properly sized numpy arrays, Py5Vectors, or iterables.

v1 = Py5Vector(1, 2, 3)
v2 = Py5Vector(10, 20, 30)

v1 + 10
Py5Vector3D([11., 12., 13.])
v1 + v2
Py5Vector3D([11., 22., 33.])

Numpy array operands must be broadcastable to a shape that numpy can work with. If operation’s result is appropriate for a Py5Vector, the result will be a Py5Vector. Otherwise, it will be a numpy array. For example:

v1 + np.random.rand(3)
Py5Vector3D([1.87035381, 2.75272306, 3.89827655])

Below, the numpy array is broadcastable because the size of the last dimension is 3, which matches the size of the vector v1. The result of the operation is a multi-dimensional array so it cannot be a vector. This operation effectively adds v1 to each row of the numpy array.

v1 + np.random.rand(4, 3)
array([[1.73211422, 2.54305579, 3.34113297],
       [1.35632567, 2.84287157, 3.29054448],
       [1.61370971, 2.28749892, 3.40714426],
       [1.77010233, 2.55551355, 3.8014344 ]])

Next, a 3D vector is matrix multiplied with a 3x2 array. The result of the calculation is an array with 2 elements, which will be returned as a 2D vector:

v1 @ np.random.rand(3, 2)
Py5Vector2D([1.98284162, 1.03399583])

Note that if the operands are reversed (and the matrix size is modified appropriately) the result is a numpy array, not a Py5Vector. It is a numpy array because this calculation is done by numpy’s matrix multiplication method and not py5’s.

np.random.rand(2, 3) @ v1
array([2.91807706, 2.47657936])

Doing a matrix multiplication with a 3x5 array creates a 5 element numpy array because py5 does not support 5D vectors:

v1 @ np.random.rand(3, 5)
array([4.10648332, 2.65936226, 3.24613295, 0.83946531, 1.45752027])

You can add or subtract Py5Vectors, like so:

v1 - v2
Py5Vector3D([ -9., -18., -27.])

Other operations like multiplication, division, modular division, or power don’t really make sense for two vectors and are not supported. If you want to perform those operations element-wise, you can use the vector’s data attribute to access the vector’s data as a numpy array. Any properly sized operation with a Py5Vector and a numpy array is always allowed:

v1 / v2.data
Py5Vector3D([0.1, 0.1, 0.1])
v2 ** v1.data
Py5Vector3D([1.0e+01, 4.0e+02, 2.7e+04])

You can do in place operations on a Py5Vector:

v1 += v2

v1
Py5Vector3D([11., 22., 33.])

In place operations that would try to change the size or type of the output operand are not possible and therefore not allowed.

try:
    v1 += np.random.rand(4, 3)
except RuntimeError as e:
    print(e)
Unable to perform addition on a Py5Vector and a numpy array, probably because of a size mismatch. The error message is: non-broadcastable output operand with shape (3,) doesn't match the broadcast shape (4,3)

Py5Vectors work well with other Python builtins:

v1 = Py5Vector4D.random() - 5

v1
Py5Vector4D([-4.11250582, -5.45592388, -5.00470372, -5.06682337])
round(v1)
Py5Vector4D([-4., -5., -5., -5.])
abs(v1)
Py5Vector4D([4.11250582, 5.45592388, 5.00470372, 5.06682337])
divmod(v1, [1, 2, 3, 4])
(Py5Vector4D([-5., -3., -2., -2.]),
 Py5Vector4D([0.88749418, 0.54407612, 0.99529628, 2.93317663]))

A Py5Vector will evaluate to True if it has at least one non-zero element.

bool(v1)
True
v2 = Py5Vector3D()

v2
Py5Vector3D([0., 0., 0.])
bool(v2)
False

Other Math Functions

There is a lerp() method for doing linear interpolations between two vectors:

v1 = Py5Vector(10, 100)
v2 = Py5Vector(20, 200)

v1.lerp(v2, 0.1)
Py5Vector2D([ 11., 110.])
v1.lerp(v2, 0.9)
Py5Vector2D([ 19., 190.])

The dist() method calculates the distance between two vectors:

v1.dist(v2)
100.4987562112089

The dot() method calculates the dot product of two vectors:

v1.dot(v2)
20200.0

And finally, the cross() method. Technically the cross product is only defined for 3D vectors, but many vector implementations allow 2D vectors for cross calculations. Unfortunately there is little consistency in how those 2D vectors are handled.

Processing will always assume that a 2D vector’s z component is zero and the cross calculation will always return a 3D vector. Processing has only one class for both 2D and 3D vectors, so that is the only thing it can do.

Numpy implements the cross method differently. When calculating a cross between a 2D and a 3D vector, numpy will assume that the 2D vector’s z component is zero and will proceed accordingly. For two 2D vectors, it will return just the z value, which is sometimes called a “wedge product”.

Py5Vector’s cross() method is consistent with numpy’s approach because being consistent with numpy was an important design goal. Therefore, Py5Vector’s cross() method makes the same assumptions as numpy and will return the same values.

v1 = Py5Vector3D.random()
v2 = Py5Vector3D.random()

Here is the cross product of two 3D vectors:

v1.cross(v2)
Py5Vector3D([ 0.21690363, -0.03800497,  0.54305208])

The cross product of a 3D vector and a 2D vector:

v1.cross(v2.xy)
Py5Vector3D([ 0.26825888, -0.12964045,  0.54305208])

That calculation assumed that the z component was zero:

v1.cross(Py5Vector(v2.xy, 0))
Py5Vector3D([ 0.26825888, -0.12964045,  0.54305208])

Note that the values are the same as what np.cross() returns, which is important.

np.cross(v1, v2.xy)
array([ 0.26825888, -0.12964045,  0.54305208])

The cross product of two 2D vectors returns a scalar.

v1.xy.cross(v2.xy)
0.5430520830395409

This is also consistent with what np.cross() returns:

np.cross(v1.xy, v2.xy)
array(0.54305208)

It would be a design flaw to throw an error or return different results than numpy.

All numpy functions should accept Py5Vector instances as if they were any other iterable. This can be used to do calculations that the Py5Vector class does not directly support.

np.outer(v1, v2)
array([[ 0.35991766,  0.74476067, -0.09163548],
       [ 0.20170858,  0.41738608, -0.05135525],
       [-0.12964045, -0.26825888,  0.03300662]])
np.sin(v1)
array([ 0.73943429,  0.44967936, -0.29529546])
np.ceil(v1)
array([ 1.,  1., -0.])