Pythonic Geometric Algebra Package
- Free software: MIT license
- Documentation: https://kingdon.readthedocs.io.
β¨ Try kingdon in your browser β¨
Cite as:
@misc{roelfs2025willingkingdoncliffordalgebra,
title={The Willing Kingdon Clifford Algebra Library},
author={Martin Roelfs},
year={2025},
eprint={2503.10451},
archivePrefix={arXiv},
primaryClass={cs.MS},
url={https://arxiv.org/abs/2503.10451},
}
Kingdon is a Geometric Algebra (GA) library which combines a Pythonic API with
symbolic simplification and just-in-time compilation to achieve high-performance in a single package.
It support both symbolic and numerical GA computations.
Moreover, kingdon uses ganja.js for visualization in notebooks,
making it an extremely well rounded GA package.
In bullet points:
- Symbolically optimized code generation.
- Leverage sparseness of input.
- ganja.js enabled graphics in jupyter notebooks.
- Agnostic to the input types: work with GA's over numpy arrays, torch tensors, sympy expressions, etc. Any object that overloads addition, subtraction and multiplication makes for valid multivector coefficients in
kingdon. - Automatic broadcasting, such that transformations can be applied to e.g. point-clouds.
- Compatible with einops if you
import kingdon.einops_backendbefore you do your einops magic. - Compatible with numba and other JIT compilers to speed-up numerical computations.
Kingdon has great symbiosis with the python array api, allowing you to construct multidimensional multivectors using NumPy, PyTorch, JAX, CuPy, Dask, and more. (The examples below use NumPy.) A multivector over arrays is just a batch of geometry β and the shape tells you the type:
alg = Algebra.fromname("3DPGA")
points = alg.point(np.random.rand(3, 5)) # Point[(5,)]
lines = alg.bivector(np.random.rand(6, 3)).normalized() # Bivector[(3,)]kingdon supports vectorized expressions in addition to for-loops:
points[:, None] @ lines[None, :] # Point[(5, 3)]: every point projected onto every lineThe a @ b = (a | b) / b projection operator projects every point onto every line in one simple expressions giving a Point[(5, 3)].
Therefore kingdon allows you to write high-level algorithms that focus purely on the
geometry, while delegating the looping to the array library of your choice.
Masking passes straight through to your coefficients, so numpy tricks just
work:
O = alg.blades.e0.dual() # origin
nearby = points[(points & O).norm().e < 1] # every point within the unit sphereFancy indexing means a whole mesh is loop-free:
v = alg.point(vertices.T) # Point[(N,)] the point cloud
facets = v[faces] # Point[(M, 3)] a point per face corner
planes = facets[..., 0] & facets[..., 1] & facets[..., 2] # Vector[(M,)] the face planes
area = 0.5 * reduce(planes.norm(), 'm -> ', 'sum').e # total surface area
volume = reduce(planes, 'm -> ', 'sum').e0 / 6 # signed volume of the meshYes, the signed volume of the whole mesh is just the sum of the e0 coefficients. π€―
But what if you do not want to manipulate the blade dimension (and hence the geometry), but you want to manipulate the batch dimensions instead?
For that, you can directly use einops on multivectors, making it easy to write high-level operations
such as rearrange, reduce, repeat, pack/unpack, einsum,
without any reference to a specific array package.
Patterns only ever mention the batch dims (the blade axis is not
yours to play with), so e.g. a vector stays a vector:
import kingdon.einops_backend
from einops import rearrange, reduce, repeat
x = alg.vector(np.random.rand(4, 3, 4)) # Vector[(3, 4)]
rearrange(x, 'a b -> b a') # Vector[(4, 3)]
reduce(x, 'a b -> a', 'mean') # Vector[(3,)]
repeat(x, 'a b -> a b c', c=5) # Vector[(3, 4, 5)]This works for any package supported by einops (NumPy, PyTorch, JAX, CuPy, and more).
pack/unpack glue multivectors together along a wildcard axis, carrying
the right dtype and living on the right device.
a = alg.vector(np.ones([4, 3, 5])) # Vector[(3, 5)]
b = alg.vector(np.ones([4, 3, 7, 5])) # Vector[(3, 7, 5)]
packed, ps = pack([a, b], 'j * k') # Vector[(3, 8, 5)]
a2, b2 = unpack(packed, ps, 'j * k') # Vector[(3, 5)], Vector[(3, 7, 5)]And einsum contracts batch dimensions blade by blade, so multivectors and
plain arrays mix freely:
vec = alg.vector(np.random.randn(4, 10, 10)) # Vector[(10, 10)]
w = np.random.randn(10, 20) # just a numpy array
einsum(vec, 'i i ->') # Vector[()] the trace
einsum(vec, w, 'i j, j k -> i k') # Vector[(10, 20)] batched matmulAnd it's fast. New GAmphetamine-style CSE, on by default for built-in operators and optional for custom operators using @alg.add_operator(symbolic=True).
3DPGA, counted in muls/adds β naive β CSE:
R >> p 72/30 β 21/18 Β· p @ P 21/15 β 6/6 Β· P >> p 33/13 β
9/7. That's hand-optimized level, automatically generated.
If you are thirsty for some examples, please visit the teahouse. A small selection of our items:
In order to demonstrate the power of Kingdon, let us first consider the common use-case of the
commutator product between a bivector and vector.
In order to create an algebra, use Algebra. When calling Algebra we must provide the signature of the
algebra, in this case we shall go for 3DPGA, which is the algebra \mathbb{R}_{3,0,1}.
There are a number of ways to make elements of the algebra. It can be convenient to work with the basis blades directly.
We can add them to the local namespace by calling globals().update(alg.blades):
>>> from kingdon import Algebra
>>> alg = Algebra(3, 0, 1)
>>> globals().update(alg.blades)
>>> b = 2 * e12
>>> v = 3 * e1
>>> b * v
-6 πβThis example shows that only the e2 coefficient is calculated, despite the fact that there are
6 bivector and 4 vector coefficients in 3DPGA. But by exploiting the sparseness of the input and by performing symbolic
optimization, kingdon knows that in this case only e2 can be non-zero.
If only a name is provided for a multivector, kingdon will automatically populate all
relevant fields with symbols. This allows us to easily perform symbolic computations.
>>> from kingdon import Algebra
>>> alg = Algebra(3, 0, 1)
>>> b = alg.bivector(name='b')
>>> b
b01 πββ + b02 πββ + b03 πββ + b12 πββ + b13 πββ + b23 πββ
>>> v = alg.vector(name='v')
>>> v
v0 πβ + v1 πβ + v2 πβ + v3 πβ
>>> b.cp(v)
(b01*v1 + b02*v2 + b03*v3) πβ + (b12*v2 + b13*v3) πβ + (-b12*v1 + b23*v3) πβ + (-b13*v1 - b23*v2) πβIt is also possible to define some coefficients to be symbolic by inputting a string, while others can be numeric:
>>> from kingdon import Algebra, symbols
>>> alg = Algebra(3, 0, 1)
>>> b = alg.bivector(e12='b12', e03=3)
>>> b
3 πββ + b12 πββ
>>> v = alg.vector(e1=1, e3=1)
>>> v
1 πβ + 1 πβ
>>> w = b.cp(v)
>>> w
3 πβ + (-b12) πβ| Operation | Expression | Infix | Inline |
|---|---|---|---|
| Geometric product | a*b |
a.gp(b) |
|
| Inner | a|b |
a.ip(b) |
|
| Scalar product | Β | a.sp(b) |
|
| Left-contraction | Β | a.lc(b) |
|
| Right-contraction | Β | a.rc(b) |
|
| Outer (Exterior) | a ^ b |
a.op(b) |
|
| Regressive | a & b |
a.rp(b) |
|
Conjugate a by b with \widetilde{b}b = 1
|
b >> a |
b.sw(a) |
|
Project a onto b with \widetilde{b}b = 1
|
a @ b |
a.proj(b) |
|
Commutator of a and b
|
Β | a.cp(b) |
|
Anti-commutator of a and b
|
Β | a.acp(b) |
|
Sum of a and b
|
a + b |
a.add(b) |
|
Difference of a and b
|
a - b |
a.sub(b) |
|
"Divide" a by b
|
a / b |
a.div(b) |
|
Inverse of a
|
Β | a.inv() |
|
Reverse of a
|
~a |
a.reverse() |
|
Grade Involution of a
|
Β | a.involute() |
|
Clifford Conjugate of a
|
Β | a.conjugate() |
|
Squared norm of a
|
Β | a.normsq() |
|
Norm of a
|
Β | a.norm() |
|
Normalize a
|
Β | a.normalized() |
|
Square root of a
|
Β | a.sqrt() |
|
Dual of a
|
Β | a.dual() |
|
Undual of a
|
Β | Β | a.undual() |
Grade k part of a
|
Β | a.grade(k) |
This package was inspired by GAmphetamine.js.











