As shown at the end of this post, I recently ported pga3d.cpp (the bivector.net C++ reference implementation of projective geometric algebra) to Julia because bivector.net does not currently offer a Julia reference implementation. Before I offer my proposed Julia reference implementation of projective geometric algebra to bivector.net, I am first showing it in this post so my design and implementation can be informally reviewed … because I am relatively new to Julia and I am extremely new to geometric algebra. Also, because some visitors to bivector.net may currently be unfamiliar with Julia, this seems like a good opportunity to make a good first impression of Julia by showcasing its capabilities that seem to be a particularly good fit for implementing projective geometric algebra applications.
DESIGN OVERVIEW AND GOALS
Taking a cue from Elon Musk’s management by rhyming principle “the best part is no part”, the proposed Julia reference implementation defines no structures: the best struct is no struct. Instead, Vector{Float32} is the data type for all the multivectors. Changing the dimension of the space (e.g., 2D, 3D, 4D, 5D) changes the length of the Vector{Float32} and changes some of the overloaded operators but does not change any of the geometric algebra equations.
In his SIBGRAPI 2021 presentation, Steven De Keninck demonstrated the advantage of having geometric algebra equations that stay exactly the same while changing the dimensions of the space. He used ganja.js, his javascript implementation of geometric algebra, to simulate the physics of a 2D wire frame square dangling from a thread, a 3D wire frame cube dangling from a thread, and a 4D wire frame tesseract dangling from a thread … all by changing a single variable defining the number of dimensions. And in his SIGGRAPH 2019 presentation, Steven De Keninck demonstrated how projective geometric algebra simplifies calculating object cross sections. He showed the surprisingly simple projective geometric algebra approach to calculating horizontal slices of a model of a chocolate bunny. (This slicing capability is my motivation for working on this project. More specifically, I want a slicer application implemented in Julia that will generate a 3D printer file that defines the shape of the ratchet mechanism on each of the many display pins in a proposed low cost braille/tactile display.)
A FAST IMPLEMENTATION OF PROJECTIVE GEOMETRIC ALGEBRA
The reason that projective geometric algebra code is surprisingly concise is that the majority of the processing is hidden in overloaded operators. For example, here is the overloaded * operator code that calculates the 3D geometric product:
function Base.:*(a::Vector{Float32},b::Vector{Float32})
res = Vector{Float32}(undef,nField)
res[1]=b[1]*a[1]+b[3]*a[3]+b[4]*a[4]+b[5]*a[5]-b[9]*a[9]-b[10]*a[10]-b[11]*a[11]-b[15]*a[15]
res[2]=b[2]*a[1]+b[1]*a[2]-b[6]*a[3]-b[7]*a[4]-b[8]*a[5]+b[3]*a[6]+b[4]*a[7]+b[5]*a[8]+b[12]*a[9]+b[13]*a[10]+b[14]*a[11]+b[9]*a[12]+b[10]*a[13]+b[11]*a[14]+b[16]*a[15]-b[15]*a[16]
res[3]=b[3]*a[1]+b[1]*a[3]-b[9]*a[4]+b[10]*a[5]+b[4]*a[9]-b[5]*a[10]-b[15]*a[11]-b[11]*a[15]
res[4]=b[4]*a[1]+b[9]*a[3]+b[1]*a[4]-b[11]*a[5]-b[3]*a[9]-b[15]*a[10]+b[5]*a[11]-b[10]*a[15]
res[5]=b[5]*a[1]-b[10]*a[3]+b[11]*a[4]+b[1]*a[5]-b[15]*a[9]+b[3]*a[10]-b[4]*a[11]-b[9]*a[15]
res[6]=b[6]*a[1]+b[3]*a[2]-b[2]*a[3]-b[12]*a[4]+b[13]*a[5]+b[1]*a[6]-b[9]*a[7]+b[10]*a[8]+b[7]*a[9]-b[8]*a[10]-b[16]*a[11]-b[4]*a[12]+b[5]*a[13]+b[15]*a[14]-b[14]*a[15]-b[11]*a[16]
res[7]=b[7]*a[1]+b[4]*a[2]+b[12]*a[3]-b[2]*a[4]-b[14]*a[5]+b[9]*a[6]+b[1]*a[7]-b[11]*a[8]-b[6]*a[9]-b[16]*a[10]+b[8]*a[11]+b[3]*a[12]+b[15]*a[13]-b[5]*a[14]-b[13]*a[15]-b[10]*a[16]
res[8]=b[8]*a[1]+b[5]*a[2]-b[13]*a[3]+b[14]*a[4]-b[2]*a[5]-b[10]*a[6]+b[11]*a[7]+b[1]*a[8]-b[16]*a[9]+b[6]*a[10]-b[7]*a[11]+b[15]*a[12]-b[3]*a[13]+b[4]*a[14]-b[12]*a[15]-b[9]*a[16]
res[9]=b[9]*a[1]+b[4]*a[3]-b[3]*a[4]+b[15]*a[5]+b[1]*a[9]+b[11]*a[10]-b[10]*a[11]+b[5]*a[15]
res[10]=b[10]*a[1]-b[5]*a[3]+b[15]*a[4]+b[3]*a[5]-b[11]*a[9]+b[1]*a[10]+b[9]*a[11]+b[4]*a[15]
res[11]=b[11]*a[1]+b[15]*a[3]+b[5]*a[4]-b[4]*a[5]+b[10]*a[9]-b[9]*a[10]+b[1]*a[11]+b[3]*a[15]
res[12]=b[12]*a[1]-b[9]*a[2]+b[7]*a[3]-b[6]*a[4]+b[16]*a[5]-b[4]*a[6]+b[3]*a[7]-b[15]*a[8]-b[2]*a[9]+b[14]*a[10]-b[13]*a[11]+b[1]*a[12]+b[11]*a[13]-b[10]*a[14]+b[8]*a[15]-b[5]*a[16]
res[13]=b[13]*a[1]-b[10]*a[2]-b[8]*a[3]+b[16]*a[4]+b[6]*a[5]+b[5]*a[6]-b[15]*a[7]-b[3]*a[8]-b[14]*a[9]-b[2]*a[10]+b[12]*a[11]-b[11]*a[12]+b[1]*a[13]+b[9]*a[14]+b[7]*a[15]-b[4]*a[16]
res[14]=b[14]*a[1]-b[11]*a[2]+b[16]*a[3]+b[8]*a[4]-b[7]*a[5]-b[15]*a[6]-b[5]*a[7]+b[4]*a[8]+b[13]*a[9]-b[12]*a[10]-b[2]*a[11]+b[10]*a[12]-b[9]*a[13]+b[1]*a[14]+b[6]*a[15]-b[3]*a[16]
res[15]=b[15]*a[1]+b[11]*a[3]+b[10]*a[4]+b[9]*a[5]+b[5]*a[9]+b[4]*a[10]+b[3]*a[11]+b[1]*a[15]
res[16]=b[16]*a[1]+b[15]*a[2]+b[14]*a[3]+b[13]*a[4]+b[12]*a[5]+b[11]*a[6]+b[10]*a[7]+b[9]*a[8]+b[8]*a[9]+b[7]*a[10]+b[6]*a[11]-b[5]*a[12]-b[4]*a[13]-b[3]*a[14]-b[2]*a[15]+b[1]*a[16]
return res
end
I attempted a vector multiplication approach to calculating this geometric product but, because it runs 8 times slower, it is currently commented out in the Julia code listed at the end of this post.
According to @time, the proposed Julia implementation at the end of this post runs on my laptop in about 2.2 milliseconds:
julia> @time utest()
point : 1e032 + 1e123
line : -1e23
plane : -3e0 + 2e1 + 1e3
rotor : 0.7071068 + 0.7071068e12
rotated point : -0.9999999e013 + 0.9999999e123
rotated line : 0.9999999e31
rotated plane : -3e0 + -2e2 + 0.9999999e3
point on plane : 0.2e021 + 1.4e032 + 1e123
point on torus : 0.85e032 + 1e123
toStr() test 1 : -1 + 1e0
toStr() test 2 : 1 + -1e0
0.002227 seconds (708 allocations: 30.234 KiB)
According to @btime, the proposed Julia implementation at the end of this post runs on my laptop in about 19 microseconds when the output to the screen is disabled:
julia> @btime utest(false)
19.200 μs (479 allocations: 14.75 KiB)
A PRETTY IMPLEMENTATION OF PROJECTIVE GEOMETRIC ALGEBRA
A standard has emerged on representing geometric algebra operators in programming languages:
Name Programming Syntax
geometric product a * b
outer product (wedge) a ^ b
regressive product (vee) a & b
inner product (dot) a | b
dual !a
sandwich product a >>> b
Following this standard programming syntax increases the portability of projective geometric algebra equations, even between different programming languages. Unfortunately, this standard programming syntax does not match the standard math syntax. This mismatch between the standard math syntax and the standard programming syntax is an unnecessary mental friction and Julia seems to be in a position to remove it. Perhaps some day Julia will offer a way for programmers to read and write their projective geometric algebra equations directly in math syntax and let Julia convert to and from the programming syntax. For example, perhaps some kind of a block macro could look something like this:
@=pga
#=
Inside the pga block macro is a block of projective
geometry algebra equations using Unicode operators
such as \wedge and \vee and \thinspace (for geometric
product) and \bullet (for dot product) and \asteraccent
(for dual) and \tilde (for reverse operator in sandwich).
=#
end
AN IMPLEMENTATION OF PROJECTIVE GEOMETRIC ALGEBRA
# pga.jl : Projective Geometric Algebra
# This is a port of pga3d.cpp, a C++ reference implementation
# of projective geometric algebra, that is available at
# https://bivector.net/tools.html
#
# The implementation of pga3d.cpp and this port of it,
# is currently limited to projective geometric algebra in
# 3D space. However, I'm looking forward to extending this
# implementation to projective geometric algebra in 2D, 4D,
# and possibly 5D spaces. That planned extension is the
# reason this file is named pga.jl instead of pga3d.jl.
#
using Printf
# define number of dimensions in space, and
# define number of fields in multivectors
nD = 3
if (nD == 1)
nField = 0 # TODO: make nonzero after implementation
elseif (nD == 2)
nField = 0 # TODO: make nonzero after implementation
elseif (nD == 3)
nField = 16
elseif (nD == 4)
nField = 0 # TODO: make nonzero after implementation
else
nField = 0
end
if (nD == 3)
# define multivector field names
basis = [ # iField
"1" # 1
"e0" # 2 basis vectors
"e1" # 3
"e2" # 4
"e3" # 5
"e01" # 6 basis bivectors
"e02" # 7
"e03" # 8
"e12" # 9
"e31" # 10
"e23" # 11
"e021" # 12 basis trivectors
"e013" # 13
"e032" # 14
"e123" # 15
"e0123" # 16 pseudoscalar
]
# define basis vectors
e0 = zeros(Float32, nField); e0[2] = 1
e1 = zeros(Float32, nField); e1[3] = 1
e2 = zeros(Float32, nField); e2[4] = 1
e3 = zeros(Float32, nField); e3[5] = 1
# operator overloads dependent upon dimension
function Base.:*(a::Vector{Float32},b::Vector{Float32})
res = Vector{Float32}(undef,nField)
res[1]=b[1]*a[1]+b[3]*a[3]+b[4]*a[4]+b[5]*a[5]-b[9]*a[9]-b[10]*a[10]-b[11]*a[11]-b[15]*a[15]
res[2]=b[2]*a[1]+b[1]*a[2]-b[6]*a[3]-b[7]*a[4]-b[8]*a[5]+b[3]*a[6]+b[4]*a[7]+b[5]*a[8]+b[12]*a[9]+b[13]*a[10]+b[14]*a[11]+b[9]*a[12]+b[10]*a[13]+b[11]*a[14]+b[16]*a[15]-b[15]*a[16]
res[3]=b[3]*a[1]+b[1]*a[3]-b[9]*a[4]+b[10]*a[5]+b[4]*a[9]-b[5]*a[10]-b[15]*a[11]-b[11]*a[15]
res[4]=b[4]*a[1]+b[9]*a[3]+b[1]*a[4]-b[11]*a[5]-b[3]*a[9]-b[15]*a[10]+b[5]*a[11]-b[10]*a[15]
res[5]=b[5]*a[1]-b[10]*a[3]+b[11]*a[4]+b[1]*a[5]-b[15]*a[9]+b[3]*a[10]-b[4]*a[11]-b[9]*a[15]
res[6]=b[6]*a[1]+b[3]*a[2]-b[2]*a[3]-b[12]*a[4]+b[13]*a[5]+b[1]*a[6]-b[9]*a[7]+b[10]*a[8]+b[7]*a[9]-b[8]*a[10]-b[16]*a[11]-b[4]*a[12]+b[5]*a[13]+b[15]*a[14]-b[14]*a[15]-b[11]*a[16]
res[7]=b[7]*a[1]+b[4]*a[2]+b[12]*a[3]-b[2]*a[4]-b[14]*a[5]+b[9]*a[6]+b[1]*a[7]-b[11]*a[8]-b[6]*a[9]-b[16]*a[10]+b[8]*a[11]+b[3]*a[12]+b[15]*a[13]-b[5]*a[14]-b[13]*a[15]-b[10]*a[16]
res[8]=b[8]*a[1]+b[5]*a[2]-b[13]*a[3]+b[14]*a[4]-b[2]*a[5]-b[10]*a[6]+b[11]*a[7]+b[1]*a[8]-b[16]*a[9]+b[6]*a[10]-b[7]*a[11]+b[15]*a[12]-b[3]*a[13]+b[4]*a[14]-b[12]*a[15]-b[9]*a[16]
res[9]=b[9]*a[1]+b[4]*a[3]-b[3]*a[4]+b[15]*a[5]+b[1]*a[9]+b[11]*a[10]-b[10]*a[11]+b[5]*a[15]
res[10]=b[10]*a[1]-b[5]*a[3]+b[15]*a[4]+b[3]*a[5]-b[11]*a[9]+b[1]*a[10]+b[9]*a[11]+b[4]*a[15]
res[11]=b[11]*a[1]+b[15]*a[3]+b[5]*a[4]-b[4]*a[5]+b[10]*a[9]-b[9]*a[10]+b[1]*a[11]+b[3]*a[15]
res[12]=b[12]*a[1]-b[9]*a[2]+b[7]*a[3]-b[6]*a[4]+b[16]*a[5]-b[4]*a[6]+b[3]*a[7]-b[15]*a[8]-b[2]*a[9]+b[14]*a[10]-b[13]*a[11]+b[1]*a[12]+b[11]*a[13]-b[10]*a[14]+b[8]*a[15]-b[5]*a[16]
res[13]=b[13]*a[1]-b[10]*a[2]-b[8]*a[3]+b[16]*a[4]+b[6]*a[5]+b[5]*a[6]-b[15]*a[7]-b[3]*a[8]-b[14]*a[9]-b[2]*a[10]+b[12]*a[11]-b[11]*a[12]+b[1]*a[13]+b[9]*a[14]+b[7]*a[15]-b[4]*a[16]
res[14]=b[14]*a[1]-b[11]*a[2]+b[16]*a[3]+b[8]*a[4]-b[7]*a[5]-b[15]*a[6]-b[5]*a[7]+b[4]*a[8]+b[13]*a[9]-b[12]*a[10]-b[2]*a[11]+b[10]*a[12]-b[9]*a[13]+b[1]*a[14]+b[6]*a[15]-b[3]*a[16]
res[15]=b[15]*a[1]+b[11]*a[3]+b[10]*a[4]+b[9]*a[5]+b[5]*a[9]+b[4]*a[10]+b[3]*a[11]+b[1]*a[15]
res[16]=b[16]*a[1]+b[15]*a[2]+b[14]*a[3]+b[13]*a[4]+b[12]*a[5]+b[11]*a[6]+b[10]*a[7]+b[9]*a[8]+b[8]*a[9]+b[7]*a[10]+b[6]*a[11]-b[5]*a[12]-b[4]*a[13]-b[3]*a[14]-b[2]*a[15]+b[1]*a[16]
return res
end
# my attempt at speeding up this calculation by vectorizing the products
# resulted, according to @btime utest(false), a 4.5 times slower unit test
# function Base.:*(a::Vector{Float32},b::Vector{Float32})
# res = b .* a[1]
#
# tmp = b[[3,1,1,9,10,3,4,5,4,5,15,9,10,11,11,15]] .*
# a[[3,2,3,3, 3,2,2,2,3,3, 3,2, 2, 2, 3, 2]]
# tmp[[5,10,12,13,14]] .*= -1
# res += tmp
#
# tmp = b[[4,6,9,1,11,2,12,13,3,15,5,7,8,16,10,14]] .*
# a[[4,3,4,4, 4,3, 3, 3,4, 4,4,3,3, 3, 4, 3]]
# tmp[[2,3,6,8,9,13]] .*= -1
# res += tmp
#
# tmp = b[[5,7,10,11,1,12,2,14,15,3,4,6,16,8,9,13]] .*
# a[[5,4, 5, 5,5, 4,4, 4, 5,5,5,4, 4,4,5, 4]]
# tmp[[2,4,6,7,11,12]] .*= -1
# res += tmp
#
# tmp = b[[9,8,4,3,15,13,14,2,1,11,10,16,6,7,5,12]] .*
# a[[9,5,9,9, 9, 5, 5,5,9, 9, 9, 5,5,5,9, 5]]
# tmp[[1,2,4,5,7,8,10,14]] .*= -1
# res += tmp
#
# tmp = b[[10,3, 5,15, 3,1,9,10,11, 1, 9,4,5,15, 4,11]] .*
# a[[10,6,10,10,10,6,6, 6,10,10,10,6,6, 6,10, 6]]
# tmp[[1,3,4,8,11,12,14]] .*= -1
# res += tmp
#
# tmp = b[[11,4,15, 5, 4,9,1,11,10, 9, 1,3,15,5, 3,10]] .*
# a[[11,7,11,11,11,7,7, 7,11,11,11,7, 7,7,11, 7]]
# tmp[[1,3,5,6,9,13,14]] .*= -1
# res += tmp
#
# tmp = b[[15,5,11,10, 9,10,11,1, 5, 4, 3,15,3,4, 1,9]] .*
# a[[15,8,15,15,15, 8, 8,8,15,15,15, 8,8,8,15,8]]
# tmp[[1,3,4,5,7,12,13]] .*= -1
# res += tmp
#
# tmp = b[[12,7,6,16,2,14,13,8]] .* a[9]
# tmp[[3,4,5,6]] .*= -1
# res[[2,6,7,8,12,13,14,16]] .+= tmp
#
# tmp = b[[13,8,16,6,14,2,12,7]] .* a[10]
# tmp[[2,3,6,7]] .*= -1
# res[[2,6,7,8,12,13,14,16]] .+= tmp
#
# tmp = b[[14,16,8,7,13,12,2,6]] .* a[11]
# tmp[[2,4,5,7]] .*= -1
# res[[2,6,7,8,12,13,14,16]] .+= tmp
#
# tmp = b[[9,4,3,15,1,11,10,5]] .* a[12]
# tmp[[2,6,8]] .*= -1
# res[[2,6,7,8,12,13,14,16]] .+= tmp
#
# tmp = b[[10,5,15,3,11,1,9,4]] .* a[13]
# tmp[[4,7,8]] .*= -1
# res[[2,6,7,8,12,13,14,16]] .+= tmp
#
# tmp = b[[11,15,5,4,10,9,1,3]] .* a[14]
# tmp[[3,5,8]] .*= -1
# res[[2,6,7,8,12,13,14,16]] .+= tmp
#
# tmp = b[[16,14,13,12,8,7,6,2]] .* a[15]
# tmp[[2,3,4,8]] .*= -1
# res[[2,6,7,8,12,13,14,16]] .+= tmp
#
# tmp = b[[15,11,10,9,5,4,3,1]] .* a[16]
# tmp[[1,2,3,4,5,6,7]] .*= -1
# res[[2,6,7,8,12,13,14,16]] .+= tmp
#
# return res
# end
function Base.:&(a::Vector{Float32},b::Vector{Float32})
res = Vector{Float32}(undef,nField)
res[16]=1*(a[16]*b[16])
res[15]=-1*(a[15]*-1*b[16]+a[16]*b[15]*-1)
res[14]=-1*(a[14]*-1*b[16]+a[16]*b[14]*-1)
res[13]=-1*(a[13]*-1*b[16]+a[16]*b[13]*-1)
res[12]=-1*(a[12]*-1*b[16]+a[16]*b[12]*-1)
res[11]=1*(a[11]*b[16]+a[14]*-1*b[15]*-1-a[15]*-1*b[14]*-1+a[16]*b[11])
res[10]=1*(a[10]*b[16]+a[13]*-1*b[15]*-1-a[15]*-1*b[13]*-1+a[16]*b[10])
res[9]=1*(a[9]*b[16]+a[12]*-1*b[15]*-1-a[15]*-1*b[12]*-1+a[16]*b[9])
res[8]=1*(a[8]*b[16]+a[13]*-1*b[14]*-1-a[14]*-1*b[13]*-1+a[16]*b[8])
res[7]=1*(a[7]*b[16]-a[12]*-1*b[14]*-1+a[14]*-1*b[12]*-1+a[16]*b[7])
res[6]=1*(a[6]*b[16]+a[12]*-1*b[13]*-1-a[13]*-1*b[12]*-1+a[16]*b[6])
res[5]=1*(a[5]*b[16]-a[8]*b[15]*-1+a[10]*b[14]*-1-a[11]*b[13]*-1-a[13]*-1*b[11]+a[14]*-1*b[10]-a[15]*-1*b[8]+a[16]*b[5])
res[4]=1*(a[4]*b[16]-a[7]*b[15]*-1-a[9]*b[14]*-1+a[11]*b[12]*-1+a[12]*-1*b[11]-a[14]*-1*b[9]-a[15]*-1*b[7]+a[16]*b[4])
res[3]=1*(a[3]*b[16]-a[6]*b[15]*-1+a[9]*b[13]*-1-a[10]*b[12]*-1-a[12]*-1*b[10]+a[13]*-1*b[9]-a[15]*-1*b[6]+a[16]*b[3])
res[2]=1*(a[2]*b[16]+a[6]*b[14]*-1+a[7]*b[13]*-1+a[8]*b[12]*-1+a[12]*-1*b[8]+a[13]*-1*b[7]+a[14]*-1*b[6]+a[16]*b[2])
res[1]=1*(a[1]*b[16]+a[2]*b[15]*-1+a[3]*b[14]*-1+a[4]*b[13]*-1+a[5]*b[12]*-1+a[6]*b[11]+a[7]*b[10]+a[8]*b[9]+a[9]*b[8]+a[10]*b[7]+a[11]*b[6]-a[12]*-1*b[5]-a[13]*-1*b[4]-a[14]*-1*b[3]-a[15]*-1*b[2]+a[16]*b[1])
return res
end
function Base.:|(a::Vector{Float32},b::Vector{Float32})
res = Vector{Float32}(undef,nField)
res[1]=b[1]*a[1]+b[3]*a[3]+b[4]*a[4]+b[5]*a[5]-b[9]*a[9]-b[10]*a[10]-b[11]*a[11]-b[15]*a[15]
res[2]=b[2]*a[1]+b[1]*a[2]-b[6]*a[3]-b[7]*a[4]-b[8]*a[5]+b[3]*a[6]+b[4]*a[7]+b[5]*a[8]+b[12]*a[9]+b[13]*a[10]+b[14]*a[11]+b[9]*a[12]+b[10]*a[13]+b[11]*a[14]+b[16]*a[15]-b[15]*a[16]
res[3]=b[3]*a[1]+b[1]*a[3]-b[9]*a[4]+b[10]*a[5]+b[4]*a[9]-b[5]*a[10]-b[15]*a[11]-b[11]*a[15]
res[4]=b[4]*a[1]+b[9]*a[3]+b[1]*a[4]-b[11]*a[5]-b[3]*a[9]-b[15]*a[10]+b[5]*a[11]-b[10]*a[15]
res[5]=b[5]*a[1]-b[10]*a[3]+b[11]*a[4]+b[1]*a[5]-b[15]*a[9]+b[3]*a[10]-b[4]*a[11]-b[9]*a[15]
res[6]=b[6]*a[1]-b[12]*a[4]+b[13]*a[5]+b[1]*a[6]-b[16]*a[11]-b[4]*a[12]+b[5]*a[13]-b[11]*a[16]
res[7]=b[7]*a[1]+b[12]*a[3]-b[14]*a[5]+b[1]*a[7]-b[16]*a[10]+b[3]*a[12]-b[5]*a[14]-b[10]*a[16]
res[8]=b[8]*a[1]-b[13]*a[3]+b[14]*a[4]+b[1]*a[8]-b[16]*a[9]-b[3]*a[13]+b[4]*a[14]-b[9]*a[16]
res[9]=b[9]*a[1]+b[15]*a[5]+b[1]*a[9]+b[5]*a[15]
res[10]=b[10]*a[1]+b[15]*a[4]+b[1]*a[10]+b[4]*a[15]
res[11]=b[11]*a[1]+b[15]*a[3]+b[1]*a[11]+b[3]*a[15]
res[12]=b[12]*a[1]+b[16]*a[5]+b[1]*a[12]-b[5]*a[16]
res[13]=b[13]*a[1]+b[16]*a[4]+b[1]*a[13]-b[4]*a[16]
res[14]=b[14]*a[1]+b[16]*a[3]+b[1]*a[14]-b[3]*a[16]
res[15]=b[15]*a[1]+b[1]*a[15]
res[16]=b[16]*a[1]+b[1]*a[16]
return res
end
function Base.:^(a::Vector{Float32},b::Vector{Float32})
res = Vector{Float32}(undef,nField)
res[1]=b[1]*a[1]
res[2]=b[2]*a[1]+b[1]*a[2]
res[3]=b[3]*a[1]+b[1]*a[3]
res[4]=b[4]*a[1]+b[1]*a[4]
res[5]=b[5]*a[1]+b[1]*a[5]
res[6]=b[6]*a[1]+b[3]*a[2]-b[2]*a[3]+b[1]*a[6]
res[7]=b[7]*a[1]+b[4]*a[2]-b[2]*a[4]+b[1]*a[7]
res[8]=b[8]*a[1]+b[5]*a[2]-b[2]*a[5]+b[1]*a[8]
res[9]=b[9]*a[1]+b[4]*a[3]-b[3]*a[4]+b[1]*a[9]
res[10]=b[10]*a[1]-b[5]*a[3]+b[3]*a[5]+b[1]*a[10]
res[11]=b[11]*a[1]+b[5]*a[4]-b[4]*a[5]+b[1]*a[11]
res[12]=b[12]*a[1]-b[9]*a[2]+b[7]*a[3]-b[6]*a[4]-b[4]*a[6]+b[3]*a[7]-b[2]*a[9]+b[1]*a[12]
res[13]=b[13]*a[1]-b[10]*a[2]-b[8]*a[3]+b[6]*a[5]+b[5]*a[6]-b[3]*a[8]-b[2]*a[10]+b[1]*a[13]
res[14]=b[14]*a[1]-b[11]*a[2]+b[8]*a[4]-b[7]*a[5]-b[5]*a[7]+b[4]*a[8]-b[2]*a[11]+b[1]*a[14]
res[15]=b[15]*a[1]+b[11]*a[3]+b[10]*a[4]+b[9]*a[5]+b[5]*a[9]+b[4]*a[10]+b[3]*a[11]+b[1]*a[15]
res[16]=b[16]*a[1]+b[15]*a[2]+b[14]*a[3]+b[13]*a[4]+b[12]*a[5]+b[11]*a[6]+b[10]*a[7]+b[9]*a[8]+b[8]*a[9]+b[7]*a[10]+b[6]*a[11]-b[5]*a[12]-b[4]*a[13]-b[3]*a[14]-b[2]*a[15]+b[1]*a[16]
return res
end
function Base.:~(a::Vector{Float32}) # reverse operator
res = copy(a)
res[[6 7 8 9 10 11 12 13 14 15]] .*= -1
return res
end
# basis bivectors (planes)
function plane(a::Float32, b::Float32, c::Float32, d::Float32)
return a*e1 + b*e2 + c*e3 + d*e0
end
# basis trivectors (points)
e123 = e1^e2^e3
e032 = e0^e3^e2
e013 = e0^e1^e3
e021 = e0^e2^e1
function point(x::Float32, y::Float32, z::Float32)
return e123 + x*e032 + y*e013 + z*e021
end
# rotate, translate, conjugate
function rotor(angle::Float32, line::Vector{Float32})
return cos(angle/2f0) + sin(angle/2f0)*normalize(line)
end
function translator(dist::Float32, line::Vector{Float32})
return 1f0 + dist/2f0 * line
end
function conjugate(a::Vector{Float32})
res = copy(a)
res[[2 3 4 5 6 8 9 10 11]] .*= -1
return res
end
# circle and torus
function circle(t::Float32, radius::Float32, line::Vector{Float32})
return rotor(t*2f0*pi,line) * translator(radius,e1*e0)
end
function torus(s::Float32, t::Float32,
r1::Float32, l1::Vector{Float32},
r2::Float32, l2::Vector{Float32})
return circle(s,r2,l2) * circle(t,r1,l1)
end
end
# overload + operator
function Base.:+(a::Vector{Float32},b::Float32)
res = copy(a)
res[1] += b
return res
end
function Base.:+(a::Float32,b::Vector{Float32})
res = copy(b)
res[1] += a
return res
end
# overload - operator
function Base.:-(a::Vector{Float32},b::Vector{Float32})
return a - b
end
function Base.:-(a::Vector{Float32},b::Float32)
res = copy(a)
res[1] -= b
return res
end
function Base.:-(a::Float32,b::Vector{Float32})
res = copy(.-b)
res[1] += a
return res
end
# normalization functions
function norm(a::Vector{Float32})
return sqrt(abs((a * conjugate(a))[1]))
end
function normalize(a::Vector{Float32})
return a ./ norm(a)
end
# convert multivector fields to string
function toStr(mv::Vector{Float32})
nField = size(mv,1)
nNZField = 0
S = String[]
for iField = 1:nField
if mv[iField] != 0
if nNZField != 0
push!(S, @sprintf(" + %0.7g%s",
mv[iField], basis[iField]))
else
push!(S, @sprintf("%0.7g%s",
mv[iField],
(iField==1) ? "" : basis[iField]))
end
nNZField += 1
end
end
return string(S...)
end
# unit test
# - outputFlag can be set to false to help with benchmarking
# - nD currently does nothing because nD currently is always 3 (like in pga3d.cpp)
function utest(outputFlag::Bool=true, nD::Int64=3)
axis_z = e1 ^ e2
origin = axis_z ^ e3
px = point(1f0, 0f0, 0f0)
if outputFlag
print("point : "); println(toStr(px))
end
line = origin & px
if outputFlag
print("line : "); println(toStr(line))
end
p = plane(2f0,0f0,1f0,-3f0)
if outputFlag
print("plane : "); println(toStr(p))
end
rot = rotor(Float32(pi/2), e1*e2)
if outputFlag
print("rotor : "); println(toStr(rot))
end
rot_point = rot * px * ~rot
if outputFlag
print("rotated point : "); println(toStr(rot_point))
end
rot_line = rot * line * ~rot
if outputFlag
print("rotated line : "); println(toStr(rot_line))
end
rot_plane = rot * p * ~rot
if outputFlag
print("rotated plane : "); println(toStr(rot_plane))
end
point_on_plane = (p | px) * p
if outputFlag
print("point on plane : "); println(toStr(normalize(point_on_plane)))
end
to = torus(0f0, 0f0,
0.25f0, e1*e2,
0.6f0, e1*e3)
point_on_torus = to * e123 * ~to
if outputFlag
print("point on torus : "); println(toStr(point_on_torus))
end
if outputFlag
print("toStr() test 1 : "); println(toStr(e0 - 1f0))
print("toStr() test 2 : "); println(toStr(1f0 - e0))
end
end