Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active July 7, 2020 20:56
Show Gist options
  • Save mathdoodle/0c250c4b094358261121088e8b0fed4c to your computer and use it in GitHub Desktop.
Save mathdoodle/0c250c4b094358261121088e8b0fed4c to your computer and use it in GitHub Desktop.
Book Ch1: Euclidean Plane Geometry Intro

Euclidean Plane Geometry Intro

Everything you want to know about the Geometric Algebra of the Euclidean plane.

Overview

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.

Programming Remarks

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.

Modeling Remarks

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.

Geometric Algebra Remarks

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, $\mathbf{a}^2 = |\mathbf{a}|^2$ follows directly as a consequence of defining symmetric vector multiplication in terms of projection. It is not an axiom with no apparent rhyme or reason!

Hello, World!

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.

Geometry done by a Computer.

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.

Discussion

What kind of things do we know about are vectors? What are the properties of vectors?

The basic Vector object.

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.

Using a pair of numbers to represent a 2D vector.

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.

Addition of vectors.

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.

Exercise

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.

Scalar Multiplication

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.
}

A vector space

With addition, subtraction, and scalar multiplication we have met most of the requirements for our representation to be called a vector.

Discussion

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.

A metric space

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.

Discussion

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?

Discussion

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?

Adding an interface for type safety and documentation.

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;
}

Exercise

Use this interface to constrain the implementation.

Moving the Vector interface and vec function to a module.

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.

Using JSXGraph to visualize our Vector.

  1. Check the JSXGraph checkbox in the Settings dialog.

  2. Make the following code changes to the index.html and index.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';

Linear Algebra to Geometric Algebra

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.

Projection and Symmetry wrt vector swap.

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

$$ proj_\mathbf{a}(\mathbf{b}) = \frac{a_x b_x + a_y b_y}{|\mathbf{a}|} \hat{\mathbf{a}} $$

By drawing a diagram, you should convince yourself that the $proj$ function has the following properties:

  1. $proj(\hat{b},\hat{a}) = proj(\hat{a},\hat{b})$

Observe that, if vector multiplication were possible, the expression $(\mathbf{a} \mathbf{b} + \mathbf{b} \mathbf{a})$ would have this property. So something like this expression would be a candidate for the implementation of the proj function.

Area and Asymmetry wrt vector swap

Suppose we try to write a function that gives the area of a parallelogram created from vectors $\vec{a}$ and $\vec{b}$.

/**
 * 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

$$ area(a,b) = (a_x b_y - a_y b_x) area(\mathbf{e_1}, \mathbf{e_2}) $$

By drawing a diagram, you should convince yourself that the $area$ function has the following property:

$area(\vec{b},\vec{a}) = -area(\vec{a},\vec{b})$

Mathematically, we would say that the $Area$ type is an oriented (signed) quantity and the $area$ function is antisymmetric wrt interchange (swap) of its input vectors.

Observe that, if vector multiplication were possible, the expression $(\vec{a} \vec{b} - \vec{b} \vec{a})$ would have this property. So something like this expression would be a candidate for the implementation of the area function.

Multiplication has Symmetric and Antisymmetric parts

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 $\vec{a} \vec{b}$ is the same as $\vec{b} \vec{a}$. In mathematical language, we don't assume that vector multiplication is commutative.

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 $\vec{a} \vec{b}$ into two equal parts:

$$ \vec{a} \vec{b} = \frac{\vec{a} \vec{b}}{2} + \frac{\vec{a} \vec{b}}{2} $$

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.

$$ \vec{a} \vec{b} = \frac{(\vec{a} \vec{b} + \vec{b} \vec{a})}{2} + \frac{(\vec{a} \vec{b}-\vec{b} \vec{a})}{2} $$

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

$$ proj(b,a) = (\frac{ab+ba}{2}) \frac{\hat{a}}{|\mathbf{a}|} $$

As a consequence of this definition show that $\mathbf{a}^2 = |\mathbf{a}|^2$

Finally, rearrange the above statement to show

$$ \frac{ab+ba}{2} = proj(b,a) a = proj(a,b) b $$

We implicitly define asymmetric multiplication of vectors by

$$ area(a, b) = \frac{ab-ba}{2} $$

And so we have our complete definition of vector multiplication in terms of the geometric concepts of proj and area.

$$ ab = proj(b,a) a + area(a, b) $$

I've just realized that I can bypass all the symmetry stuff by writing

$$ ab = a(b_\parallel + b_\perp) $$

We now recognize that

$$ b_\parallel = proj(b,a) $$

$$ a b_\perp = area(a, b) $$

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

$$ area_\parallel(a,b) = proj(b,a) a $$

This quantity is unchanged by rotations in the plane (and would also be unchanged by rotations were $a$ and $b$ in a vector space of more than two dimensions). It is also not changed by reflections, so

$proj(b, a) a$ is a scalar area

Let

$$ area_\perp(a,b) = a b_\perp $$

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

$a b_\perp$ is a bivector area

$ab = $ scalar area $+$ bivector 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.

The geometric product in a computational representation.

Show

$$ ab = (a_x e1 + a_y e2)(b_x e1 + b_y e2) = (a_x b_x + a_y b_y) + (a_x b_y - a_y b_x) e1e2 $$

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

$\vec{v}$ corresponds to $[x,y]$ means $\vec{v} = x e_1 + y e_2$,

$M$ corresponds to $[a,x,y,b]$ means $M = a + x e_1 + y e_2 + b e_1 e_2$

$M$ is what we call a Multivector. You might think that we could go on multiplying a vector by a multivector to produce even more exotic mathematical objects, but, as we shall discover, we need go no further. Think of $M$ as the most general kind of geometric object that we can create in a Euclidean vector space that is spanned by two vectors.

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'.

Expanding a Vector into a Multivector.

Since

$M$ corresponds to $[a,x,y,b]$ means $M = a + x e_1 + y e_2 + b e_1 e_2$,

$\vec{v}$ corresponds to $[0,x,y,0]$ means $\vec{v} = x e_1 + y e_2$.

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.

Refactoring Vector to make way for G2.

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.

Closing the Algebra in G2

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.

Test-Driven Development (TDD)

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.

Exercise

Write a specification in G2.spec.ts to test the e2 constant.

Exercise

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)
    })
})

Exercise

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.

Exercise

Generalize the magnitude and direction methods so that they give sensible results for all multivectors.

Exercise

The STEMCstudio documentation describes other operators that can be implemented by types like G2. Review the documentation and implement others such as unary minus.

Discussion

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.

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("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("toString", function() {
const M = multivector(2, 3, 5, 7);
it("format should be [a, x, y, b]", function() {
expect(M.toString()).toBe('[2, 3, 5, 7]')
})
})
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)
})
})
describe("e2", function() {
it("scalar coordinate should be zero", function() {
expect(e2.a).toBe(0)
})
it("vector coordinates should be [0, 1]", function() {
expect(e2.x).toBe(0)
expect(e2.y).toBe(1)
})
it("bivector coordinate should be zero", function() {
expect(e2.b).toBe(0)
})
})
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)
})
})
describe("subtraction", 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)
})
})
describe("multiplication: e1 * e2", function() {
const M = e1 * e2;
it("scalar property should correct", function() {
expect(M.a).toBe(0)
})
it("vector property should be zero", function() {
expect(M.x).toBe(0)
expect(M.y).toBe(0)
})
it("pseudo property should correct", function() {
expect(M.b).toBe(1)
})
})
describe("multiplication: e1 * e1", function() {
const M = e1 * e1;
it("scalar property should correct", function() {
expect(M.a).toBe(1)
})
it("vector property should be zero", function() {
expect(M.x).toBe(0)
expect(M.y).toBe(0)
})
it("pseudo property should correct", function() {
expect(M.b).toBe(0)
})
})
describe("multiplication: e2 * e2", function() {
const M = e2 * e2;
it("scalar property should correct", function() {
expect(M.a).toBe(1)
})
it("vector property should be zero", function() {
expect(M.x).toBe(0)
expect(M.y).toBe(0)
})
it("pseudo property should correct", function() {
expect(M.b).toBe(0)
})
})
describe("multiplication: e2 * e1", function() {
const M = e2 * e1;
it("scalar property should correct", function() {
expect(M.a).toBe(0)
})
it("vector property should be zero", function() {
expect(M.x).toBe(0)
expect(M.y).toBe(0)
})
it("pseudo property should correct", function() {
expect(M.b).toBe(-1)
})
})
describe("multiplication: M * number", function() {
const L = multivector(2, 3, 5, 7)
const R = 11;
const M = L * R;
it("scalar property should correct", function() {
expect(M.a).toBe(22)
})
it("vector property should be zero", function() {
expect(M.x).toBe(33)
expect(M.y).toBe(55)
})
it("pseudo property should correct", function() {
expect(M.b).toBe(77)
})
})
describe("multiplication: number * M", function() {
const L = 11;
const R = multivector(2, 3, 5, 7)
const M = L * R;
it("scalar property should correct", function() {
expect(M.a).toBe(22)
})
it("vector property should be zero", function() {
expect(M.x).toBe(33)
expect(M.y).toBe(55)
})
it("pseudo property should correct", function() {
expect(M.b).toBe(77)
})
})
}
export interface G2 {
a: number;
x: number;
y: number;
b: number;
magnitude: () => number;
direction: () => G2;
toString: () => string;
}
export function scalar(a: number): G2 {
return multivector(a, 0, 0, 0);
}
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 isG2 = function(M: G2): boolean {
return typeof M.a === 'number' && typeof M.x === 'number' && typeof M.y === 'number' && typeof M.b === 'number'
}
const magnitude = function() {
return Math.sqrt(x * x + y * y);
}
const toString = function() {
return `[${a}, ${x}, ${y}, ${b}]`;
}
const __add__ = function(rhs: G2) {
return multivector(a + rhs.a, x + rhs.x, y + rhs.y, b + rhs.b);
}
const __sub__ = function(rhs: G2) {
return multivector(a - rhs.a, x - rhs.x, y - rhs.y, b - rhs.b);
}
const __mul__ = function(rhs: number | G2) {
if (typeof rhs === 'number') {
return multivector(a * rhs, x * rhs, y * rhs, b * rhs)
}
else if (isG2(rhs)) {
return multivector(x * rhs.x + y * rhs.y, 0, 0, x * rhs.y - y * rhs.x);
}
else {
throw new Error(`mul(rhs): rhs must be a number or G2.`);
}
}
const __rmul__ = function(lhs: number | G2) {
if (typeof lhs === 'number') {
return multivector(lhs * a, lhs * x, lhs * y, lhs * b);
}
else {
throw new Error(`lhs must be a number or G2.`);
}
}
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 zero = multivector(0, 0, 0, 0);
export const one = scalar(1);
export const e1 = vec(1, 0);
export const e2 = vec(0, 1);
export const I = multivector(0, 0, 0, 1);
<!DOCTYPE html>
<html>
<head>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<script src='https://jspm.io/system.js'></script>
<!-- SHADERS-MARKER -->
<!-- SCRIPTS-MARKER -->
</head>
<body>
<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>
<script>
// CODE-MARKER
</script>
<script>
System.import('./index.js')
</script>
</body>
</html>
import { G2, one, e1, e2, I } from './G2';
const board = JXG.JSXGraph.initBoard('box', {
axis: true,
boundingbox: [-1, 2, 2, -1],
showCopyright: false,
showNavigation: false
});
function arrow(v: G2, strokeColor: string) {
return board.create('line', [[0, 0], [v.x, v.y]], {
straightFirst: false,
straightLast: false,
lastArrow: true,
strokeColor,
strokeWidth: 2
});
}
board.suspendUpdate();
arrow(e1, '#ff0000')
arrow(e2, '#0000ff')
arrow(e1 + e2, '#00ff00')
board.unsuspendUpdate();
function write(text: string) {
const elementId = "viewer"
const pre = document.getElementById(elementId)
if (pre instanceof HTMLElement) {
pre.innerHTML += text + '\n'
}
else {
throw new Error(`${elementId} is not a valid HTMLElement`)
}
}
try {
write(`${2 * one + 3 * e1 + 5 * e2 + 7 * I}`)
}
catch (e) {
write(e)
}
{
"description": "Book Ch1: Euclidean Plane Geometry Intro",
"dependencies": {
"DomReady": "1.0.0",
"jasmine": "3.4.0",
"jsxgraph": "1.3.5"
},
"name": "",
"version": "",
"operatorOverloading": true,
"author": "David Geo Holmes",
"keywords": [
"JSXGraph",
"Vectors",
"2D",
"magnitude",
"direction",
"GA",
"Multivector",
"Euclidean"
]
}
body {
background-color: white;
}
<!DOCTYPE html>
<html>
<head>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<script src='https://jspm.io/system.js'></script>
<!-- SCRIPTS-MARKER -->
</head>
<body>
<script>
// CODE-MARKER
</script>
<script>
System.import('./tests.js')
</script>
</body>
</html>
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
}
{
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"jsx": "react",
"module": "system",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": false,
"skipLibCheck": true,
"sourceMap": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"target": "es5",
"traceResolution": true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment