Some integral octonions.
Version of Monday 25 April 2016.
Dave Barber's other pages.

This report offers a C++11 implementation of several kinds of integral octonions. It is largely based on John Baez' discussion, especially parts three and six thereof.

Everyone is welcome to download, modify, use, and redistribute the source code.

Like many mathematical articles, this one begins by establishing some nomenclature.

• In order to reduce ambiguity, we use the term real integers for the ordinary integers, which of course are real numbers:

{ … −3, −2, −1, 0, +1, +2, +3 … }

• The half-odds, which are not real integers, are the following set:

{ … −52, −32, −12, +12, +32, +52 … }

• It is often helpful to call the real integers half-evens to emphasize a counterpart relationship between them and the half-odds:

{ … −3, −2, −1, 0, +1, +2, +3 … } = { … −62, −42, −22, 02, +22, +42, +62 … }

• In the aggregate, the half-odds and half-evens are termed dimidia (singular dimidium):

{ … −42, −32, −22, −12, 02, +12, +22, +32 +42 … }

• The Gaussian integers are those complex numbers that use real integers for the real and imaginary parts.

There are two principal ways to define integral quaternions:

• Under the Lipschitz criterion, all four components are half-evens.
• Under the Hurwitz criterion, either all four components are half-evens, or all four components are half-odds. Half-evens and half-odds cannot be mixed.

Octonions are a generalization of quaternions, and the integral octonions studied here will contain:

• eight half-even components, or
• eight half-odd components, or
• four half-even components and four half-odd components.

If other combinations of half-even and half-odd components are attempted, multiplication will not be closed. A more general definition of integral octonions is not known to the present author.

In general, an octonion can be written as an octuple of real numbers, and consequently an integral octonion can be notated as an octuple of dimidia. We enclose the components in shallow angle brackets:

U = ⟨ u0, u1, u2, u3, u4, u5, u6, u7

Certain octonion constants have special names:

e0 = ⟨ 1, 0, 0, 0, 0, 0, 0, 0 ⟩
e1 = ⟨ 0, 1, 0, 0, 0, 0, 0, 0 ⟩
e2 = ⟨ 0, 0, 1, 0, 0, 0, 0, 0 ⟩
e3 = ⟨ 0, 0, 0, 1, 0, 0, 0, 0 ⟩
e4 = ⟨ 0, 0, 0, 0, 1, 0, 0, 0 ⟩
e5 = ⟨ 0, 0, 0, 0, 0, 1, 0, 0 ⟩
e6 = ⟨ 0, 0, 0, 0, 0, 0, 1, 0 ⟩
e7 = ⟨ 0, 0, 0, 0, 0, 0, 0, 1 ⟩

Together, these notations mean we can write the following:

U = u0 · e0 + u1 · e1 + u2 · e2 + u3 · e3 + u4 · e4 + u5 · e5 + u6 · e6 + u7 · e7

Addition and subtraction of octonions is simple:

U + V = ⟨ u0 + v0, u1 + v1, u2 + v2, u3 + v3, u4 + v4, u5 + v5, u6 + v6, u7 + v7
UV = ⟨ u0v0, u1v1, u2v2, u3v3, u4v4, u5v5, u6v6, u7v7

Multiplication by a real integer is unsurprising:

U · n = n · U = ⟨ n · u0, n · u1, n · u2, n · u3, n · u4, n · u5, n · u6, n · u7

Multiplication of two octonions is complicated: there are 480 varieties of the operation, as discussed on another page. Fortunately, any two of them are related by isomorphism, and all 480 boil down to essentially the same thing. With that in mind, we have arbitrarily selected a multiplication table with convenient patterns, numbered #406 on that other page and displayed below. Because octonion multiplication is noncommutative, it is necessary to distinguish the first factor from the second factor.

octonion
multiplication
#406
second
factor
e0e1e2e3e4e5e6e7
first
factor
e0+e0+e1+e2+e3+e4+e5+e6+e7
e1+e1e0+e4+e7e2+e6e5e3
e2+e2e4e0+e5+e1e3+e7e6
e3+e3e7e5e0+e6+e2e4+e1
e4+e4+e2e1e6e0+e7+e3e5
e5+e5e6+e3e2e7e0+e1+e4
e6+e6+e5e7+e4e3e1e0+e2
e7+e7+e3+e6e1+e5e4e2e0

Observe that e0 is the identity element for multiplication, and behaves the same as the real number 1. The other seven e constants anti-commute in multiplication, so that en · em = − em · en. However, octonions in general neither commute nor anti-commute, and there is no simple relationship between U · V and V · U. Along those lines, octonions exhibit alternate (but not full) associativity. Still, multiplication is distributive over addition.

The product U · V can be expanded this way:

 (U · V)0 = u0 · v0 − u1 · v1 − u2 · v2 − u3 · v3 − u4 · v4 − u5 · v5 − u6 · v6 − u7 · v7 (U · V)1 = (u0 · v1 + u1 · v0) + (u2 · v4 − u4 · v2) + (u5 · v6 − u6 · v5) + (u3 · v7 − u7 · v3) (U · V)2 = (u0 · v2 + u2 · v0) + (u3 · v5 − u5 · v3) + (u6 · v7 − u7 · v6) + (u4 · v1 − u1 · v4) (U · V)3 = (u0 · v3 + u3 · v0) + (u4 · v6 − u6 · v4) + (u7 · v1 − u1 · v7) + (u5 · v2 − u2 · v5) (U · V)4 = (u0 · v4 + u4 · v0) + (u5 · v7 − u7 · v5) + (u1 · v2 − u2 · v1) + (u6 · v3 − u3 · v6) (U · V)5 = (u0 · v5 + u5 · v0) + (u6 · v1 − u1 · v6) + (u2 · v3 − u3 · v2) + (u7 · v4 − u4 · v7) (U · V)6 = (u0 · v6 + u6 · v0) + (u7 · v2 − u2 · v7) + (u3 · v4 − u4 · v3) + (u1 · v5 − u5 · v1) (U · V)7 = (u0 · v7 + u7 · v0) + (u1 · v3 − u3 · v1) + (u4 · v5 − u5 · v4) + (u2 · v6 − u6 · v2)

From their behavior in multiplication, which is similar to that of complex numbers, u0 is termed the real part of octonion U, while u1 through u7 are the imaginary parts. For instance, (u0)2 = +1 and (u1)2 = −1. Note that (u2)n and (un)2 are not the same thing.

The inner product of two octonions is:

inner (U, V) = u0 · v0 + u1 · v1 + u2 · v2 + u3 · v3 + u4 · v4 + u5 · v5 + u6 · v6 + u7 · v7

We use the square magnitude for the norm: || U || = inner (U, U). Importantly, || U || · || V || equals || U·V ||.

A nonstandard octonion operation valuable here is rotation. Imaginary components are moved to the right, and cycle around. The real component is unaffected. Examples:

 U = ⟨ u0, u1, u2, u3, u4, u5, u6, u7 ⟩ rotate_imag (U, +1) = ⟨ u0, u7, u1, u2, u3, u4, u5, u6 ⟩ rotate_imag (U, +2) = ⟨ u0, u6, u7, u1, u2, u3, u4, u5 ⟩ rotate_imag (U, −1) = ⟨ u0, u2, u3, u4, u5, u6, u7, u1 ⟩

Further,

rotate_imag (rotate_imag (U, m), n) = rotate_imag (U, m + n)

rotate_imag (U, 0) = rotate_imag (U, ±7) = U

The supplied computer program supports several classes of integral octonions:

 class components output tag comments baseclass class octonion • 8 half-evens • 8 half-odds • 4 half-evens and 4 half-odds octo This is the most general case. derivedclasses struct graves : public octonion • 8 half-evens grav This corresponds to the Lipschitz quaternions. struct klein : public octonion • 8 half-evens • 8 half-odds klei struct lattice : public octonion • 8 half-evens • 8 half-odds latt This represents lattice E8. This is like klein with the additional requirement that the sum of all components be an even real integer. struct cayley_n : public octonion • 8 half-evens • 4 half-evens and 4 half-odds cay0cay1cay2etc These use a different set of octonions for each value of n for zero through seven. The half-odds when present must conform to certain patterns that depend on the value of n.

The output tag is included in each octonion that the program displays, because otherwise octonions of all classes would be printed the same.

An example of a klein object that is not a lattice object, because the sum is an odd real integer, is ⟨ +12, −12, +12, −12, +12, −12, +12, +12 ⟩.

Each of the derived classes restricts values to a subset of those available to the base class, but nearly all the calculations work the same. Hence the derived classes do little more than verify inputs, leaving the main work work to the base class. The class structure is not polymorphic, because it does not need to be — in fact, slicing is used intentionally.

Although the first four classes listed are fairly simple, cayley_n is more complicated because it is in fact a parameterization of eight classes. The program uses class cayley_0 to represent what are called the Kirmse integers, which are not closed under multiplication. However, there is a simple way (sometimes in the literature called a "trick") to modify cayley_0 so as to yield cayley_1 through cayley_7, which are closed. Here is how that works:

• Start with the set of all cayley_0 octonions. In each, exchange the first (subscript 0) and second (subscript 1) components. What results is the set of cayley_1 octonions.
• Go back to the set of all cayley_0 octonions. In each, exchange the first (subscript 0) and third (subscript 2) components. What results is the set of cayley_2 octonions.
• This swap-with-real mechanism applies to the other five imaginary components as well.

Even more, classes cayley_1 through cayley_7 are related by rotation. With a rotate_imag parameter of +1, an octonion of type cayley_1 becomes a cayley_2, a cayley_4 becomes a cayley_5, a cayley_7 becomes a cayley_1, et cetera. Meanwhile, a cayley_0 rotates into a cayley_0. If a multiplication rule other than #406 is chosen, all of this might no longer be true.

The table below contains the patterns of half-even and half-odd components governing each of the cayley_n. The two patterns within one cell of the table have opposite placement of half-evens and half-odds.

 used incayley_n patternse = half-even, o = half-odd { e e e e e e e e } { o o o o o o o o } all { e e e e o o o o } { o o o o e e e e } 6 { e e e o e o o o } { o o o e o e e e } 0, 1, 2, 4 { e e e o o e o o } { o o o e e o e e } 7 { e e e o o o e o } { o o o e e e o e } 3 { e e e o o o o e } { o o o e e e e o } 5 { e e o e e o o o } { o o e o o e e e } 5 { e e o e o e o o } { o o e o e o e e } 4 { e e o e o o e o } { o o e o e e o e } 2 { e e o e o o o e } { o o e o e e e o } 0, 1, 3, 7 { e e o o e e o o } { o o e e o o e e } 3 { e e o o e o e o } { o o e e o e o e } 7 { e e o o e o o e } { o o e e o e e o } 6 { e e o o o e e o } { o o e e e o o e } 0, 1, 5, 6 { e e o o o e o e } { o o e e e o e o } 2 { e e o o o o e e } { o o e e e e o o } 4 { e o e e e o o o } { o e o o o e e e } 7 { e o e e o e o o } { o e o o e o e e } 0, 2, 3, 5
 used incayley_n patternse = half-even, o = half-odd { e o e e o o e o } { o e o o e e o e } 1 { e o e e o o o e } { o e o o e e e o } 4 { e o e o e e o o } { o e o e o o e e } 6 { e o e o e o e o } { o e o e o e o e } 5 { e o e o e o o e } { o e o e o e e o } 3 { e o e o o e e o } { o e o e e o o e } 4 { e o e o o e o e } { o e o e e o e o } 1 { e o e o o o e e } { o e o e e e o o } 0, 2, 6, 7 { e o o e e e o o } { o e e o o o e e } 1 { e o o e e o e o } { o e e o o e o e } 0, 3, 4, 6 { e o o e e o o e } { o e e o o e e o } 2 { e o o e o e e o } { o e e o e o o e } 7 { e o o e o e o e } { o e e o e o e o } 6 { e o o e o o e e } { o e e o e e o o } 5 { e o o o e e e o } { o e e e o o o e } 2 { e o o o e e o e } { o e e e o o e o } 0, 4, 5, 7 { e o o o e o e e } { o e e e o e o o } 1 { e o o o o e e e } { o e e e e o o o } 3

Here are highlights of class octonion:

```class octonion {
// Each component of guts is TWICE
// the actual value it represents.
arr_int_8 guts;
public:
octonion (
int const, int const, int const, int const,
int const, int const, int const, int const,
bool const
);

octonion (arr_int_8 const &);

octonion operator+ () const;
octonion operator~ () const; // complex conjugate
octonion operator- () const;

octonion   operator+  (octonion const &) const;
octonion & operator+= (octonion const &);
octonion   operator-  (octonion const &) const;
octonion & operator-= (octonion const &);

octonion   operator*  (octonion const &) const;
octonion & operator*= (octonion const &);
octonion   operator*  (int const) const;
octonion & operator*= (int const);

int at (int const) const;
arr_int_8 const & at ();

octonion rotate_imag (int const) const;
octonion nudge (int const, int const) const;
int inner (octonion const &) const;
int squ_mag () const;
void view (ostream &, char const * const) const;
};

bool operator== (octonion const &, octonion const &);
bool operator!= (octonion const &, octonion const &);
bool lexico_LT  (octonion const &, octonion const &);
bool lexico_GT  (octonion const &, octonion const &);
```

The nine-parameter constructor takes eight integers corresponding to the eight components of an octonion, and a bool parameter:

• When true, each integer parameter is twice the corresponding actual value. This is how half-odd parameters are entered.
• When false, each integer parameter is precisely the corresponding actual value.
Either way, the value as stored is twice the actual value; this allows half-odds to be manipulated exactly. For instance, this source code:
```    octonion const a {+3, -2,  0, -6, +5, +7, -1, +2, false};
octonion const b {+3, -2,  0, -6, +5, +7, -1, +2, true};
cout << "\n a: " << a;
cout << "\n b: " << b;
```
results in this output:
```    a: [ +3    -2     0    -6    +5    +7    -1    +2   octo ]
b: [ +3/2  -1     0    -3    +5/2  +7/2  -1/2  +1   octo ]
```
The respective internal representations are:
```    a:   +6,  -4,   0, -12, +10, +14,  -2,  +4
b:   +3,  -2,   0,  -6,  +5,  +7,  -1,  +2
```

The one-parameter constructor takes an arr_int_8 holding all eight components. Each must be twice the actual value, and the numbers are copied unchanged into the octonion's internal representation.

Any constructor that is public verifies that the eight supplied components comply with whatever rules apply to that class. Not shown here are protected or private constructors, which might skip the verification procedure if correctness has been established by other means.

With a parameter, function at returns a copy of one component which is twice the actual value. This corresponds to the number that the nine-parameter constructor requires when the bool is true.

Without a parameter, function at returns a constant reference to all eight components. Again, each number within the reference is twice the actual value, corresponding to what the one-parameter constructor requires.

Function nudge increases one component by any real integer value, except that lattice::nudge requires that the amount of change be an even number. The value supplied as a parameter is multiplied by two for the internal representaion.

Functions lexico_LT and lexico_GT compare two octonions according to lexicographical rules. Although lexicographical less-than and greater-than relations for octonions have little mathematical significance, they might be helpful if a list of octonions needs to be sorted. By contrast, operator== and operator!= embody their full mathematical import.

graves and klein are used in the same way as octonion.

lattice is similar, but it differs in that complex conjugation is barred, and the nudge function requires an even increment. Included for reference is lattice_unit_table, which contains all 240 unit octonions in the lattice set, each having a norm of 2 (not 1).

cayley_0 through cayley_7 are also used similarly, although the declarations are more complicated because a template is employed. cayley_0 prohibits multiplication of two octonions because multiplication is not closed. cayley_0 through cayley_7 additionally offer these rotation functions, which are templated because they incur type conversion:

```    template <int dist> typename std::enable_if <
(cay_ind == 0) && (dist == dist), cayley_0
>::type rotate_imag () const;

template <int dist> typename std::enable_if <
(cay_ind != 0) && (mod7 (dist + cay_ind) == 1), cayley_1
>::type rotate_imag () const;

template <int dist> typename std::enable_if <
(cay_ind != 0) && (mod7 (dist + cay_ind) == 2), cayley_2
>::type rotate_imag () const;

template <int dist> typename std::enable_if <
(cay_ind != 0) && (mod7 (dist + cay_ind) == 3), cayley_3
>::type rotate_imag () const;

template <int dist> typename std::enable_if <
(cay_ind != 0) && (mod7 (dist + cay_ind) == 4), cayley_4
>::type rotate_imag () const;

template <int dist> typename std::enable_if <
(cay_ind != 0) && (mod7 (dist + cay_ind) == 5), cayley_5
>::type rotate_imag () const;

template <int dist> typename std::enable_if <
(cay_ind != 0) && (mod7 (dist + cay_ind) == 6), cayley_6
>::type rotate_imag () const;

template <int dist> typename std::enable_if <
(cay_ind != 0) && (mod7 (dist + cay_ind) == 0), cayley_7
>::type rotate_imag () const;
```

As with lattice_unit_table, cayley_0_unit_table through cayley_7_unit_table enumerate the 240 unit vectors of each class.