Products

Summary: The geometric product is the sum of the inner and outer product. It is the foundation of Geometric Algebra.
from geomalgebra.basisblades import ga2d, BasisBlade
from geomalgebra.multivectors import Multivector, add
# For convenience, we unpack the basis blades into the global scope
scalar, e1, e2, e12 = ga2d

The Geometric Product

We have seen the dot product and the wedge product, but the most important product in GA is the geometric product.

For arbitrary vectors (1-blades), \(a\) and \(b\), the geometric product is:

\[ ab = a \cdot b + a \wedge b \]

Although we saw what the outer product does geometrically, we glossed over it’s properties. The outer product is associative and distributitve, and also \(\vec{a} \wedge \vec{a} = 0\) (extending \(\vec{a}\) along itself produces no area!) and \(\vec{a} \wedge \vec{b} = - \vec{a} \wedge \vec{b}\) (orientation matters, like we saw earlier).

The dot products works like it does in linear algebra. Perpendicular vectors have a dot product of \(0\), and parallel vectors have a dot product of \(1\).

To better understand the geometric product, try the following questions on your own. If you get stuck, you can reveal a walkthrough of the answer.

  1. What is \(e_1 e_2\)?

\[\begin{aligned} e_1 e_2 &= e_1 \cdot e_2 + e_1 \wedge e_2 \newline &= 0 + e_1 \wedge e_2 & \text{Dot product of perpendicular vectors is zero} \newline &= e_1 \wedge e_2 = e_{12} \newline \end{aligned} \]

The geometric product of two (non-parallel) vectors is a bivector.

  1. Find \(e_1 e_1\) (i.e. \(e_1^2\)):

\[\begin{aligned} e_1 e_1 &= e_1 \cdot e_1 + e_1 \wedge e_1 \newline &= 1 + e_1 \wedge e_1 & \text{Dot product of paralell vectors is one} \newline &= 1 + 0 & \text{Outer/Wedge product of parallel vectors is zero} \newline &= 1 \end{aligned} \]

Interesting. Here we have an entity, \(e_1\) that is distinct from the real number \(1\), but that squares to 1.

  1. Last example, calculate \(e_{12}e_{12}\):

\[\begin{aligned} e_{12} e_{12} &= e_1 e_2 e_1 e_2 \newline &= - e_2 e_1 e_1 e_2 & e_{12} = e_1\wedge e_2 = -e_2 \wedge e_1 = -e_{21} \newline &= - e_2 e_1^2 e_2 & e_i^2 = 1 \newline &= - e_2 e_2 = - e_2^2 \newline &= -1 \end{aligned} \]

Also very interesting. We have an entity that squares to -1, like an imaginary number.

See Imaginary Numbers Are Not Real for more on this result, space-time physics, and some history of Geometric Algebra.

Implementation for BasisBlades

Based on the calculations above, \(e_i e_i\) cancels out the basis \(e_i\), and \(e_i e_j\) (\(i \neq j\)) retains both bases.

This is pretty similar to the XOR bitwise function. If you have 1 XOR 1, it cancels to 0. Also two different bits, like 1 XOR 0, will become 1.

^ is the XOR operator in Python.

For example, \(e_1 e_{12}\) as an XOR looks like:

\[ \begin{array}{ccc} e_1 & 0 & 1 \\ e_{12} & 1 & 1 \\ \hline e_2 & 1 & 0 \end{array} \]

# Correct
print(BasisBlade.bit_bases2d.get(e1 ^ e2))
# Correct
print(BasisBlade.bit_bases2d.get(e1 ^ e1))
# Incorrect! Wrong sign! Should be -s (i.e. -1.0 scalar)
print(BasisBlade.bit_bases2d.get(e12 ^ e12))
e12
s
s

The XOR is close, but isn’t quite right. When manipulating the vectors algebraically we made sure to change the sign when swapping the order of operands (\(e_1 e_2 = -e_2 e_1\)).

But how do we encode the sign change with the bit-representation of basis blades? First let’s assume canonical order is postive (canonical order is when the subscript of the bases are in increasing order).

Then if we are presented with bases out of canonical order, say \(e_2 e_1\), we can determine that the first basis is canonically greater than the second by counting how many \(1\) s in the first basis are in a greater position than \(1\) s in the second basis.

For example,

\[\begin{array}{ccc} e_2 & 1 & 0 \\ e_1 & 0 & 1 \\ \end{array} \]

The first number, \(e_2\), has a \(1\) in a greater position than the second number, \(e_1\). Therefore, flipping the order of these bases once will put the numbers in canonical order. Every flip changes the sign, which means the geometric product of these bases is negative.

Another example,

\[ \begin{array}{ccc} e_2 & 1 & 0 \\ e_{12} & 1 & 1 \\ \end{array} \]

Again, there is a \(1\) in the first number, \(e_2\), ahead of a \(1\) in the second number, so the answer is negative.

A computer can’t visually inspect binary numbers like we can, but we can simulate the same logic by sliding bits in the the first number towards the right, and using the bitwise AND operator and a bit_count function to count how many \(1\) s are ahead of the \(1\) s in the second number after each slide. Effectively, we count how many bases in the first number are greater than the bases in the second number, thereby informing us of how many sign changes to make.

Starting from a positive number, an odd number of sign changes yields a negative number and an even number of sign changes yields a positive.
def canonical_sign(basis_1: int, basis_2: int) -> int:
    """Count the number of basis blade swaps required to get 'a' and 'b' in canonical order
    Canonical order means increasing order is positive, i.e: e1^e2 is positive, e2^e1 is negative
    """
    basis_1 = basis_1 >> 1
    num_swaps = 0
    while basis_1:
        # Count how many bases in basis_1 are canonically greater than those in basis_2
        num_swaps += int.bit_count(basis_1 & basis_2)
        basis_1 = basis_1 >> 1
    num_swaps_is_odd = (num_swaps & 1) == 0
    return 1. if (num_swaps_is_odd) else -1.

canonical_sign

 canonical_sign (basis_1:int, basis_2:int)

Count the number of basis blade swaps required to get ‘a’ and ‘b’ in canonical order Canonical order means increasing order is positive, i.e: e1^e2 is positive, e2^e1 is negative

assert canonical_sign(ga2d.e1, ga2d.e2) == 1.
assert canonical_sign(ga2d.e2, ga2d.e1) == -1.

Scalar multiplication works like usual, so the geometric product for basis blades is:

def basis_blade_gp(b1: BasisBlade, b2: BasisBlade):
    """Geometric Product for basis blades"""
    sign = canonical_sign(b1.basis, b2.basis)
    return BasisBlade(sign * b1.weight * b2.weight, b1.basis ^ b2.basis)

basis_blade_gp

 basis_blade_gp (b1:geomalgebra.basisblades.BasisBlade,
                 b2:geomalgebra.basisblades.BasisBlade)

Geometric Product for basis blades

# Basis Bivector squares to -1 under the geometric product - just like imaginary numbers!
print(basis_blade_gp(BasisBlade(1., e12), BasisBlade(1., e12)))
-1.0s

Implementation for Multivectors

The geometric product for multivectors is:

  1. associative: \(A(BC) = (AB)C\)
  2. commutative under scalar multiplication: \(\lambda A = A \lambda\) (where \(\lambda\) is a scalar)
  3. distributive: \(A(B + C) = AB + AC\)

where \(A, B, C\) are multivectors.

In terms of implementation, this just means we need to multiply all of the terms of one multivector by all of the terms of the other multivectors. It’s really just the same as Polynomial multiplication.

For simplicity, we won’t try to implement any fancy multiplication algorithm that runs better than \(O(n^2)\).

def gp(m1: Multivector|BasisBlade, m2: Multivector|BasisBlade) -> Multivector:
    """Geometric product for Multivectors and Basis Blades"""
    basis_blades: list[BasisBlade] = list()
    if isinstance(m1, BasisBlade): m1 = Multivector([m1])
    if isinstance(m2, BasisBlade): m2 = Multivector([m2])
    for basis_1 in m1.blades.values():
        for basis_2 in m2.blades.values():
            basis_blades = [*basis_blades, basis_blade_gp(basis_1, basis_2)]
    return Multivector(basis_blades)

gp

 gp (m1:geomalgebra.multivectors.Multivector|geomalgebra.basisblades.Basis
     Blade, m2:geomalgebra.multivectors.Multivector|geomalgebra.basisblade
     s.BasisBlade)

Geometric product for Multivectors and Basis Blades

m1 = Multivector([BasisBlade(2., scalar), BasisBlade(3., e1)])
m2 = Multivector([BasisBlade(4., scalar), BasisBlade(2., e1)])
print(f'({m1})({m2}) = {gp(m1, m2)}')
(2.0s + 3.0e1)(4.0s + 2.0e1) = 14.0s + 16.0e1

We can verify this result by hand:

\[\begin{aligned} (2 + 3e_1)(4 + 2e_1) &= (2)(4) + 2(2e_1) + 4(3e_1) + (3e_1)(2e_1) \newline &= 8 + 4e_1 + 12e_1 + 6 \newline &= 8 + 6 + 4e_1 + 12e_1 \newline &= 14 + 16e_1 \end{aligned} \]

m1 = Multivector([BasisBlade(-9., e12), BasisBlade(6., scalar)])
m2 = Multivector([BasisBlade(5, e2), BasisBlade(-2., e1)])
print(f'({m1})({m2}) = {gp(m1, m2)}')
(-9.0e12 + 6.0s)(5e2 + -2.0e1) = -57.0e1 + 12.0e2

Again, by hand:

\[\begin{aligned} (-9e_{12} + 6)(5e_2 - 2e_1) &= (-9e_{12})(5e_2) + (-9e_{12})(-2e_1) + 6(5e_2) + 6(-2e_1) \newline &= -45e_1 - 18e_2 + 30e_2 - 12e_1 \newline &= -45e_1 - 12e_1 + 30e_2 - 18e_2 \newline &= -57e_1 + 12e_2 \end{aligned} \]

Readings

Geometric Algebra Primer (Suter, 2003)

  • Chapter 3.1 The Geometric Product
  • Chapter 3.3 The Geometric Product Continued

What’s Next?

This is the end for now! Thank you for taking the time to learn from these tutorials :)

In the future, I plan to add more on applications of Geometric Algebra to geometry, physics, and mathematics.