Python, object operations and vectors
A few days ago I was bored and decided to read a book about raytracing, the code was in C++ but thanks to my love for Jupyter notebooks I decided to rewrite the code in Python, so far so good (maybe one day I will post about it), then I faced a small issue, I forgot most of my vector math and linear algebra classes in high school :( (I don’t hold a computer science degree). I decide to refresh my little knowledge of linear algebra and vector operations and I saw it was a good exercise to explore Python operators and special methods.
What is a Vector and Point?
The easy way to describe a point is just as a location in space. A point is described by coordinates and in some books it is described as a transposed one row matrix with its coordinates, \((x, y, z)^\top\) or as a one column matrix:
A vector doesn’t have a location, but it has direction and length, the length is usually named magnitude. Vectors can be named and their name in mathematics are expressed by a letter with bold or a bar name (\(\mathbf{a}, \vec{a}\)).
Let’s describe a vector in Python:
class Vector:
def __init__(self, x: float = 0, y: float = 0, z: float = 0):
self.x = x
self.y = y
self.z = z
def __repr__(self) -> str:
return f'<Vector {self.x}, {self.y}, {self.z}>'
Adding and substracting vectors
This is the easiest operation, basically it is mathematically described as:
In Python it will look something like this:
from typing import TypeVar
TVector = TypeVar('TVector', bound='Vector')
class Vector:
def __init__(self, x: float = 0, y: float = 0, z: float = 0):
self.x = x
self.y = y
self.z = z
def __repr__(self) -> str:
return f'<Vector {self.x}, {self.y}, {self.z}>'
def add(other: TVector) -> TVector:
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
But to be honest, we can use one of the Python special methods so it will look more natural to do something like Vector(1, 2, 3) + Vector(4, 5, 6)
:
from typing import TypeVar
TVector = TypeVar('TVector', bound='Vector')
class Vector:
def __init__(self, x: float = 0, y: float = 0, z: float = 0):
self.x = x
self.y = y
self.z = z
def __repr__(self) -> str:
return f'<Vector {self.x}, {self.y}, {self.z}>'
def __add__(other: TVector) -> TVector:
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(other: TVector) -> TVector:
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
Scaling a Vector
The operation of a Vector and a real number is called scaling, it is simple:
Again, let’s use a Python special method for it, in this case the multiplication of a real and a Vector is a Vector, in this case it is not enough to override the __mul__
operator, mostly because that will allow only operations between a vector and an integer NOT the opposite. To allow operations between an integer and a vector we will have to override the __mul__
operator in the integer or use the __rmul__
operator, or well, named right multiplication.
from typing import TypeVar
TVector = TypeVar('TVector', bound='Vector')
TNumber = TypeVar('TNumber', int, float)
class Vector:
def __init__(self, x: float = 0, y: float = 0, z: float = 0):
self.x = x
self.y = y
self.z = z
def __repr__(self) -> str:
return f'<Vector {self.x}, {self.y}, {self.z}>'
def __add__(other: TVector) -> TVector:
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(other: TVector) -> TVector:
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(other: TNumber) -> TVector:
return Vector(self.x * other, self.y * other, self.z * other)
__rmul__ = __mul__
Equality
Vector are like matrices, they are equal if all the members are the same, in Python this is easy:
def __eq__(self, other: TVector) -> bool:
return self.x == other.x and self.y == other.y and self.z == other.z
Length or magnitude
This is represented in mathematical notation by the vector name around bars (\(|\ \mathbf{v}\ |\)) and it is basically defined as:
In Python we can implement this as a property:
@property
def length(self) -> float:
return math.sqrt(self.x**2 + self.y**2 + self.z**2)
But Python has as well the operator abs
so it will allow something like abs(Vector(1, 2, 3))
and this make sense to me. Let’s implement that special method:
def __abs__(self):
return self.length
Unit vector
Vectors are usually used to express direction, but compare vector direction is easier if we ignore the length or normalize the length of the vector to 1. This “special” vector is named a unit vector and there is only one unit vector per vector. In mathematics the unit vector is expressed as hat vector (\(\hat{v}\)) and it is equal to the vector divided by the length.
We have to implement a vector division as well, this is as simple as the multiplication:
def __truediv__(self, other: TNumber) -> TVector:
return Vector(self.x / other, self.y / other, self.z / other)
@property
def unit(self):
return self / self.length
We don’t need to implement the __rtruediv__
operation, it doesn’t make sense to divide a number by a vector.
Dot product
The dot product of two vectors is basically one of the more important operations in vector maths, it is described as:
What we care is the one in the middle. We can write this in Python as:
def dot(other: TVector) -> float:
return self.x*other.x + self.y*other.y + self.z*other.z
I really don’t like doing things like Vector(1, 2, 3).dot(Vector(4, 5, 6))
but gladly in Python 3 we have a dot product or matrix multiplication:
def __matmul__(self, other: TVector) -> float:
return self.x*other.x + self.y*other.y + self.z*other.z
Now we can do Vector(1, 2, 3) @ Vector(4, 5, 6)
.
How does it look like?
This is how our full class looks like:
import math
from typing import TypeVar
TVector = TypeVar('TVector', bound='Vector')
TNumber = TypeVar('TNumber', int, float)
class Vector:
def __init__(self, x: float = 0, y: float = 0, z: float = 0):
self.x = x
self.y = y
self.z = z
def __repr__(self) -> str:
return f'<Vector {self.x}, {self.y}, {self.z}>'
def __add__(other: TVector) -> TVector:
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(other: TVector) -> TVector:
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(other: TNumber) -> TVector:
return Vector(self.x * other, self.y * other, self.z * other)
__rmul__ = __mul__
def __eq__(self, other: TVector) -> bool:
return self.x == other.x and self.y == other.y and self.z == other.z
def __truediv__(self, other: TNumber) -> TVector:
return Vector(self.x / other, self.y / other, self.z / other)
@property
def unit(self):
return self / self.length
@property
def length(self) -> float:
return math.sqrt(self.x**2 + self.y**2 + self.z**2)
def __abs__(self) -> float:
return self.length
def __matmul__(self, other: TVector) -> float:
return self.x*other.x + self.y*other.y + self.z*other.z