Everything you want to know about the Geometric Algebra of the Euclidean plane.
This is a rough sketch of a tutorial for creating a computational representation of a multivector for Euclidean 2D space. It should be guided by an experienced mentor who would fill in the mathematical details, physics, and/or programming mechanics. The resulting computational/mathematical object will be used in future lessons.
An important part of this study is to see how the computation is performed using a coordinate representation while the mathematical and computational object is manipulated by the programmer in a way that makes no reference to coordinates. This is important because it closes the gap between the description of physical laws, which don't care about coordinates, and their study using mathematical notation and computation.
The approach taken is to first implement the features of a vector space and then add a Euclidean metric. The results are first viewed in the coordinate representation using a correspondence to compass directions, East and North. The Vector type created is then rendered geometrically using JSXGraph. We then move on to study projections and area from the point of view of Geometric Algebra, generalizing the vector object to a full multivector.
This tutorial does not explain the ins-and-outs of JavaScript, TypeScript or Web programming. It should however be accessible to an intermediate programmer or a novice with guidance.
I have avoided using class
to define objects and instead have gone with the Douglas Crockford approach. While the class
approach has a certain convenience, the Crockford approach is a worthy Best Practice in that it avoids the messiness of the this
keyword and has other advantages. This is somewhat an experiment to see if it better supports the goal of not confusing the novice while at the same time exposing the features of JavaScript that make it so powerful.
Computational Modeling can be seen as another kind of Modeling-based instruction. The computational model is another representation of the system. This representation can be seen textually and in graphical output. The similarities and differences between mathematical notation and computer code should be observed. Students should be encouraged to review each other's code constructively and suggest improvements or identify errors. Students can work in teams, as pairs, or individually.
The development here is not axiomatic (the axiomatic approach appears to the author to be more sensible in the context of mathematics). Instead, it is operational and perhaps more intuitive (which appears to the author to be more sensible for physics). The downside is that it connects with a specific kind of vector space (real space vectors), and might have to be re-analyzed for other types. The advantage is that it can be seen that mathematics is being constructed to be useful in describing physics and the reasons for any assumptions are more obvious. For example,
It's customary in any new programming environment to create the simplest possible program that tells you that the environment is working correctly.
Let's do that, and create a useful utility at the same time!
Modify the index.html
file to add a pre
element.
<body>
<pre id='viewer'></pre>
<script>
Add the following lines to the index.ts
file.
function write(text) {
const pre = document.getElementById('viewer')
pre.innerHTML += text + '\n'
}
write('Hello, World!')
Now click the Launch Program
icon.
You should see the text Hello, World!
in the output window. Yay!
The write
function can be used at any time to view the result of an expression evaluation.
We're going to start by using the computer to create a representation of a geometric object called a directed line segment or vector. This object could represent a movement of a certain distance in a certain direction, but it has other powerful uses in Mathematics, Physics and Geometry. By creating a computational representation, we can have the computer do all the tedious calculations required to solve problems in Physics, Geometry and Mathematics. We'll build on this representation to create a powerful computational structure that is general enough to handle any kind of geometric problem.
What kind of things do we know about are vectors? What are the properties of vectors?
We'll start by creating a simple function that creates an object.
function vec() {
return {};
}
write(vec())
You should see [object Object]
in the output window. Not very impressive, but at least we are creating an object.
Now here's the idea. We are going to represent a single step to the East (or Right or x-direction) by the number 1
. Two steps by the number 2
, etc. We also want to keep track of the number of steps North (or Up or y-direction). So we'll need two numbers and we'll need to make sure they don't get mixed up. So, let's call them x
and y
.
Modify the vec
function so that it can receive both the x
and y
values:
function vec(x: number, y: number) {
return {};
}
You will need to modify the call to the vec
function. Let's create some variables to represent East and North and display them:
function vec(x: number, y: number) {
const toString = function() {
return `${x} E + ${y} N`;
};
return {
toString
};
}
const E = vec(1, 0);
const N = vec(0, 1);
write(`E => ${E}`);
write(`N => ${N}`);
You should see E => 1 E + 0 N
and N => 0 E + 1 N
.
Clearly, we can improve the toString
method of the object returned by the vec
function. This will be left as an exercise.
Now let's add E and N together and see what we get!
write(`E + N => ${E + N}`);
The result, E + N => 1 E + 0 N0 E + 1 N
, is a bit perplexing!
What's going on here is that the JavaScript runtime, not knowing how to properly add two vectors, is individually converting the E and N objects to strings then concatenates them together.
We'll fix this adding a special method, __add__
, to our vector object that performs addition, as well as getter properties that provide the x
and y
properties required by the add function. You will also need to check the Operator Overloading option in the Settings dialog.
function vec(x: number, y: number) {
const toString = function() {
return `${x} E + ${y} N`;
}
const __add__ = function(rhs) {
return vec(x + rhs.x, y + rhs.y)
}
const obj = {
get x() {return x;},
get y() {return y;},
toString,
__add__
};
return obj;
}
You should now see E + N => 1 E + 1 N
.
Implement subtraction using the special __sub__
method. Test your implementation by examining the results of the following operations:
E - N
E - E
N - E
N - N
.
It's rather inconvenient to achieve 3 steps to the East by computing E + E + E
. Furthermore, what it we want to define a fractional number of steps? We need to be able to multiply by a number.
Suppose we try E * 2
. We'll need the following special method to make this expression computable:
const __mul__ = function(rhs) {
if (typeof rhs === 'number') {
return vec(x * rhs, y * rhs)
}
else {
throw new Error(`rhs must be a number.`)
}
}
Add this function and don't forget to expose it on the obj
variable.
We now consider the expression 2 * E
. This requires a slightly different special method because the number appears on the left-hand-side of the vector. Here is the right-multiplication method with an empty implementation:
const __rmul__ = function(lhs) {
// Exercise: Complete the implementation.
}
With addition, subtraction, and scalar multiplication we have met most of the requirements for our representation to be called a vector.
What is a vector space? What are the Peano axioms? What have we implemented so far?
You should be able to perform some more complex expressions such as 3 * E + 4 * N
.
Our vector is not yet able to tell us its magnitude. You may already know that the length of the hypoteneuse of a right angled triangle in everyday (Euclidean) space can be computed from the length of the other two sides.
Implement a magnitude
method that returns the length of the vector. You may need to use the built-in JavaScript Math
library to obtain a sqrt
function.
Implement a direction
method that returns a vector that has been scaled so that its length is 1
. The direction
method should maintain the direction of the vector so that the following expression is true:
v.magnitude() * v.direction()
is the same as v
.
A vector exists independently of any coordinate system but we have to choose on in order to compute. What does it mean to perform coordinate-free calculations if the implementation is using coordinates underneath?
Is the concept of magnitude independent of the coordinate system? Is the concept of direction independent of the coordinate system. Would it be better to consider relative measures?
We can define an interface which represents the type of the vector. Here is a starting example.
export interface Vector {
x: number;
y: number;
magnitude: () => number;
direction: () => Vector;
toString: () => string;
}
Use this interface to constrain the implementation.
Create a new file called Vector.ts
and move the code for both the interface Vector
and the function vec
to that file. Add the keyword export
before both the interface
and function
keywords. Adding the export
keyword makes your file into a module whose exports can be imported elsewhere. Returning to the index.ts
file, notice that there are now errors.
At the top of index.ts
, add the following import statement:
import {} from './Vector';
Now place the cursor between the curly braces of the import statement and press the Ctrl-Spacebar
keys together. Select the vec
export so that the import now reads:
import {vec} from './Vector';
The errors should go away and your output should work again.
-
Check the JSXGraph checkbox in the Settings dialog.
-
Make the following code changes to the
index.html
andindex.ts
files.
<div id='box' class='jxgbox' style='width:500px; height:500px'></div>
<ul>
<li>
<a href='http://jsxgraph.uni-bayreuth.de' target='_blank' class='JXGtext'>JSXGraph Home Page</a>
</li>
</ul>
<pre id='viewer'></pre>
import {vec} from './Vector';
const E = vec(1, 0)
const N = vec(0, 1)
const board = JXG.JSXGraph.initBoard('box', {
axis: true,
boundingbox: [-1, 2, 2, -1],
showNavigation: false
});
board.suspendUpdate();
const eastArrow = board.create('line', [[0, 0], [E.x, E.y]], {
straightFirst: false,
straightLast: false,
lastArrow: true,
strokeColor: '#ff0000',
strokeWidth: 2
});
const northArrow = board.create('line', [[0, 0], [N.x, N.y]], {
straightFirst: false,
straightLast: false,
lastArrow: true,
strokeColor: '#0000ff',
strokeWidth: 2
});
board.unsuspendUpdate();
function write(text) {
const pre = document.getElementById('viewer')
pre.innerHTML += text + '\n'
}
You should now be viewing a graph with a red arrow on it connecting the origin point [0, 0] to [1, 0], and a blue arrow from the origin to [0, 1].
The code for creating an Arrow is duplicated and should be extracted into a function. In the following solution I have also renamed E
to e1
and N
to e2
. This naming convention is more general than East and North and will also allow us to move into the third dimension!
import {vec, Vector} from './Vector';
const board = JXG.JSXGraph.initBoard('box', {
axis: true,
boundingbox: [-1, 2, 2, -1],
showNavigation: false
});
function arrow(v: Vector, strokeColor: string) {
return board.create('line', [[0, 0], [v.x, v.y]], {
straightFirst: false,
straightLast: false,
lastArrow: true,
strokeColor,
strokeWidth: 2
});
}
const e1 = vec(1, 0)
const e2 = vec(0, 1)
board.suspendUpdate();
arrow(e1, '#ff0000')
arrow(e2, '#0000ff')
arrow(2 * e1 + e2, '#00ff00')
board.unsuspendUpdate();
function write(text) {
const pre = document.getElementById('viewer')
pre.innerHTML += text + '\n'
}
Before continuing, let's move the e1
and e2
constants into the Vector
module, export them from there and import them in index.ts
.
You might find it interesting that we can now rename e1
and e2
to suit the problem in hand...
import {vec, Vector, e1 as E, e2 as N} from './Vector';
import {vec, Vector, e1 as i, e2 as j} from './Vector';
We have been adding vectors to vectors and multiplying vectors by numbers. Both these operations produce another vector. We can ask whether it might be possible to multiply vectors and what sort of objects would this multiplication create? Would this be useful?
Multiplication turns out to be the building block from which we can construct other operations such as reflections and rotations. In some sense it is the holy grail of a long struggle to define a mathematical notation for geometric operations. Unfortunately, this is not widely known.
Before attempting to define how vector multiplication should work, we take a look at two geometric operations and their symmetries.
Suppose that we try to write a function that gives the projection of one vector onto another. We might start by writing something like the following:
/**
* Projection of b onto a
*/
function proj(b: Vector, a: Vector): Vector {
// Magic happens in here!
}
Show that
By drawing a diagram, you should convince yourself that the
$proj(\hat{b},\hat{a}) = proj(\hat{a},\hat{b})$
Observe that, if vector multiplication were possible, the expression proj
function.
Suppose we try to write a function that gives the area of a parallelogram created from vectors
/**
* Area of parallelogram defined by a and b
*/
function area(a: Vector, b: Vector): Area {
// Magic happens in here!
}
Notice that the return type of the function is some new Area
quantity that is to be defined. We imagine that the area
function should have a property that reflects a counter-clockwise or clockwise orientation determined by the ordering and direction of the input vectors. For example area(e1, e2)
would have a counter-clockwise orientation.
Show that
By drawing a diagram, you should convince yourself that the
Mathematically, we would say that the
Observe that, if vector multiplication were possible, the expression area
function.
Let's assume that vector multiplication is possible and that it has most of the normal properties of the algebra of ordinary numbers. The one exception is that we will not assume that the product
We want to make a connection between multiplication and the ideas about symmetry for projection and area.
Since, projection and area are two different concepts, we start by simply dividing
We'll make the first term on the right symmetric wrt vector interchange by adding a swapped quantity and the second term antisymmetric by subtracting the same quantity. All in all, we will have added nothing and so the mathematical statement will still be true.
Now we see something remarkable. The product of two vectors decomposes into two parts, one symmetric wrt vector interchange, the other antisymmetric wrt vector interchange. This makes a connection between vector multiplication and the concepts of projection and area.
To nail down our definition of vector multiplication we must specify what it means in terms of our proj
and area
functions.
We implicitly define symmetric multiplication of vectors by
As a consequence of this definition show that
Finally, rearrange the above statement to show
We implicitly define asymmetric multiplication of vectors by
And so we have our complete definition of vector multiplication in terms of the geometric concepts of proj
and area
.
I've just realized that I can bypass all the symmetry stuff by writing
We now recognize that
A source of confusion is that ab appears to contain two quantities that don't seem to mix. One looks like a number, the other like a directed area. However, it violates nothing in mathematics. So what is going on? Some clarity may be provided by the following analysis.
Let
This quantity is unchanged by rotations in the plane (and would also be unchanged by rotations were
Let
This quantity is certainly not a scalar because its aspect is a plane (through the origin). In 2D it would not change under a rotation transformation, but in higher dimensions it could. It also would change under reflections. It obviously isn't a vector. We call it a bivector (or pseudoscalar in 2D). So
$ab = $ scalar area
This may seem a bit pedantic, but it is a potential obstacle and/or it's not immediately obvious that the quantities on the right should have the same units. But they are both areas and must have the same units. They don't have to have the same grade.
More generally, we might not speak of area but rather a product of two units or two dimensions specifiers.
Show
Looking at the expression on the right, it should be clear that by multiplying two vectors we have created a number and a signed area. We can represent this easily in our code as follows. Just as we had properties x
and y
to represent a vector, we add two new properties a
and b
to represent the number part and the directed area part respectively.
Just as
Mathematically, we use the word closed to describe an algebra that stops producing new elements after multiplying all the known existing elements together. The word algebra, when used correctly, means the rules governing the multiplication of vectors in a vector space. Just sayin'.
Since
A vector is just a special kind of multivector with the a
and b
properties set to zero. This provides the clue as to how we should proceed. We'll take our existing vector representation and expand it. A good name for our new multivector type would be G2
. It's short and expresses that our multivector is a Geometric object constructed from a 2D space.
When we prepare our code for an enhancement to the design, usually to make it more flexible, we call the process refactoring.
First, I've taken the Vector.ts
file and renamed it to G2.ts
. Then, I renamed the Vector
interface to G2
and fixed up any old broken references. The vec
function has had its parameter list expanded to include a
and b
and has been renamed to multivector
. Finally, for convenience and backwards compatibility I created and exported a vec
function that creates a vector using the multivector
function.
I haven't changed any of the the implementation code yet.
export interface G2 {
a: number;
x: number;
y: number;
b: number;
magnitude: () => number;
direction: () => G2;
}
export function vec(x: number, y: number): G2 {
return multivector(0, x, y, 0);
}
export function multivector(a: number, x: number, y: number, b: number): G2 {
const direction = function() {
const L = magnitude();
return vec(x / L, y / L);
}
const magnitude = function() {
return Math.sqrt(x * x + y * y);
}
const toString = function() {
return `[${x}, ${y}]`;
}
const __add__ = function(rhs: G2) {
return vec(x + rhs.x, y + rhs.y)
}
const __sub__ = function(rhs: G2) {
return vec(x - rhs.x, y - rhs.y)
}
const __mul__ = function(rhs) {
if (typeof rhs === 'number') {
return vec(x * rhs, y * rhs)
}
else {
throw new Error(`mul(rhs): rhs must be a number.`)
}
}
const __rmul__ = function(lhs) {
if (typeof lhs === 'number') {
return vec(lhs * x, lhs * y)
}
else {
throw new Error(`lhs must be a number.`)
}
}
const obj = {
get a() {return a;},
get x() {return x;},
get y() {return y;},
get b() {return b;},
direction,
magnitude,
toString,
__add__,
__sub__,
__mul__,
__rmul__
};
return obj;
}
export const e1 = vec(1, 0);
export const e2 = vec(0, 1);
You will also need to fix up broken references in script.ts
. Make sure that your code is running as before before proceeding.
We talked about how the multivector G2 is the most general type of geometric object that exists in Euclidean 2D space. This means that have some work to do to expand our vector into being a multivector. We'll have to revisit addition and subtraction. We'll also have to expand multiplication to support not only numbers but other G2 multivectors.
In programming circles there is a style of programming called Test-Driven Development. It works like this. The developer first writes a test for some code that may not have been developed yet. If the test passes, a more complex test is written. If the test fails, the code is fixed. This cycle proceedes until all the required features for the code have been implemented. A variant on this is called Behavior-Driven Development.
Our G2 type is becoming more complicated and we certainly want it to work flawlesly in future applications. So we'll write tests as we develop and so that we can re-test easily in future to ensure that our code does not regress.
We'll use a framework called Jasmine. Ensure that the Jasmine dependency is checked in the Settings dialog. Your project should already contain a tests.html
file and a tests.ts
file.
Create a G2.spec.ts
file to contain the test specifications for your G2
type. The following code is a starting example that tests the exported e1
constant.
import {e1, e2, G2, multivector, vec} from './G2'
export default function() {
describe("multivector(a, x, y, b)", function() {
const a = Math.random();
const x = Math.random();
const y = Math.random();
const b = Math.random();
const M = multivector(a, x, y, b);
it("a, x, y, b properties should correct", function() {
expect(M.a).toBe(a)
expect(M.x).toBe(x)
expect(M.y).toBe(y)
expect(M.b).toBe(b)
})
})
describe("e1", function() {
it("scalar coordinate should be zero", function() {
expect(e1.a).toBe(0)
})
it("vector coordinates should be [1, 0]", function() {
expect(e1.x).toBe(1)
expect(e1.y).toBe(0)
})
it("bivector coordinate should be zero", function() {
expect(e1.b).toBe(0)
})
})
}
This specification and any others are called by the test harness code in tests.ts
import G2 from './G2.spec'
window['jasmine'] = jasmineRequire.core(jasmineRequire)
jasmineRequire.html(window['jasmine'])
const env = jasmine.getEnv()
const jasmineInterface = jasmineRequire.interface(window['jasmine'], env)
extend(window, jasmineInterface)
const htmlReporter = new jasmine.HtmlReporter({
env: env,
getContainer: function() { return document.body },
createElement: function() { return document.createElement.apply(document, arguments) },
createTextNode: function() { return document.createTextNode.apply(document, arguments) },
timer: new jasmine.Timer()
})
env.addReporter(htmlReporter)
DomReady.ready(function() {
htmlReporter.initialize()
describe("G2", G2)
env.execute()
})
/*
* Helper function for extending the properties on objects.
*/
export default function extend<T>(destination: T, source: any): T {
for (let property in source) {
destination[property] = source[property]
}
return destination
}
I won't describe how this works; you can find more information on the Jasmine website or by digging into the code. Just note the import of the specification(s) at the top and the function call to describe the G2
specification that is called when the DOM has been loaded.
You should now be able to execute the tests by using the Choose Program option and selecting tests.html
.
Write a specification in G2.spec.ts
to test the e2
constant.
Write a specification in G2.spec.ts
to test addition of arbitrary multivectors then fix the code until the test passes. Here's a specification example.
describe("addition", function() {
const M1 = multivector(Math.random(), Math.random(), Math.random(), Math.random());
const M2 = multivector(Math.random(), Math.random(), Math.random(), Math.random());
const M = M1 + M2;
it("scalar property should correct", function() {
expect(M.a).toBe(M1.a + M2.a)
})
it("vector property should correct", function() {
expect(M.x).toBe(M1.x + M2.x)
expect(M.y).toBe(M1.y + M2.y)
})
it("pseudo property should correct", function() {
expect(M.b).toBe(M1.b + M2.b)
})
})
Complete the G2 implementation! For convenience, you probably should create constants to represent zero
, one
, and I
(the unit of area). Ensure that you test every combination of multiplying all the constants. Verify both the mul and rmul functions.
Be systematic in organizing your tests. This will be a lot of work, but when you are done you will have a powerful and robust computational tool for 2D numerical modeling.
Generalize the magnitude
and direction
methods so that they give sensible results for all multivectors.
The STEMCstudio documentation describes other operators that can be implemented by types like G2
. Review the documentation and implement others such as unary minus.
How would the task of programming a G3, G1 or G0 compare to G2? Consider the number of basis vectors, the number of algebra elements, and the number of arithmetic operations.