** Note - this project is WIP **
A Geometric Algebra code generator for JavaScript with advanced symbolic optimisation and a focus on performance.
GAmphetamine is fast. Not just compared to it's predecessor ganja.js, but also compared to GA implementations for other languages. The comparison with ganja.js is however most relevant, so we've summarized some of this in the table below.
| task | Algebra | ganja.js | GAmphetamine.js | Speedup |
|---|---|---|---|---|
| 1M geometric product of bivectors | 1332.78 ms | 95.09 ms | 14.0 x | |
| 1M commutator product of bivectors | 963.82 ms | 59.73 ms | 16.1 x | |
| 10M sandwich rotor on trivector | 3491.78 ms | 589.49 ms | 5.9 x | |
| 1M bivector inverse | 3215.18 ms | 79.79 ms | 40.29 x | |
| 1M sandwich rotor on vector | 304558.73 ms | 387.16ms | 768.64 x | |
| 1M commutator product of bivectors | 305316.87 ms | 98.86ms | 3088.22 x |
Runtime performance is important, but so is the performance of the Algebra and code generation. The following table shows the time-to-first product for both ganja.js and GAmphetamine.js
| Algebra | Not Precompiled (default) |
Flat, CSE |
Precompiled Typed, CSE |
Typed, no CSE |
Ganja.js |
|---|---|---|---|---|---|
| 1.33 ms | 21.10 ms | 26.30 ms | 23.20 ms | 0.73 ms | |
| 2.37 ms | 20.30 ms | 70.40 ms | 53.60 ms | 0.86 ms | |
| 2.90 ms | 44.10 ms | 138.40 ms | 94.20 ms | 1.24 ms | |
| 3.40 ms | 203.20 ms | 359.20 ms | 200.90 ms | 2.56 ms | |
| 4.75 ms | 258.40 ms | 1533.10 ms | 539.00 ms | 7.62 ms |
Note that GAmphetamine defaults to not precompiled, which means that the actual code for the operators you use is generated at runtime, when they are first used between a given pair of types.
The key to GAmphetamine's performance is coefficient level symbolic optimisations. This feature is used internally to generate efficient code for the various operators, but is also provided via the API.
Algebra(3, ()=>Element.vector('a') | Element.vector('b') + '')
(a1*b1+a2*b2+a3*b3)
Additionally, you can request GAmphetamine to output as
Algebra(3, {printFormat : 'latex'}, ()=>{
const a = Element.vector('a_');
const b = Element.vector('b_');
console.log( String.raw`
\begin{aligned}
(${a})*(${b}) &= ${a | b} \\
&+ ${a ^ b}
\end{aligned}
`);
});produces the following output
\begin{aligned}
(a_1\mathbf e_{1} + a_2\mathbf e_{2} + a_3\mathbf e_{3})*(b_1\mathbf e_{1} + b_2\mathbf e_{2} + b_3\mathbf e_{3}) &= (a_1b_1+a_2b_2+a_3b_3)
&+ (a_1b_2-a_2b_1)\mathbf e_{12} + (a_1b_3-a_3b_1)\mathbf e_{13} + (a_2b_3-a_3b_2)\mathbf e_{23}
\end{aligned}
which renders as
GAmphetamine offers similar visualisation capabilities to ganja.js. This is WIP.
GAmphetamine offers a wide ranges of options to create your algebra. Let's review a few examples.
Mathematically, the algebras we will create are all quotient algebras of the tensor algebra with
an ideal generated by elements of the form
Where
In our first example, however, we show how you can avoid making any of those difficult choices by using one of the named algebras.
Algebra("3DPGA", ()=> ... );
// ๐โยฒ = 0
// ๐โยฒ = ๐โยฒ = ๐โยฒ = +1Here, the algebra created will be startIndex) and that it is indeed
Here is a list of these algebras and the options they set.
| name | algebra | basis |
|---|---|---|
| 2DPGA | ||
| 3DPGA | ||
| STA | lexical | |
| 2DCGA | lexical | |
| 3DCGA | lexical |
If you are ok with lexical ordering, the following syntax allows you to create an algebra directly with the required number of generators of each type.
Algebra(p, q, r, ()=> ... );
// if r โ 0
// ๐โยฒ, ..., ๐แตฃโโยฒ = 0
// eแตฃยฒ, ..., ๐แตฃโโโโยฒ = +1
// ๐แตฃโโยฒ, ... = -1
// if r = 0
// ๐โยฒ, ..., ๐โโโยฒ = +1
// ๐โยฒ, ... = -1This syntax will first create
Algebra("--0+-", ()=>{ ... });
// ๐โยฒ = ๐โยฒ = ๐โ
ยฒ = -1
// ๐โยฒ = 0
// ๐โยฒ = +1Which would create a 5 dimensional algebra where the first two and the last basis vector square
to
For even further control, you can access non-unit diagonal metrics via the options object:
Algebra({metric:[0.5,-0.5]}, ()=>{ ... });
// ๐โยฒ = 0.5
// ๐โยฒ = -0.5There are several ways to overload the default basis names. For example you could use
Algebra("STA", {startIndex : 0}, ()=>{ ... });
// ๐โยฒ = +1
// ๐โยฒ = ๐โยฒ = ๐โยฒ = -1To get a version of STA that starts with a time vector of ๐โ (matching nicely with ฮณโ). Or you can change
the order of basis vectors in a basis k-blade, and basis k-blades in a k-vector by specifying your own complete basis.
Algebra({ metric : "++", basis : ["1","e2","e1","e21"]}, ()=>{ ... });
// ๐โยฒ = ๐โยฒ = +1
// ๐โ * ๐โ = -eโโkeep in mind to keep your grades here in order, first all elements of grade 0, then 1 etc. You get full control over memory layout by specifying custom types!
By default, GAmphetamine will create a set of classes representing your vectors, bivectors, rotors, etc. Each of those clases then implements all of the geometric algebra operations you expect, with implementations optimized for each of the specific type combinations.
For a simplified approach, it is possible to have GAmphetamine create only a single multivector type. (it will
however have flat:true
Algebra("3DPGA", {flat:true}, ()=>{ ... });However, by default, GAmphetamine creates the following custom types for optimal memory use and performance
| name | description |
|---|---|
| scalar | pure scalar multivector |
| vector | |
| bivector | |
| trivector | |
| ... | ... |
| rotor | |
| study |
where the memory layout of each of these types is determined by default by the basis order. It is however possible to specify custom types with custom ordering.
Algebra(2,{types:[
{name:'scalar',layout:["1"]},
{name:'vector',layout:["e1","e2"]},
{name:'multivector',layout:["1","e12","e2","e1"]}
]}, ()=>{ ... });To create an algebra with three types, scalars, vectors and multivectors. In such an algebra the outer product between two vectors will output a multivector (as opposed to the default bivector).
note: The 'scalar' and 'multivector' types are required, and should be the first and last types defined.
By default, GAmphetamine will provide all basic geometric algebra operations between all of the defined types. Below is a list of these operators, with a short description.
| method | formula | description |
|---|---|---|
| add | ย $ a + b $ | addition |
| sub | $ a - b $ | subtraction |
| gp | $ ab $ | geometric product |
| op | $ a \wedge b $ | outer product |
| ip | $ a \cdot b $ | inner product |
| lip | $ a \rfloor b $ | left contraction |
| rip | $ a \lfloor b $ | right contraction |
| reverse | $ \tilde a $ | reverse |
| involute | $ \hat a $ | grade involution |
| conjugate | $ \hat {\tilde a} $ | Clifford conjugate |
| dual | $ a^* $ | dual |
| undual | $ a^{-*} $ | undual |
| prj | $ (a \cdot b)b^{-1} $ | projection |
| rp | $ a \vee b $ | regressive product |
| cp | $ a \times b $ | commutator product |
| norm | $ \lvert a \rvert$ | norm |
| normalized | $ \bar a $ | normalisation |
| sqrt | $ \sqrt a $ | square root |
| sw | $ -1^{st}ab\tilde a $ | sandwich |
| inverse | $ a^{-1} $ | inverse |
| cprj | $ $ | camera projection (internal) |
It is however also easy to add your own operators and methods. The most straightforward way of doing this, is to use the addMethod function exposed on Element. As an example, let us add an anti-commutator operator to all of our classes.
Algebra( "+++", ()=>{
var acp = (a,b) => 0.5*(a*b + b*a);
Element.addMethod(acp, "acp");
return 1e1.acp(1e23);
})which for this example, would ended up compiling the following function for the anti-commutator product on the last line :
function acp_vector_bivector (a,b,res=new classes.trivector()) {
const a0=a[0],a1=a[1],a2=a[2],b0=b[0],b1=b[1],b2=b[2];
res[0]=a0*b2-a1*b1+a2*b0;
return res;
}By default, GAmphetamine stores the coefficients for each of your multivector types using Float32Array typed
javascript arrays. You can easily change this to any of the other typed array versions.
Algebra(3, {baseType: Float64Array}, ()=>{ ... });For console output you can setup both the printFormat and printPrecision
Algebra(3,{printFormat: "latex", printPrecision: 5}, ()=>{ ... });
// multivector.toString will now produce LaTeX output
// with numbers printed with 5 digits behind the decimal point.| printFormat | Description | example |
|---|---|---|
latex |
Output |
\mathbf e_{12} |
ascii |
Output unicode. | ๐โโ |
By default, the code generated by GAmphetamine will perform CSE and prefetch array variables to optimize performance. This is a trade-off between that sacrifies compilation time for improved runtime performance. Both options can be controlled via the options object.
Algebra(
p, q, r,
{
CSE : true, // Common Subexpression Elimination.
prefetch : true // Prefetch Variables.
precompile : false, // Precompile all functions
},
()=>{ ... }
);Precompilation forces GAmphetamine to create all operators for all combinations of types at startup. By default, each product is only compiled when it is used for the first time. There are few reasons to ask for precompilation as it will typically generate a large amount of code that is subsequently never used. (for "3DPGA" GAmpetamine precompile creates almost 333kb of code, and takes 700ms to do so.)
Combined, the full options object looks like :
options = {
p,q,r, // metric signature in p,q,r format
metric, // metric as string with -+0 ("-+++") or array ([0,1,1,1])
basis, // custom basis to use (["1","e1","e2","e21"])
types, // an array of types of multivectors to support, and their layout.
methods, // an array of methods to add.
printPrecision, // number of significant digits to print behind the decimal separator
printFormat, // ["console"||"","latex"]
startIndex, // starting index for the first basis vector. (default is 1, or 0 if q != 0)
Cayley, // full custom cayley table.
precompile, // precompile all functions. (gets slow for big algebras, defaults to false)
flat, // use flat storage model. (full 2^n sized multivectors)
CSE, // perform extra CSE. (defaults to true)
prefetch, // prefetch mv coefficients.
debug, // store debug information (code generated, etc ..)
}The Algebras generated by GAmphetamine all support a unified set of operators
| operator | name | function call | inline syntax |
|---|---|---|---|
| addition | a.add(b) |
a + b |
|
| subtraction | a.sub(b) |
a - b |
|
| geometric product | a.gp(b) |
a * b |
|
| outer product | a.op(b) |
a ^ b |
|
| inner product | a.ip(b) |
a | b |
|
| regressive product | a.rp(b) |
a & b |
|
| left contraction | a.lp(b) |
a << b |
|
| right contraction | a.rip(b) |
a >> b |
|
| sandwich product | a.sw(b) |
a >>> b |
|
| commutator product | a.cp(b) |
||
| hodge dual | a.dual() |
!a |
|
| hodge undual | a.undual() |
||
| reverse | a.reverse() |
~a |
|
| involute | a.involute() |
||
| conjugate | a.conjugate() |
||
| grade selection | a.grade(k) |
||
| inverse | a.inverse() |
a**-1 |
|
| square root | a.sqrt() |
a**.5 |
|
| norm | a.norm() |
||
| normalize | a.normalized() |
The symbolic optimizer that GAmphetamine uses internally to generate its code is also available via the API. This
allows you to do various symbolic computations and use GAmphetamine to help you for example check your proofs or
output your
To create a multivector with named coefficients, simply feed in strings to the respective constructor functions.
Algebra(3, ()=>{
var s = Element.scalar("s");
var t = Element.vector("x","y","z");
return s*t + '';
})
// (s*x) eโ + (s*y) eโ + (s*z) eโWhile you can fully specify own names to label each of your coefficients as above, it is also possible to let GAmphetamine index your names automatically.
Algebra(3, ()=>{
var s = Element.scalar("s");
var v = Element.vector("v");
return s*v + '';
})
// (s*v1) eโ + (s*v2) eโ + (s*v3) eโTo do so simply specify only a single coefficient as in the example above for the vector [,(,{, GAmphetamine will also close those brackets for you. This is very practical for
Algebra(3, {printFormat : "latex"}, ()=>{
var a = Element.vector("a_{");
var b = Element.vector("b_{");
return a*b + '';
})
// (a_{1}b_{1}+a_{2}b_{2}+a_{3}b_{3}) + (a_{1}b_{2}-a_{2}b_{1})\mathbf e_{12} + (a_{1}b_{3}-a_{3}b_{1})\mathbf e_{13} + (a_{2}b_{3}-a_{3}b_{2})\mathbf e_{23}$$ (a_{1}b_{1}+a_{2}b_{2}+a_{3}b_{3}) + (a_{1}b_{2}-a_{2}b_{1})\mathbf e_{12} + (a_{1}b_{3}-a_{3}b_{1})\mathbf e_{13} + (a_{2}b_{3}-a_{3}b_{2})\mathbf e_{23}$$ Here the result can be directly included in your document. To help verify an expression, simply subtract the RHS from the LHS and verify the result is zero.
For example let us verify that
Algebra(3, ()=>{
var a = Element.vector("a_{");
var B = Element.bivector("B_{");
// a*B = a|b + a^B
return (a*B) - (a|B + a^B);
})+''
// 0TBC
TBC
TBC
TBC
TBC
TBC
TBC