Basis Blades

Summary: Basis blades span the entire space. They are implemented using binary numbers.
from dataclasses import dataclass, field
from collections import namedtuple

What’s a Basis?

A Basis is a finite set \(\mathcal{B} = \{\vec{b_1}, \vec{b_2}, ..., \vec{b_n}\}\) of vectors in a vector space \(\vec{V}\). \(\mathcal{B}\) is a basis for \(\vec{V}\) if every vector in \(\vec{V}\) is a linear combination of the vectors in \(\mathcal{B}\) in one and only one way 1.

A vector space, \(V\), is a set (or collection) of elements that can be scaled and linearly added together. See Abstract Vectors Spaces by 3Blue1Brown.

I recommend watching 3Blue1Brown’s explanation of Bases & Linear Combinations for a more intuitive explanation.

Note that orthogonal bases are denoted \(e_i\) instead of \(\vec{b}_i\).

In Geometric Algebra, we have \(2^n\) basis blades (\(k\)-dimensional vectors) for a \(n\)-dimensional vector space, instead of just \(n\) basis vectors like in Linear Algebra.

For example, a 3D vector space in Linear Algebra has three basis vectors: \(\hat{i}, \hat{j}, \hat{k}\).

However, in Geometric Algebra a 3D vector space has 8 basis \(k\)-blades:

  • 0-blades (scalar): \(1\)
  • 1-blades (vectors): \(e_1, e_2, e_3\) (these correspond with \(\hat{i}, \hat{j}, \hat{k}\))
  • 2-blades (bivectors): \(e_{12}, e_{13}, e_{23}\)
  • 3-blades (trivectors): \(e_{123}\)

Implementation

Notice that every basis blade either contains a basis vector \(e_i\) or it doesn’t. Consider the GA 3D vector space; \(e_{12}\) contains \(e_1\) and \(e_2\), but doesn’t contain \(e_3\). In other words, we can encode every basis blade by the presence or absence of each basis 1-vector. We use a bit to encode presence (1) or absence (0), and denote the basis 1-vector by the position of each bit:

\[ {0 \atop e3} {0 \atop e2} {0 \atop e1} \]

Therefore the whole space of 3D is encoded like this:

\[\begin{array}{cccc} . & e_3 & e_2 & e_1 \\ 1 (\text{scalar}) & 0 & 0 & 0 \\ e_1 & 0 & 0 & 1 \\ e_2 & 0 & 1 & 0 \\ e_3 & 1 & 0 & 0 \\ e_{12} & 0 & 1 & 1 \\ e_{13} & 1 & 0 & 1 \\ e_{23} & 1 & 1 & 0 \\ e_{123} & 1 & 1 & 1 \\ \end{array} \]

In general, we need \(2^n\) bits to represent all basis blades of a \(n\)-dimensional space.

For simplicity, we’ll stick to a 2 dimensional vector space for our implementation. In 2D we have 4 basis \(k\)-blades: \(1\) (unit scalar), \(e1\), \(e2\), and the unit bivector, \(e_{12}\).

Computers store numbers in binary already, but Python defaults to printing numbers out in decimal. We can write binary numbers in Python as a string or integer, so we choose integer for convenience.

# Binary string - must explicity convert to int
print('0b101', int('0b101', 2))
# Binary integer
print(0b101)
0b101 5
5

We could use a list to store all the binary numbers for each basis, but namedtuples are more readable.

Plus, when we import basis blades into other notebooks, we can import the tuple instead of each individual basis blade.

GA2D = namedtuple('GA2D', ['scalar', 'e1', 'e2', 'e12'])
ga2d = GA2D(0b00, 0b01, 0b10, 0b11)
ga2d
GA2D(scalar=0, e1=1, e2=2, e12=3)
# For convenience, we unpack the basis blades into the global scope
scalar, e1, e2, e12 = ga2d

Weighted Basis Blades

While basis blades are defined with a weight of 1.0, we could associate a custom weight value to each basis blade using a class. This makes it easier to implement multivectors in later notebooks, since multivectors are a sum of weighted basis blades.

In our Python implementation, we group together the weight and basis data using a dataclass.

@dataclass(frozen=True)
class BasisBlade:
    """A BasisBlade is a fundamental element of a vector space"""
    # --- Class variables (defined without type hint); shared by all BasisBlades
    bit_bases2d = {0b0: 's', 0b1: 'e1', 0b10: 'e2', 0b11: 'e12'}

    # --- Instance variables; made "immutable" by frozen=True
    # a.k.a scale or magnitude associated with blade
    weight: float = field(default=0.)
    # integer encoding 
    basis: int = field(default=0)
    # basis_name depends on basis, so it can only be set post-initialization
    basis_name: str = field(init=False)

    def __post_init__(self):
        # Update the "immutable" basis_name field
        object.__setattr__(self, 'basis_name', self.bit_bases2d.get(self.basis))

    def __str__(self) -> str:
        """Pretty print like math notation"""
        prettyprint = f'{self.weight if self.weight != 1 else ""}{self.basis_name}'
        return prettyprint

BasisBlade

 BasisBlade (weight:float=0.0, basis:int=0)

A BasisBlade is a fundamental element of a vector space

The dataclass decorator automagically implements useful methods for our BasisBlade class like __eq__, __repr__, __init__, and more. See this mCoding video for an overview, and the dataclasses docs for details.

Also note that bit_bases2d is a class variable which means every instance of this class shares the same bit_bases2d. Contrast this with the instance variables weight, basis, and basis_name.

Finally, note that we use object.__set_attr__(...) to modify the BasisBlade.basis_name attribute, despite the class being “frozen”. This is the only time a BasisBlade attribute needs to change post-initialization. See this StackOverflow post for more details.

# example basis blade
BasisBlade(4.0, e1)
BasisBlade(weight=4.0, basis=1, basis_name='e1')

The BasisBlade class supports pretty-printing like math notation.

# Weight and basis
print(BasisBlade(4.0, e1))
# If weight=1. than only basis shown
print(BasisBlade(1.0, e12))
# 's' (for scalar) helps distinguish BasisBlade class instance from Python floats
print(BasisBlade(-12., scalar))
4.0e1
e12
-12.0s
# dataclass automagically implements __eq__ method for us
BasisBlade(-3.4, e2) == BasisBlade(-3.4, e2)
True

Besides __eq__, no other operators are overloaded! This keeps the BasisBlade class small, and makes the implementation of operators simpler to understand.

Of course, lack of operator-overloading makes our implementation more inconvenient to use. For a fully-featured, operator-overloaded Geometric Algebra library see Clifford.

# We cannot multiply floats and BasisBlades using '*' without operator overloading!
try:
    3.4 * BasisBlade(1., e1)
except TypeError as err:
    print(err)
unsupported operand type(s) for *: 'float' and 'BasisBlade'

Readings

Geometric Algebra Primer (Suter, 2003)

  • Chapter 2.3 Basis Blades

What’s Next?

Next, we implement multivectors.