Geometric algebra provides a framework which unifies all of the concepts that we have discussed so far. It is a powerful tool which generalizes the ideas of vectors and dot products, and elegantly describes rotations and reflections.
We describe reflections using geometric algebra, and use their connection with rotation to describe a rotation. We will use the clifford library to demonstrate these concepts.
We define the geometric product of two vectors to be a sum of two components: a commutative product () and an anti-commutative product ():
Here is a bivector, an element of the plane spanned by and ; we'll see below exactly what its magnitude and orientation mean. The dot product is just the usual dot product of two vectors. With that, we make the following observations:
Let's see these three facts directly, and along the way pin down what a bivector's magnitude and orientation actually are. First, we build as an oriented area:
import clifford as cf
layout, blades = cf.Cl(2) # create a 2D clifford algebra
# blades is a dict {'': 1, 'e1': (1^e1), 'e2': (1^e2), 'e12': (1^e12)}
# we can assign the variables e1 = blades['e1'], e2 = blades['e2'] and so on automatically by:
locals().update(blades)
v1 = 3*e1
v2 = e1 + e2
print(v1 ^ v2)
'''
Output
(3^e12)
'''
print(v1^v1)
'''
Output
0
'''
Given an orthonormal basis , we can represent any vector as a linear combination of these basis vectors. We can also represent any bivector as a linear combination of the basis bivectors .
For example, consider the parallelogram created by the vectors and in a 3D space.
We can it as a combination of the parallelograms created by projections on the basis bivectors
Some properties of and notations for 3D basis vectors :
Thus we have:
The scalar part is the dot product , and the bivector part is a linear combination of the basis bivectors:
so the parallelogram spanned by the two original vectors decomposes into a signed area of on the plane, on the plane, and on the plane, exactly the magnitudes we saw projected earlier.
layout, blades = cf.Cl(3) # create a 3D clifford algebra
locals().update(blades)
a = 2*e1 + e2 + e3
b = e1 + e2 + 2*e3
print(a * b) # sum of a scalar and a bivector
'''
Output:
5 + (1^e12) + (3^e13) + (1^e23)
'''
print(a ^ b) # a bivector expressed as a sum of projections on the basis planes
'''
Output:
(1^e12) + (3^e13) + (1^e23)
'''
The square of the bivector is:
The bivector behaves like the imaginary number .
This is a key insight that connects geometric algebra with complex numbers.
Indeed one can see that:
But what does it mean to multiply a vector by an element of the form scalar + bivector? Note that the bivector is a bivector in xy plane with magnitude and a positive orientation. Let's understand the tuple of a scalar and a bivector, , further.
Given any two unit vectors in the 2D plane with , we can see that:
For example, to rotate a vector by 90°, all of the choices below work:
In each case, : and sweep together through a full turn while stays fixed, so the readout never moves.
Also note that if rotates a vector by , then rotates the vector by . This is because:
(yes, there is a notion of an inverse of a vector, and also a bivector among other objects, in geometric algebra).
One last thing to note is that rotates the vector counterclockwise by while rotates the vector clockwise by . It also means that .
import clifford as cf
from math import pi, sin, cos
layout, blades = cf.Cl(2) # create a 2D clifford algebra
locals().update(blades)
x = 1^e1 # the vector to be rotated
theta = pi/4 # the angle of rotation
# pick any two vectors with an angle of theta between them
random_angle = 1.4376
p = (cos(random_angle)^e1) + (sin(random_angle)^e2)
q = (cos(random_angle + theta)^e1) + (sin(random_angle + theta)^e2)
pq = p*q # the bivector R = pq which is also a rotor since the vectors are unit vectors
print(pq)
'''
Output:
0.70711 + (0.70711^e12)
This is equivalent to cos(theta) + i*sin(theta) in the complex plane
'''
print(pq * x) # Rv rotates the vector x by -theta
'''
Output:
(0.70711^e1) - (0.70711^e2)
'''
print(x * pq) # vR rotates the vector x by theta
'''
Output:
(0.70711^e1) + (0.70711^e2)
'''
We just saw that . Using the same reasoning, we can show that and . Thus the bivectors behave like the imaginary numbers respectively. You can also ensure that .
That said, I think the real appreciation of geometric algebra comes not from finding these connections but from the fact that it provides a unified framework to describe rotations, reflections and many other operations in a simple and elegant way. So let's focus on that.
We start by generalizing how vectors were being rotated in a 2D plane. We had a bivector of magnitude 1 and another vector in the same plane. Then we saw that rotated the vector by an angle . This pattern actually works for any plane in a higher dimensional space.
Given a unit bivector and a vector in the same plane, the quantity with rotates the vector by an angle in the plane defined by .
This is a generalization of the 2D rotation we saw earlier. The limitation that we have right now is that we can only rotate vectors in the same plane as the bivector . We will see how to overcome this limitation in the next section.
To show why, let's use a small trick: define the quantity as a product of two unit vectors in two different ways. We pick vectors , , in the same plane as the bivector , placed on a unit circle so we can see the angles between them line up.
The is ,
and is also . Then we can write:
We can represent any vector in the plane defined by as a linear combination of the basis vectors . So if we show that rotates the vector by and rotates the vector by , then we have shown that rotates the vector by in the plane defined by . Now we have:
,
and . Thus, got rotated by to and got rotated by to .
So, given as any vector in the plane, drawn here as the amber combination of and :
which rotates the vector by in the plane defined by , landing on , the orange combination of and .
That is, once rotates the basis vectors and , inherits the same rotation by .
import clifford as cf
from math import sqrt
layout, blades = cf.Cl(3) # create a 3D clifford algebra
locals().update(blades)
x = (0^e1) + (1^e2) + (-4^e3) # the vector to be rotated
SQRT14 = sqrt(14)
SQRT3 = sqrt(3)
# define coefficients of unit vectors p and q
p1, p2, p3 = 1/SQRT14, 2/SQRT14, -3/SQRT14
q1, q2, q3 = 1/SQRT3, 1/SQRT3, 1/SQRT3
# define the unit vectors p and q
'''
note that the dot product of p and q is zero i.e p|q = 0
thus, the angle between p and q is 90 degrees
'''
p = (p1^e1) + (p2^e2) + (p3^e3)
q = (q1^e1) + (q2^e2) + (q3^e3)
'''
define the rotor R = p^q
the rotor R will rotate any vector x by an angle of ±90 degrees
'''
pq = p*q
print(f'pq is {pq}')
# now let's rotate the vector
x_rotated_1 = pq * x
x_rotated_2 = x * pq
print(x_rotated_1)
print(x_rotated_2)
print(f'Dot product between x and x_rotated_1 is {x_rotated_1|x}')
'''
pq is -(0.1543^e12) + (0.61721^e13) + (0.77152^e23)
-(2.62316^e1) - (3.08607^e2) - (0.77152^e3)
(2.62316^e1) + (3.08607^e2) + (0.77152^e3)
Dot product between x and x_rotated_1 is 0
'''
If we try to rotate a vector that is not in the plane defined by the bivector , we will get something that is not a vector:
x_not_in_plane = (0^e1) + (1^e2) + (2^e3)
print(pq * x_not_in_plane)
'''
Output:
(1.08012^e1) + (1.54303^e2) - (0.77152^e3) - (0.92582^e123)
'''
We cover reflection about a line first. Let's say the reflection of about a line defined by a unit vector is .
This reflection can be seen as a rotation of where . This rotation happens in the plane defined by and .
Rotating by about lands exactly on .
Indeed, is , twice the angle to the mirror line. Writing for the unit vector along , we can describe this reflection as:
Now let's talk about reflection about a hyperplane. Note that the reflection about a hyperplane is just the negative of the reflection about a line that is perpendicular to the hyperplane. This is because:
As you can see, these two are exactly opposite operations. Thus, the reflection about a hyperplane represented by a normal vector is given by negating the line-reflection formula above:
Notice the simplicity of this expression. It applies to vectors in all dimensions and does not depend on the basis being used. This is the power of geometric algebra.
Now we will define a general rotation in 3D space using reflections. We will use the fact that a rotation is a composition of two reflections. If the first reflection is about a line defined by a unit vector and the second reflection is about a line defined by a unit vector , then the composition of these two reflections is a rotation by an angle where is the angle between and . This is because the composition of two reflections is a rotation by twice the angle between the two lines. Thus, we can write the rotation as:
If we reflected the vector around the planes represented by the vectors and , we would still have gotten the same result because:
Now note that and are inverses of each other. Thus if we define , we can write the rotation as:
If we want to rotate by an angle of , we just need to find unit vectors which have an angle of between them. Does it look eerily similar to the formula for a rotation using quaternions? That is because quaternions are a subset of geometric algebra!
'''
To rotate the vector x_not_in_plane by 90 degrees, we find a vector which forms an angle of 45 degrees with p
'''
r1, r2, r3 = (p1+q1)/2, (p2+q2)/2, (p3+q3)/2
r_norm = sqrt(r1**2 + r2**2 + r3**2)
r1, r2, r3 = r1/r_norm, r2/r_norm, r3/r_norm
r = (r1^e1) + (r2^e2) + (r3^e3)
pr = p*r
y_not_in_plane = pr * x_not_in_plane * pr.inv()
print(y_not_in_plane)
'''
Output:
(0.36584^e1) + (2.11446^e2) - (0.62866^e3)
'''
We have gone over different ways to describe a 3D rotation. In each case, the number of free variables needed to describe a rotation is 3.
In the next section, we will cover a different approach to describe rotations in 3D space. However the number of free variables needed to describe a rotation will remain the same.