Skip to main content

Numeric geometric algebra for Python — named functions, no ambiguity, unicode pretty-printing

Project description

ga — Geometric Algebra for Python

A numeric geometric algebra library with a stable, programmer-first API.

  • Named functions are the contractgp, op, grade, reverse, dual, inverse never change meaning
  • Operators are sugar*, ^, |, ~ are convenience only
  • No ambiguity — every inner product variant has its own name
  • Unicode pretty-printing3 + 2e₁ - e₃, γ₀γ₁, σₓσᵧ
  • Symbolic expression trees — write grade(R * v * ~R, 1) and see ⟨RvR̃⟩₁
  • Naming and evaluation.name("B"), .anon(), .lazy(), .eager() mutate in-place; .eval() returns a copy
  • LaTeX-driven naming.name(latex=r"\theta") auto-derives unicode (θ) and ASCII (theta)

Install

uv add numpy   # only dependency

Quick Start

from galaga import *

# Create a 3D Euclidean algebra
alg = Algebra((1, 1, 1))
e1, e2, e3 = alg.basis_vectors()

# Build multivectors naturally
v = 3*e1 + 4*e2
B = e1 ^ e2            # bivector
mv = 1 + 2*e1 + 3*B    # mixed grade

print(v)    # 3e₁ + 4e₂
print(B)    # e₁₂
print(mv)   # 1 + 2e₁ + 3e₁₂

Algebra Construction

The signature tuple defines each basis vector's square: +1, -1, or 0.

cl3 = Algebra((1, 1, 1))           # Cl(3,0) — 3D Euclidean
sta = Algebra((1, -1, -1, -1))     # Cl(1,3) — Spacetime Algebra
pga = Algebra((1, 1, 1, 0))        # Cl(3,0,1) — Projective GA

Constructors

alg.basis_vectors()              # (e₁, e₂, e₃) — named + eager
alg.basis_vectors(lazy=True)     # (e₁, e₂, e₃) — named + lazy (symbolic)
alg.pseudoscalar()               # e₁₂₃ (also alg.I)
alg.identity            # scalar 1 (𝟙)
alg.scalar(5.0)         # 5
alg.vector([1, 2, 3])   # e₁ + 2e₂ + 3e₃
alg.blade("e12")        # e₁₂
alg.rotor(B, radians=θ) # rotor for rotation by θ in plane B

Products

Every product has a definitive named function. Operators are optional shorthand.

Operation Function Operator Unicode
Geometric product gp(a, b) a * b juxtaposition
Outer (wedge) product op(a, b) a ^ b
Left contraction left_contraction(a, b)
Right contraction right_contraction(a, b)
Doran–Lasenby inner doran_lasenby_inner(a, b) a | b ·
Hestenes inner hestenes_inner(a, b) ·
Scalar product scalar_product(a, b)
Commutator commutator(a, b) [a, b]
Anticommutator anticommutator(a, b) {a, b}
Lie bracket lie_bracket(a, b) [a, b]
Jordan product jordan_product(a, b) {a, b}
e1, e2, e3 = alg.basis_vectors()

gp(e1, e2)                  # e₁₂
op(e1, e2)                  # e₁₂
left_contraction(e1, e1^e2) # e₂
scalar_product(e1, e1)      # 1

# Operator shorthand
e1 * e2     # geometric product
e1 ^ e2     # outer product
e1 | (e1^e2)  # Doran–Lasenby inner product

Unified Inner Product

The ip function dispatches to the specific inner product you want — no ambiguity.

ip(e1, e1)                            # Doran–Lasenby (default)
ip(e1, e1, mode="hestenes")           # Hestenes
ip(e1, e1 ^ e2, mode="left")          # left contraction
ip(e1 ^ e2, e2, mode="right")         # right contraction
ip(e1, e2, mode="scalar")             # scalar product

When Do the Inner Products Differ?

For vector-on-vector they all agree. The differences show up with mixed grades:

Expression Left contraction Right contraction Hestenes Doran–Lasenby
vector, bivector e₂ (grade 2−1=1) 0 (1−2 < 0) e₂ (|1−2|=1) e₂ (|1−2|=1)
bivector, vector 0 (1−2 < 0) e₂ (grade 2−1=1) -e₂ (|2−1|=1) -e₂ (|2−1|=1)
scalar, vector 3e₁ (passes through) 0 (0−1 < 0) 0 (kills scalars) 3e₁ (|0−1|=1)
vector, scalar 0 (1−0 < 0) 3e₁ (passes through) 0 (kills scalars) 3e₁ (|1−0|=1)
e12 = e1 ^ e2

left_contraction(e1, e12)      # e₂  — vector "removes" from bivector
left_contraction(e12, e1)      # 0   — can't remove higher from lower

right_contraction(e12, e1)     # e₂  — mirror of left contraction
right_contraction(e1, e12)     # 0

hestenes_inner(e1, e12)        # e₂  — uses |grade difference|
hestenes_inner(e12, e1)        # -e₂ — nonzero both ways (unlike left/right)
hestenes_inner(cl3.scalar(3), e1)  # 0 — always zero if either is scalar

doran_lasenby_inner(e1, e12)        # e₂  — same as Hestenes for non-scalars
doran_lasenby_inner(cl3.scalar(3), e1)  # 3e₁ — includes scalars (unlike Hestenes)

Rule of thumb: Doran–Lasenby (the | operator) is the most general — it includes scalars. Hestenes is the same but kills scalars. Left contraction is the most common in GA literature.

Unary Operations

Operation Function Operator Unicode
Reverse reverse(x) ~x
Grade involution involute(x)
Clifford conjugate conjugate(x)
B = e1 ^ e2

reverse(B)      # -e₁₂  (or ~B)
involute(B)     #  e₁₂  (even grade unchanged)
conjugate(B)    # -e₁₂  (reverse ∘ involute)

Grade Operations

mv = 3 + 2*e1 + (e1 ^ e2)

grade(mv, 0)    # 3
grade(mv, 1)    # 2e₁
grade(mv, 2)    # e₁₂
mv[0]           # 3       (shorthand for grade)
mv[1]           # 2e₁
mv[2]           # e₁₂
grades(mv, [0, 2])  # 3 + e₁₂

even_grades(mv)     # 3 + e₁₂  (grades 0, 2, ...)
odd_grades(mv)      # 2e₁      (grades 1, 3, ...)

scalar(mv)      # 3.0 (float)

You can also use grade() directly:

grade(mv, "even")   # same as even_grades(mv)
grade(mv, "odd")    # same as odd_grades(mv)

Norm, Unit, Inverse

v = 3*e1 + 4*e2

norm(v)         # 5.0
norm2(v)        # 25.0
unit(v)         # 0.6e₁ + 0.8e₂
normalize(v)    # same as unit(v)
inverse(v)      # v⁻¹ such that v * inverse(v) = 1

Convenience properties on multivectors:

v.inv           # inverse(v)
v.dag           # reverse(v)  — the "dagger"
v.sq            # gp(v, v)    — squared (also v**2)
v.scalar_part   # grade-0 coefficient as float
v.vector_part   # grade-1 coefficients as np.ndarray

Extract components for use with numpy, matplotlib, etc:

v = 3*e1 + 4*e2 + 5*e3
v.vector_part           # np.array([3., 4., 5.])
v.scalar_part           # 0.0

mv = 7 + v + (e1^e2)
mv.vector_part          # np.array([3., 4., 5.])
mv.scalar_part          # 7.0

Dual

dual(e1 ^ e2)    # e₃  (in 3D Euclidean)
undual(e3)       # e₁₂

Predicates

is_scalar(alg.scalar(5))    # True
is_vector(e1 + e2)          # True
is_bivector(e1 ^ e2)        # True
is_even(1 + (e1 ^ e2))      # True
is_rotor(R)                 # True (even-graded and R*~R = 1)

Rotation Example

Rotate e₁ by 90° in the e₁e₂ plane:

import numpy as np

theta = np.pi / 2
B = e1 ^ e2
R = alg.rotor(B, radians=theta)

v_rotated = R * e1 * ~R    # sandwich product
print(v_rotated)            # e₂

Or manually:

R = alg.scalar(np.cos(theta/2)) - np.sin(theta/2) * B

Exponential & Logarithm

exp(B) builds a rotor from a bivector directly — no manual cos/sin:

B = (np.pi / 4) * (e1 ^ e2)
R = exp(B)                      # cos(π/4) + sin(π/4) e₁₂
print(R * e1 * ~R)              # e₂ (90° rotation)

exp handles all signatures automatically:

  • Euclidean bivector (B² < 0): uses cos/sin
  • Timelike bivector (B² > 0): uses cosh/sinh (Lorentz boosts)
  • Null bivector (B² = 0): returns 1 + B (translations in PGA)

log(R) is the inverse — extract the bivector from a rotor:

B_back = log(R)                 # recovers the bivector
R_back = exp(log(R))            # roundtrip: R_back == R

Note: alg.rotor(B, radians=θ) computes exp(-θ/2 * B) for a unit bivector B.

Projection, Rejection, Reflection

v = 3*e1 + 4*e2 + 5*e3
plane = e1 ^ e2

project(v, plane)    # 3e₁ + 4e₂   (component in the plane)
reject(v, plane)     # 5e₃         (component perpendicular)

Projection and rejection always sum back to the original:

project(v, plane) + reject(v, plane) == v   # True

Reflection flips the component parallel to a normal vector:

reflect(e1 + e2, e1)    # -e₁ + e₂   (flip the e₁ part)
reflect(e2, e1)         #  e₂        (perpendicular: unchanged)
reflect(e1, e1)         # -e₁        (parallel: negated)

Double reflection is always identity: reflect(reflect(v, n), n) == v.

Spacetime Algebra

sta = Algebra((1, -1, -1, -1), names="gamma")
g0, g1, g2, g3 = sta.basis_vectors()

print(g0 * g0)      #  1   (timelike)
print(g1 * g1)      # -1   (spacelike)
print(g0 * g1)      # γ₀γ₁ (bivector)
print(sta.I)        # 𝑰

Basis Naming

Choose how basis vectors display — both in code (repr) and in print output (str).

Presets

# Default: e₁, e₂, e₃
alg = Algebra((1, 1, 1))

# Gamma: γ₀, γ₁, γ₂, γ₃
sta = Algebra((1, -1, -1, -1), names="gamma")

# Sigma (numbered): σ₁, σ₂, σ₃
pauli = Algebra((1, 1, 1), names="sigma")

# Sigma (xyz): σₓ, σᵧ, σz
pauli = Algebra((1, 1, 1), names="sigma_xyz")

Custom Names

Provide (code_names, unicode_names) tuples:

alg = Algebra((1, 1, 1), names=(["a", "b", "c"], ["𝐚", "𝐛", "𝐜"]))
a, b, c = alg.basis_vectors()
print(a + 2*b)      # 𝐚 + 2𝐛
print(repr(a + 2*b)) # a + 2b

Blade Lookup

Works with both default and custom names:

alg.blade("e12")         # e₁₂
sta.blade("g0g1")        # γ₀γ₁

Display

str() uses unicode, repr() uses ASCII by default:

mv = 3 + 2*e1 - e3

str(mv)     # '3 + 2e₁ - e₃'
repr(mv)    # '3 + 2e1 - e3'

To make repr() use unicode too (nicer in IPython/REPL), pass repr_unicode=True:

alg = Algebra((1, 1, 1), repr_unicode=True)
e1, e2, e3 = alg.basis_vectors()

repr(3*e1 + 4*e2)  # '3e₁ + 4e₂'

The pseudoscalar always displays as I / 𝑰:

print(alg.I)        # 𝑰
print(repr(alg.I))  # I  (or 𝑰 with repr_unicode=True)

Coefficients of ±1 are suppressed: e₁₂ not 1e₁₂, -e₃ not -1e₃.

Symbolic Expressions

Name multivectors and build expression trees that render as mathematical notation.

Naming and Evaluation

Any multivector can be named and made symbolic with .name(). All configuration methods (.name(), .anon(), .lazy(), .eager()) mutate in-place and return self. Only .eval() returns a new copy.

alg = Algebra((1, 1, 1))
e1, e2, e3 = alg.basis_vectors()

B = (e1 ^ e2).name("B")   # mutates, sets lazy
print(B)              # B
print(B.anon())       # e₁₂  (name removed in-place)

B.name("B")           # re-name it
print(B.eval())       # e₁₂  (new copy, B unchanged)

LaTeX-driven naming

Pass latex= and unicode/ASCII are derived automatically:

theta = alg.scalar(0.5).name(latex=r"\theta")
# unicode: θ, ascii: theta, latex: \theta — all auto-derived

F = (e1 ^ e2).name(latex=r"\mathbf{F}")
# unicode: 𝐅, ascii: F — derived from \mathbf{F}

n = e1.name(latex=r"\hat{n}")
# unicode: n̂, ascii: hat_n — combining accent

The label parameter is optional when latex is provided. User-supplied unicode= and ascii= always take precedence over auto-derivation.

Naming and evaluation are orthogonal axes:

Anonymous Named
Eager e1 + e2e₁ + e₂ e1e₁ (basis blades)
Lazy B.anon() → expr tree B = (e1^e2).name("B")B

Lazy Propagation

When a lazy value participates in an operation, the result is lazy:

B = (e1 ^ e2).name("B")
x = B + e3
print(x)              # B + e₃  (symbolic)
print(x.eval())       # e₁₂ + e₃  (concrete)

Names don't propagate — the result is anonymous but named operands appear by name in the expression tree.

Lazy Basis Blades

For fully symbolic workflows, use lazy=True — every operation builds an expression tree automatically:

e1, e2, e3 = alg.basis_vectors(lazy=True)

e1 ^ e2              # e₁∧e₂  (not e₁₂)
e1 * e2              # e₁e₂
3 * e1 + e2          # 3e₁ + e₂
(e1 ^ e2).eval()     # e₁₂  (concrete when you need it)

Division renders as fractions, exp() renders symbolically:

theta = alg.scalar(0.5).name("θ")
B = (e1 ^ e2).name("B")
print(-B * theta / 2)     # -Bθ/2
print(exp(-B * theta / 2))  # exp(-Bθ/2)

sym() Compatibility

sym() still works as a convenience alias. Unlike .name(), it copies the original (does not mutate):

from galaga.symbolic import sym, grade, reverse, simplify

R = sym(e1 * e2, "R")   # copy of e1*e2, named "R"
v = sym(e1, "v")         # copy of e1, named "v" — e1 unchanged
v = sym(e1 + 2*e2, "v")

Rendering

R = (e1 * e2).name("R")
v = e1.name("v")

print(R * v * ~R)                     # RvR̃
print(grade(R * v * ~R, 1))           # ⟨RvR̃⟩₁

Full rendering table:

Expression Code Renders as
Geometric product R * v * ~R RvR̃
Wedge a ^ b a∧b
Left contraction a | b a⌋b
Right contraction right_contraction(a, b) a⌊b
Doran–Lasenby inner doran_lasenby_inner(a, b) A·B
Hestenes inner hestenes_inner(a, b) A·B
Scalar product scalar_product(a, b) A∗B
Reverse ~R
Involute involute(v)
Conjugate conjugate(v)
Dual dual(v) v⋆
Undual undual(v) v⋆⁻¹
Norm norm(v) ‖v‖
Unit unit(v)
Inverse v.inv v⁻¹
Grade projection grade(A * B, 2) ⟨AB⟩₂
Even grades even_grades(A) ⟨A⟩₊
Odd grades odd_grades(A) ⟨A⟩₋
Squared squared(R) or R.sq
Addition a + b a + b
Scalar multiply 3 * a 3a

Evaluation

Every lazy multivector can be evaluated to its concrete form:

expr = grade(R * v * ~R, 1)
print(expr)          # ⟨RvR̃⟩₁
print(expr.eval())   # concrete Multivector result

.eval() returns a new anonymous eager copy (non-mutating). .eager() mutates in-place.

LaTeX Output

Every expression has a .latex() method for use in documents, notebooks, and markdown:

expr = grade(R * v * ~R, 1)
expr.latex()            # \langle R v \tilde{R} \rangle_{1}
expr.latex(wrap='$')    # $\langle R v \tilde{R} \rangle_{1}$
expr.latex(wrap='$$')   # $$\n...\n$$  (display block)

The wrap parameter is handy in f-strings for marimo/Jupyter markdown cells:

mo.md(f"{expr.latex(wrap='$')} = {expr.eval().latex(wrap='$')}")

In Jupyter notebooks, expressions render automatically via _repr_latex_().

Concrete Multivector objects also have .latex():

v = 3*e1 + 4*e2
v.latex()  # 3 e_{12}

Full LaTeX rendering table:

Expression Code Unicode LaTeX
Geometric product R * v * ~R RvR̃ R v \tilde{R}
Wedge a ^ b a∧b a \wedge b
Left contraction left_contraction(a, b) a⌋b a \;\lrcorner\; b
Right contraction right_contraction(a, b) a⌊b a \;\llcorner\; b
Doran–Lasenby inner a | b A·B A \cdot B
Hestenes inner hestenes_inner(a, b) A·B A \cdot B
Scalar product scalar_product(a, b) A∗B A * B
Reverse ~R \tilde{R}
Involute involute(v) v^\dagger
Conjugate conjugate(v) \bar{v}
Dual dual(v) v⋆ v^*
Undual undual(v) v⋆⁻¹ v^{*^{-1}}
Norm norm(v) ‖v‖ \lVert v \rVert
Unit unit(v) \hat{v}
Inverse v.inv v⁻¹ v^{-1}
Grade projection grade(A * B, 2) ⟨AB⟩₂ \langle A B \rangle_{2}
Even grades even_grades(A) ⟨A⟩₊ \langle A \rangle_{\text{even}}
Odd grades odd_grades(A) ⟨A⟩₋ \langle A \rangle_{\text{odd}}
Squared squared(R) or R.sq R^2
Addition a + b a + b a + b
Scalar multiply 3 * a 3a 3 a

Drop-in Functions

The symbolic module provides drop-in replacements for all ga functions. They detect lazy Multivector or Expr arguments and build trees; with plain eager Multivector arguments they delegate to the numeric core:

from galaga.symbolic import gp, grade, reverse

# With lazy/named MV → builds expression tree
R = (e1 * e2).name("R")
v = e1.name("v")
grade(R * v * ~R, 1)   # returns lazy Multivector with expr tree

# With eager MV → returns Multivector directly (zero overhead)
grade(e1 + e2, 1)      # returns eager Multivector

Notation

Override how operations render — per-algebra:

from galaga.notation import Notation, NotationRule

# Use Hestenes convention (reverse as dagger)
alg = Algebra((1, 1, 1), notation=Notation.hestenes())
e1, e2, _ = alg.basis_vectors(lazy=True)
v = e1.name("v")
print(reverse(v))   # v†

# Or override individual rules
alg.notation.set("Reverse", "unicode", NotationRule(kind="postfix", symbol="†"))
alg.notation.set("Dual", "unicode", NotationRule(kind="prefix", symbol="*"))

Built-in presets: Notation.default(), Notation.hestenes(), Notation.doran_lasenby().

Simplification

simplify() applies algebraic rewrite rules to expression trees:

from galaga.symbolic import sym, simplify, grade, norm, unit, inverse, op

alg = Algebra((1, 1, 1))
e1, e2, e3 = alg.basis_vectors()

v = sym(e1, "v")
R = sym(alg.rotor(e1^e2, radians=0.5), "R")
a = sym(e1, "a")
B = sym(e1^e2, "B")

simplify(~~v)              # v         (double reverse)
simplify(inverse(inverse(v)))  # v     (double inverse)
simplify(a ^ a)            # 0         (wedge self = 0)
simplify(a + a)            # 2a        (collection)
simplify(3 * (2 * v))      # 6v        (scalar collapse)
simplify(R * ~R)           # 1         (rotor normalization)
simplify(norm(unit(v)))    # 1         (unit has norm 1)
simplify(grade(v, 1))      # v         (v is known grade-1)
simplify(grade(v, 2))      # 0         (v has no grade-2)

simplify() accepts both Expr objects and lazy Multivector objects.

Grade is auto-detected from the multivector data, so sym(e1, "v") knows it's grade-1 and sym(e1^e2, "B") knows it's grade-2. Simplification runs to a fixed point, so cascading rules like a - (-a) → a + a → 2a resolve fully.

Sandwich Product

The sandwich product R x R̃ is common enough to deserve a shortcut:

sandwich(R, e1)     # R * e1 * ~R
sw(R, e1)           # same thing, short alias

Works in the symbolic layer too:

from galaga.symbolic import sandwich

R = (alg.rotor(e1^e2, radians=np.pi/2)).name("R")
v = e1.name("v")
print(sandwich(R, v))        # RvR̃
print(sandwich(R, v).eval()) # e₂

Aliases

Short names for experienced users, long names for readability:

Short Long
gp geometric_product
op wedge, outer_product
ip inner_product
rev reverse
unit normalize, normalise
sw sandwich
alg.rotor alg.rotor_from_bivector, alg.rotor_from_plane_angle

Recipes

Things you can build from the primitives — no extra functions needed.

Angle Between Vectors

angle = np.arctan2(norm(a ^ b), scalar(a | b))

Uses atan2 for numerical stability (works even when vectors are nearly parallel or perpendicular). The wedge magnitude is |a||b|sin θ, the inner product is |a||b|cos θ.

Check Parallel / Perpendicular

parallel      = np.isclose(norm(a ^ b), 0)    # wedge vanishes
perpendicular = np.isclose(scalar(a | b), 0)  # inner product vanishes

Compose Rotations

Rotors compose by geometric product — apply R1 first, then R2:

R_final = R2 * R1
v_rotated = R_final * v * ~R_final

Order matters: R2 * R1 means "do R1, then R2" (right-to-left, like matrix multiplication).

Interpolate a Rotation (SLERP)

R_half = exp(0.5 * log(R))       # 50% of the rotation
R_t    = exp(t * log(R))         # fraction t ∈ [0, 1]

log extracts the bivector, scaling it interpolates the angle, exp rebuilds the rotor.

Gram–Schmidt (Orthogonalize)

Make b orthogonal to a by removing the parallel component:

b_orth = reject(b, a)            # component of b perpendicular to a

For a full basis, chain rejections:

u1 = unit(a)
u2 = unit(reject(b, a))
u3 = unit(reject(reject(c, a), u2))

Rotate 90° Within a Plane

B = e1 ^ e2
R90 = alg.rotor(B, degrees=90)
perp = R90 * v * ~R90            # v rotated 90° in the e₁e₂ plane

Useful for finding the perpendicular direction within a subspace.

Area and Volume

The wedge product directly gives oriented area and volume:

area = norm(u ^ v)               # parallelogram area
vol  = norm(u ^ v ^ w)           # parallelepiped volume

These work in any dimension — norm(a ^ b) is the area of the parallelogram spanned by a and b, regardless of the ambient space.

Cross Product (3D Only)

cross = dual(a ^ b)              # a × b as a vector

The wedge gives a bivector (oriented plane); the dual converts it to the normal vector. Only meaningful in 3D where bivectors and vectors are dual.

Bivector Commutator Algebra

Bivectors form a Lie algebra under the commutator product:

commutator(e1^e2, e2^e3)         # 2e₁₃ — unnormalised (ab - ba)
lie_bracket(e1^e2, e2^e3)        # e₁₃  — normalised ½(ab - ba)

The library provides both conventions:

Function Definition Use case
commutator(a, b) ab - ba Raw commutator
anticommutator(a, b) ab + ba Raw anticommutator
lie_bracket(a, b) ½(ab - ba) Lie algebra with clean structure constants
jordan_product(a, b) ½(ab + ba) Symmetric product (equals inner product for vectors)

In 3D Euclidean space, the Lie bracket of bivectors is isomorphic to the vector cross product. In Cl(1,3), it gives the Lorentz algebra.

API Reference

Algebra(signature, names=None)

Method / Property Description
basis_vectors(lazy=False) Tuple of basis 1-vectors (lazy=True for symbolic)
pseudoscalar() Unit pseudoscalar
I Unit pseudoscalar (property)
identity Scalar 1
scalar(value) Scalar multivector
vector(coeffs) 1-vector from list
blade(name) Basis blade by name
rotor(B, radians=, degrees=) Rotor for rotation in plane B

Multivector

Property / Method Description
[k] Grade-k projection (x[2] = grade(x, 2))
.name(label, *, latex=, unicode=, ascii=) Set display name, set lazy (mutates). label optional if latex given.
.anon() Remove display name (mutates)
.lazy() Set lazy mode (mutates)
.eager() Force eager, strip name (mutates)
.eager("B") Force eager, keep/set name (mutates)
.eval() Return a new anonymous eager copy (non-mutating)
.inv Inverse
.dag Reverse (dagger)
.sq Squared (geometric product with self)
.scalar_part Grade-0 coefficient as float
.vector_part Grade-1 coefficients as np.ndarray
.algebra Parent algebra
.data NumPy coefficient array

Functions

Function Description
gp(a, b) Geometric product
op(a, b) Outer (wedge) product
left_contraction(a, b) Left contraction a ⌋ b
right_contraction(a, b) Right contraction a ⌊ b
hestenes_inner(a, b) Hestenes inner product
scalar_product(a, b) Scalar product
ip(a, b, mode=...) Unified inner product dispatcher
commutator(a, b) (ab - ba) / 2
anticommutator(a, b) (ab + ba) / 2
reverse(x) Reverse
involute(x) Grade involution
conjugate(x) Clifford conjugate
grade(x, k) Grade-k projection
grades(x, ks) Multi-grade projection
scalar(x) Extract scalar coefficient
dual(x) Dual
undual(x) Undual
norm(x) √|x x̃|
norm2(x) ⟨x x̃⟩₀
unit(x) Normalize to unit
inverse(x) Versor inverse
squared(x) — geometric product with self
sandwich(r, x) Sandwich product r x r̃
exp(B) Bivector exponential → rotor
log(R) Rotor logarithm → bivector
project(v, B) Component of v in subspace B
reject(v, B) Component of v perpendicular to B
reflect(v, n) Reflect v in hyperplane orthogonal to n
even_grades(x) Even-grade components
odd_grades(x) Odd-grade components
is_scalar(x) True if pure scalar
is_vector(x) True if pure 1-vector
is_bivector(x) True if pure 2-vector
is_even(x) True if even-graded
is_rotor(x) True if even and x*~x = 1

Examples

Interactive marimo notebooks in examples/:

  • naming_demo.py — Naming and evaluation API: .name(), .anon(), .lazy(), .eager(), real-world examples
  • lazy_blades_demo.py — Lazy basis blades: basis_vectors(lazy=True) for fully symbolic workflows
  • latex_naming_demo.py — LaTeX-driven naming across physics domains (QM, STA, PGA)
  • rendering_gallery.py — Visual gallery of all expression types with LaTeX rendering
  • symbolic_demo.py — Symbolic expression trees, rendering, simplification, LaTeX output
  • spacetime_algebra.py — Spacetime Algebra Cl(1,3): boosts, rotations, EM field, Thomas–Wigner rotation
  • quantum_physics.py — Quantum spin-½: Bloch sphere, measurement, Stern–Gerlach, Larmor precession
  • pga_demo.py — Projective GA Cl(3,0,1): translations, reflections, motors, screw interpolation
  • special_relativity_lazy.py — Lazy STA walkthrough of boosts, Minkowski diagrams, and rapidity addition
  • electromagnetism_lazy.py — Faraday bivector construction, invariants, and boosted field components
  • quantum_spin_lazy.py — Lazy Pauli-blade construction of spin states and Stern-Gerlach probabilities
  • pauli_equation_toy.py — Toy Pauli-equation notebook for spin precession as rotor evolution
  • optics_polarisation_lazy.py — Polariser projectors and wave-plate rotors in 2D GA
  • pga_geometry_constructions.py — PGA point-line-triangle constructions with lazy joins
  • planar_kinematics_lazy.py — Rotor-based planar rigid-body kinematics and trajectory plotting
  • coupled_oscillators_modes.py — Normal modes as a basis change in configuration-space GA
  • exterior_algebra_intuition.py — Wedge-product intuition for oriented length, area, and volume
  • duality_and_complements.py — Side-by-side notebook for metric duality vs combinatorial complements
  • aharonov_bohm.py — Holonomy-first Aharonov-Bohm notebook using internal phase rotors
  • one_g_travel_calculator.py — Relativistic 1g travel calculator with accelerate, coast, decelerate, and Earth-vs-ship time
  • twin_paradox.py — Worldline and proper-time notebook for the twin paradox
  • relativistic_rocket_equation.py — Rapidity-first rocket equation notebook with burn and coast analysis
  • thomas_precession.py — Non-collinear boost composition and residual Thomas-Wigner rotation
  • em_waves_sta.py — Plane-wave electromagnetic fields and null invariants in STA
  • robot_kinematics_pga.py — Two-link planar robot kinematics with PGA-style motors
  • rotors_from_reflections.py — Reflection composition notebook showing how rotors arise geometrically
  • pauli_matrices_vs_ga.py — Side-by-side comparison of Pauli matrices/spinors and Cl(3,0) rotors/vectors
  • dirac_matrices_vs_sta.py — Side-by-side comparison of Dirac gamma matrices and STA boosts/vectors
  • maxwell_equations_sta.py — Maxwell’s equations in compact STA form with field splits and wave invariants
  • thin_lens_and_rays_pga.py — Thin-lens image construction notebook with projective optics geometry
  • quantum_gates_ga.py — Single-qubit gates as Bloch-sphere rotations in Cl(3,0)
  • kepler_orbits_ga.py — Kepler orbit and hodograph notebook with explicit orbital bivectors
  • qubits_and_superposition_ga.py — Intro to qubits and superposition via rotors and Bloch vectors
  • measurement_and_interference_ga.py — Phase, recombination, and interference explained with GA rotors
  • single_qubit_circuits_ga.py — Small single-qubit circuit builder using rotor composition
  • grover_search_ga.py — Grover search visualized as repeated rotations in a two-state search plane
  • deutsch_jozsa_ga.py — Deutsch-Jozsa interference notebook with oracle phases and recombination
  • bell_states_and_correlations.py — Bell singlet correlation notebook with geometric measurement-axis comparison
  • lorentz_force_sta.py — Qualitative Lorentz-force motion notebook in the STA field picture
  • camera_geometry_pga.py — Simple pinhole camera geometry and image-plane intersections
  • null_geometry_sta.py — Null vectors, light cones, and observer geometry in STA
  • quantum_teleportation_ga.py — Geometric teaching notebook for teleportation as Bloch-vector correction
  • phase_estimation_geometry.py — Phase estimation taught as repeated rotor-angle accumulation
  • screw_motion_pga.py — Screw-motion notebook combining rotation and axial drift in PGA style
  • fresnel_polarisation_ga.py — Fresnel reflection notebook using GA polarisation decomposition
  • projectors_ga.py — Projection and rejection onto lines and planes using GA blades
  • inner_product_family.py — Comparison notebook for the library’s inner-product family
  • involutions_and_grade_ops.py — Reverse, involute, conjugate, grade projection, and even/odd decomposition
  • norms_units_inverses.py — Squared values, norms, unitization, and inverses on vectors and bivectors
  • commutator_lie_jordan.py — Symmetric and antisymmetric product splits through named operations
  • duality_and_subspaces.py — Duality operations combined with project/reject/reflect examples
  • exp_log_rotors.py — Rotor exponentials and logarithms from bivector generators
  • sandwich_products.py — Reflections and rotor sandwiches in one focused notebook
  • meets_joins_pga.py — Small projective joins notebook using PGA complement patterns
uv run marimo edit examples/quantum_physics.py

Internals

  • Basis blades are represented as bitmaskse₁ = 0b001, e₂ = 0b010, e₁₂ = 0b011
  • Multiplication tables are precomputed at algebra creation time
  • Multivector coefficients are dense NumPy arrays of length 2ⁿ
  • The Algebra object is immutable after creation
  • Multivector is lightweight: just an algebra reference + data array

Testing

uv run pytest tests/ -v                          # run all tests
uv run pytest tests/ --cov=ga --cov-report=term  # with coverage

845 tests, 98% coverage. Tests include:

  • Algebraic identities (associativity, distributivity, reverse-of-product)
  • Golden tests for Cl(2,0), Cl(3,0), Cl(1,3)
  • All five inner products with mixed-grade cases where they diverge
  • Exponential/logarithm roundtrips (Euclidean, hyperbolic, null)
  • Projection/rejection complement property, reflection involution
  • Symbolic rendering, evaluation, simplification rules
  • LaTeX output for both Multivector and Expr
  • Naming/evaluation semantics: .name(), .anon(), .lazy(), .eager()
  • Lazy propagation through all operators
  • Lazy basis blades: basis_vectors(lazy=True)
  • MV / MV division, ScalarDiv/Div/Exp expression nodes
  • Precedence-aware rendering (93 dedicated tests)
  • LaTeX symbol mapping (101 tests) and auto-derivation
  • Small value display with explicit format specs
  • All 10 spec use cases from the symbolic redesign
  • Edge cases and error handling

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

galaga-0.3.0.tar.gz (98.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

galaga-0.3.0-py3-none-any.whl (56.3 kB view details)

Uploaded Python 3

File details

Details for the file galaga-0.3.0.tar.gz.

File metadata

  • Download URL: galaga-0.3.0.tar.gz
  • Upload date:
  • Size: 98.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for galaga-0.3.0.tar.gz
Algorithm Hash digest
SHA256 a4fa6a2cb035512977016779996c15f2d9dad36c004e698180a93da0fe014ea2
MD5 e49ec1d106668039213d057052169324
BLAKE2b-256 98a90c960600b059e9690f220f4e9705e05ff05c733495422e2c3504b968273c

See more details on using hashes here.

File details

Details for the file galaga-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: galaga-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 56.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for galaga-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 383ac4e3b31792c63b13c746f847b6135491e5331f7a8d292644cae6c88a566c
MD5 a4794c31568ee6e1676e693d183c9cc3
BLAKE2b-256 d4b31d3bb4b106295872c3d39001aa4dc72c2d094126d960291c27f8402b9528

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page