from dataclasses import dataclass, field
from collections import namedtuple
Basis Blades
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.
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} \]
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 namedtuple
s are more readable.
Plus, when we import basis blades into other notebooks, we can import the tuple instead of each individual basis blade.
= namedtuple('GA2D', ['scalar', 'e1', 'e2', 'e12'])
GA2D = GA2D(0b00, 0b01, 0b10, 0b11)
ga2d ga2d
GA2D(scalar=0, e1=1, e2=2, e12=3)
# For convenience, we unpack the basis blades into the global scope
= ga2d scalar, e1, e2, e12
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
= {0b0: 's', 0b1: 'e1', 0b10: 'e2', 0b11: 'e12'}
bit_bases2d
# --- Instance variables; made "immutable" by frozen=True
# a.k.a scale or magnitude associated with blade
float = field(default=0.)
weight: # integer encoding
int = field(default=0)
basis: # basis_name depends on basis, so it can only be set post-initialization
str = field(init=False)
basis_name:
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"""
= f'{self.weight if self.weight != 1 else ""}{self.basis_name}'
prettyprint 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
4.0, e1) BasisBlade(
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
-3.4, e2) == BasisBlade(-3.4, e2) BasisBlade(
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.