Geometric (Clifford) algebra

5. Geometric algebra (aka Clifford algebra)
Geometric algebra provides a framework which unifies all of the above 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 (and many other operations). We will describe a reflection using geometric algebra and use the above connection with rotations to describe a rotation. We will use clifford library to demonstrate these concepts.
5.1 Some definitions
We define the geometric product of two vectors to be a sum of two components: a commutative product (\( a.b = b.a \)) and an anti-commutative product (\( a \wedge b = -b \wedge a \)):
\( ab = a \cdot b + a \wedge b \)
\( ba = a \cdot b - a \wedge b \)
Here \( a \wedge b \) is a bivector - it represents an element of a plane created by vectors \( a \) and \( b \) with a magnitude and an orientation. The magnitude is the area of the parallelogram formed by \( a \) and \( b \) and the orientation gives it a sign (+ or -). The dot product \( a \cdot b \) the usual dot product of two vectors. We make the following observations:
For any vector \( v \), \( v \wedge v = -v \wedge v \Rightarrow v \wedge v = 0 \Rightarrow vv = v.v \) and \( u^2 = uu = 1 \) for any unit vector \( u \).
For two perpendicular vectors \( a,b \), \( a.b = 0 \Rightarrow ab = a \wedge b \).
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
'''
    
5.2 The key idea behind bivectors (and multivectors)
Given an orthonormal basis \( e_1, e_2, ..., e_n \), we can represent any vector \( v \) as a linear combination of these basis vectors. We can also represent any bivector \( B \) as a linear combination of the basis bivectors \( e_i \wedge e_j \). For example, consider the parallelogram created by the vectors \( \begin{bmatrix} 2 \\ 1 \\ 1 \end{bmatrix} \) and \( \begin{bmatrix} 1 \\ 1 \\ 2 \end{bmatrix} \) in a 3D space. We can represent it as a combination of the parallelograms created by projections on the basis bivectors \( e_1 \wedge e_2, e_2 \wedge e_3, e_3 \wedge e_1 \) which represent the xy, yz and zx planes respectively. Here we show what these projected vectors and parallelograms look like.
We define the geometric product for 3D basis vectors \( e_1, e_2, e_3 \) as:
Since these basis vectors are unit vectors, we have \( e_1e_1 = e_2e_2 = e_3e_3 = 1 \)
A unit bivector in xy plane is represented by \( e_1e_2 = e_{12} \)
Similarly, a unit bivector in yz plane is defined like so: \( e_2e_3 = e_{23} \)
And finally a unit bivector in xz plane: \( e_1e_3 = e_{13} \)
The anti-commutativity of the wedge product gives us: \( e_{ij} = -e_{ji}, \ i \ne j \)
Thus we have \( \begin{bmatrix} 2 \\ 1 \\ 1 \end{bmatrix} \)\( \begin{bmatrix} 1 \\ 1 \\ 2 \end{bmatrix} \)\( = (2e_1 + e_2 + e_3)(e_1 + e_2 + 2e_3) = 2 + 1 + 2 + e_1e_2 + e_2e_3 - 3e_3e_1 \). Note how the parallelogram is represented as a linear combination of the basis bivectors with magnitudes of \( 1,1,-3 \) respectively as we saw earlier.
Extending the idea of components of a vector to components of a plane
e1e2(2,1)(1,1)e2e3(1,1)(1,2)e1e3(2,1)(1,2)
Projections of the parallelogram created by vectors \( \begin{bmatrix} 2 \\ 1 \\ 1 \end{bmatrix} \)\(\text{,}\)\( \begin{bmatrix} 1 \\ 1 \\ 2 \end{bmatrix} \) on the \( e_1e_2, e_2e_3, e_1e_3 \) planes with magnitudes \( 1,1,3 \).
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)
'''
5.3 Connection with imaginary number \( i \)
The square of the bivector \( e_1e_2 \) is \( (e_1e_2)^2 = (e_1e_2)(e_1e_2) = e_1e_2(-e_2e_1) = -e_1(e_2^2)e_1 = -e_1^2 = -1 \). Thus the bivector \( e_1e_2 \) behaves like the imaginary number \( i \). This is a key insight that connects geometric algebra with complex numbers.
Indeed one can see that \( (xe_1 + ye_2)(cos \theta + sin \theta e_{12}) = (xcos \theta - ysin \theta)e_1 + (xsin \theta + ycos \theta)e_2 \). But what does it mean to multiply a vector by a bivector an element of the form scalar + bivector? Note that the bivector \( sin \theta e_{12} \) is a bivector in xy plane with magnitude \( sin \theta \) and a positive orientation. Let's understand the tuple of a scalar and a bivector, \( cos \theta + sin \theta e_{12} \), further.
5.3.1 Rotation in 2D
Given any two unit vectors \( p,q \) in the 2D plane with an angle of \( θ \) between them, we can see that:
\( p.q = cos \theta \)
\( p \wedge q = sin \theta \ e_1e_2 \): this is because the (signed) area of the parallelogram spanned by two unit vectors is equal to the (signed) area between them.
\( pq = p.q + p \wedge q = cos \theta + sin \theta e_1e_2 \) which is the quantity we just used above to rotate a vector.
For example, to rotate a vector \( \begin{bmatrix} x \\ y \end{bmatrix} \) by 90°, all of the choices below work:
\( p \ = \ 1e_1 + 0e_2, \ q \ = \ 0e_1 + 1e_2 \)
\( p \ = \ (1/√2)e_1 + (1/√2)e_2, \ q \ = \ -(1/√2)e_1 + (1/√2)e_2 \)
\( p \ = \ 0e_1 + 1e_2, \ q \ = \ -1e_1 + 0e_2 \)
In each case, \( pq = e_1e_2 = (cos \pi/2) + (sin \pi/2) e_1e_2 \).
Also note that if \( pq \) rotates a vector by \( θ \), then \( qp = cos \theta - sin \theta e_1e_2 \) rotates the vector by \( -θ \). This is because \( p^2 = q^2 = 1 \Rightarrow (pq)(qp) = 1 \Rightarrow qp = (pq)^{-1} \) (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 \( v(pq) \) rotates the vector \( v \) counterclockwise by \( θ \) while \( (pq)v \) rotates the vector clockwise by \( θ \). It also means that \( v(pq) = (qp)v = (pq)^{-1}v \).
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)
'''
5.4 Connection with quaternions
We just saw that \( (e_1e_2)^2 = -1 \). Using the same reasoning, we can show that \( (e_2e_3)^2 = -1 \) and \( (e_1e_3)^2 = -1 \). Thus the bivectors \( e_1e_2, e_2e_3, e_1e_3 \) behave like the imaginary numbers \( i, j, k \) respectively. You can also ensure that \( ijk = (e_1e_2)(e_2e_3)(e_1e_3) = -1 \).
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.
5.5 Describing a rotation in 3D
We start by generalizing how vectors were being rotated in a 2D plane - we had a bivector \( B = e_1e_2 \) of magnitude 1 and another vector \( v \) in the same plane. Then we saw that \( R = cos \theta + sin \theta B \) rotated the vector \( v \) by an angle \( θ \). This pattern actually works for any plane in a higher dimensional space. Given a unit bivector \( B \) and a vector \( v \) in the same plane, the quantity \( vR, \ R = cos \theta + sin \theta B \) rotates the vector \( v \) by an angle \( θ \) in the plane defined by \( B \). 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 \( B \). We will see how to overcome this limitation in the next section.
To show why, let's use a small trick - define the quantity \( R = cos \theta + sin \theta B \) as a product of two unit vectors in two different ways. We pick vectors \( p,q,r \) in the same plane as the bivector \( B \) such that the angle between \( p,q \) and the angle between \( q,r \) is \( θ \). Then we can write:
\( B = pq = qr \) because \( p.q = q.r, \ p \wedge q = q \wedge r \)
We can represent any vector in the plane defined by \( B \) as a linear combination of the basis vectors \( p,q \). So if we show that \( pR \) rotates the vector \( p \) by \( θ \) and \( qR \) rotates the vector \( q \) by \( θ \), then we have shown that \( vR \) rotates the vector \( v \) by \( θ \) in the plane defined by \( B \). Now we have:
\( pR = p(pq) = q \)
\( qR = q(qr) = r \)
Thus, \( p \) got rotated by \( θ \) to \( q \) and \( q \) got rotated by \( θ \) to \( r \). Thus, \( vR, \ v = \alpha p + \beta q \Rightarrow vR = \alpha q + \beta r \) rotates the vector \( v \) by \( θ \) in the plane defined by \( B \).
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 \( B \), 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)
'''
5.6 Describing a reflection
We cover reflection about a line first. Let's say the reflection of a vector \( v \) about a line defined by a unit vector \( u \) is \( v_r \). This reflection can be seen as a rotation of \( 2θ \) where \( θ \) is the angle from \( v \) to \( u \). This rotation happens in the plane defined by \( u \) and \( v \). Thus, we can describe this reflection as:
\( v_r = ||v|| v_{unit}(v_{unit}u)(v_{unit}u) = ||v||uv_{unit}u = uvu \)
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:
Reflection about a line: keep the component along the line the same and negate the component perpendicular to the line.
Reflection about a hyperplane: keep the component along the hyperplane the same and negate the component perpendicular to the hyperplane.
As you can see, these two are exactly opposite operations. Thus, the reflection about a hyperplane represented by a normal vector \( u \) is given by:
\( v_r = -uvu \)
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.
5.7 Describing a rotation using reflections
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 \( m \) and the second reflection is about a line defined by a unit vector \( n \), then the composition of these two reflections is a rotation by an angle \( 2θ \) where \( θ \) is the angle between \( m \) and \( n \). 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:
\( v_{rotated} = n(mvm)n = (nm)v(mn) \)
If we reflected the vector around the planes represented by the vectors \( m \) and \( n \), we would still have gotten the same result because:
\( v_{rotated} = -n(-mvm)n = (nm)v(mn) \)
Now that \( mn \) and \( nm \) are inverses of each other. Thus if we define \( R = mn \), we can write the rotation as:
\( v_{rotated} = RvR^{-1} \)
If we want to rotate \( v \) by an angle of \( \theta \), we just need to find unit vectors \( m,n \) which have an angle of \( \theta/2 \) 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!
'''
Two 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)
'''
5.8 Number of free variables for 3D rotation
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.
Euler angles: 3 angles \( \phi, \theta, \psi \) corresponding to the three rotations.
Axis-angle representation: two values to describe the unit vector and one value to describe the angle.
Quaternions: Same as the axis-angle representation.
Geometric algebra: The plane of rotation is described by two unit vectors. The angle of rotation is described by the angle between them. Each unit vector needs two free variables. So it may seem like we need four variables here. But the fact that their dot product is equal to \( cos \theta \) puts an extra constraint and thus we need only three variables.
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.