This notebook is part of the clifford documentation: https://clifford.readthedocs.io/.
In this notebook, we give a more detailed look at how to use clifford, using the algebra of three dimensional space as a context.
First, we import clifford as cf, and instantiate a three dimensional geometric algebra using Cl() (docs).
In [1]:
    
import clifford as cf
layout, blades = cf.Cl(3)  # creates a 3-dimensional clifford algebra
    
Given a three dimensional GA with the orthonormal basis,
$$e_{i}\cdot e_{j}=\delta_{ij}$$The basis consists of scalars, three vectors, three bivectors, and a trivector.
$$\{\hspace{0.5em} \underbrace{\hspace{0.5em}\alpha,\hspace{0.5em}}_{\mbox{scalar}}\hspace{0.5em} \underbrace{\hspace{0.5em}e_{1},\hspace{1.5em}e_{2},\hspace{1.5em}e_{3},\hspace{0.5em}}_{\mbox{vectors}}\hspace{0.5em} \underbrace{\hspace{0.5em}e_{12},\hspace{1.5em}e_{23},\hspace{1.5em}e_{13},\hspace{0.5em}}_{\mbox{bivectors}}\hspace{0.5em} \underbrace{\hspace{0.5em}e_{123}\hspace{0.5em}}_{\text{trivector}} \hspace{0.5em} \}$$Cl()  creates the algebra and returns a layout and blades. The layout holds information and functions related this instance of G3,  and the blades is a dictionary which contains the basis blades, indexed by their string representations,
In [ ]:
    
blades
    
You may wish to explicitly assign the blades to variables like so,
In [ ]:
    
e1 = blades['e1']
e2 = blades['e2']
# etc ...
    
Or, if you're lazy and just working in an interactive session you can use locals() to update your namespace with all of the blades at once.
In [ ]:
    
locals().update(blades)
    
Now, all the blades have been defined in the local namespace
In [ ]:
    
e3, e123
    
The basic products are available
In [ ]:
    
e1*e2  # geometric product
    
In [ ]:
    
e1|e2  # inner product
    
In [ ]:
    
e1^e2  # outer product
    
In [ ]:
    
e1^e2^e3  # even more outer products
    
Python's operator precedence makes the outer product evaluate after addition. This requires the use of parentheses when using outer products. For example
In [ ]:
    
e1^e2 + e2^e3  # fail, evaluates as
    
In [ ]:
    
(e1^e2) + (e2^e3)  # correct
    
Also the inner product of a scalar and a Multivector is 0,
In [ ]:
    
4|e1
    
So for scalars, use the outer product or geometric product instead
In [ ]:
    
4*e1
    
Multivectors can be defined in terms of the basis blades. For example you can construct a rotor as a sum of a scalar and bivector, like so
In [ ]:
    
from scipy import cos, sin  
theta = pi/4
R = cos(theta) - sin(theta)*e23
R
    
You can also mix grades without any reason
In [ ]:
    
A = 1 + 2*e1 + 3*e12 + 4*e123
A
    
The reversion operator is accomplished with the tilde ~ in front of the Multivector on which it acts
In [ ]:
    
~A
    
Taking a projection onto a specific grade $n$ of a Multivector is usually written
$$\langle A \rangle _n$$can be done by using soft brackets, like so
In [ ]:
    
A(0)  # get grade-0 elements of R
    
In [ ]:
    
A(1)  # get grade-1 elements of R
    
In [ ]:
    
A(2)  # you get it
    
Using the reversion and grade projection operators, we can define the magnitude of $A$
$$|A|^2 = \langle A\tilde{A}\rangle$$
In [ ]:
    
(A*~A)(0)
    
This is  done in the abs() operator
In [ ]:
    
abs(A)**2
    
The inverse of a Multivector is defined as $A^{-1}A=1$
In [ ]:
    
A.inv()*A
    
In [ ]:
    
A.inv()
    
The dual of a multivector $A$ can be defined as $$AI^{-1}$$
Where, $I$ is the pseudoscalar for the GA. In $G_3$, the dual of a vector is a bivector,
In [ ]:
    
a = 1*e1 + 2*e2 + 3*e3 
a.dual()
    
You can toggle pretty printing with with pretty()  or ugly(). ugly returns an eval-able string.
In [ ]:
    
cf.ugly()
A.inv()
    
You can also change the displayed precision
In [ ]:
    
cf.pretty(precision=2)
A.inv()
    
This does not effect the internal precision used for computations.
Reflecting a vector $c$ about a normalized vector $n$ is pretty simple,
$$ c \rightarrow ncn$$
In [ ]:
    
c = e1+e2+e3    # a vector
n = e1          # the reflector
n*c*n          # reflect `a` in hyperplane normal to `n`
    
Because we have the inv() available, we can equally well reflect in un-normalized vectors using, 
$$ a \rightarrow nan^{-1}$$
In [ ]:
    
a = e1+e2+e3    # the vector
n = 3*e1          # the reflector
n*a*n.inv()
    
Reflections can also be made with respect to the a 'hyperplane normal to the vector $n$', in this case the formula is negated $$c \rightarrow -ncn^{-1}$$
A vector can be rotated using the formula $$ a \rightarrow Ra\tilde{R}$$
Where $R$ is a rotor. A rotor can be defined by multiple reflections,
$$R=mn$$or by a plane and an angle,
$$R = e^{-\frac{\theta}{2}\hat{B}}$$For example
In [ ]:
    
import math
R = math.e**(-math.pi/4*e12) # enacts rotation by pi/2 
R
    
In [ ]:
    
R*e1*~R    # rotate e1 by pi/2 in the e12-plane
    
Maybe we want to define a function which can return rotor of some angle $\theta$ in the $e_{12}$-plane,
$$ R_{12} = e^{-\frac{\theta}{2}e_{12}} $$
In [ ]:
    
R12 = lambda theta: e**(-theta/2*e12)
R12(pi/2)
    
And use it like this
In [ ]:
    
a = e1+e2+e3
R = R12(math.pi/2)
R*a*~R
    
You might as well make the angle argument a bivector, so that you can control the plane of rotation as well as the angle
$$ R_B = e^{-\frac{B}{2}}$$
In [ ]:
    
R_B = lambda B: math.e**(-B/2)
    
Then you could do
In [ ]:
    
R12 = R_B(math.pi/4*e12)
R23 = R_B(math.pi/5*e23)
    
or
In [ ]:
    
R_B(math.pi/6*(e23+e12))  # rotor enacting a pi/6-rotation in the e23+e12-plane
    
Maybe you want to define a function which returns a function that enacts a specified rotation,
$$f(B) \rightarrow \underline{R_B}(a) = R_Ba\tilde{R_B}$$This just saves you having to write out the sandwich product, which is nice if you are cascading a bunch of rotors, like so $$ \underline{R_C}( \underline{R_B}( \underline{R_A}(a)))$$
In [ ]:
    
def R_factory(B):
    def apply_rotation(a):
        R = math.e**(-B/2)
        return R*a*~R
    return apply_rotation
    
R = R_factory(pi/6*(e23+e12))  # this returns a function 
R(a)  # which acts on a vector
    
Then you can do things like
In [ ]:
    
R12 = R_factory(math.pi/3*e12)
R23 = R_factory(math.pi/3*e23)
R13 = R_factory(math.pi/3*e13)
R12(R23(R13(a)))
    
To make cascading a sequence of rotations as concise as possible, we could define a function which takes a list of bivectors $A,B,C,..$ , and enacts the sequence of rotations which they represent on a some vector $x$.
$$f(A,B,C,x) = \underline{R_A} (\underline{R_B} (\underline{R_C}(x)))$$
In [ ]:
    
from functools import reduce  
# a sequence of rotations
def R_seq(*args):
    *Bs, x = args
    R_lst = [math.e**(-B/2) for B in Bs]  # create list of Rotors from list of Bivectors
    R = reduce(cf.gp, R_lst)          # apply the geometric product to list of Rotors
    return R*x*~R
# rotation sequence by  pi/2-in-e12 THEN pi/2-in-e23
R_seq(pi/2*e23, pi/2*e12, e1)
    
If you want to use different names for your basis as opposed to e's with numbers, supply the Cl() with a list of names. For example for a two dimensional GA,
In [ ]:
    
layout,blades = cf.Cl(2, names=['','x','y','i'])
blades
    
In [ ]:
    
locals().update(blades)
    
In [ ]:
    
1*x + 2*y
    
In [ ]:
    
1 + 4*i