]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/octonions.py
octonions: replace yesterday's hacky octonion matrices with the real thing.
[sage.d.git] / mjo / octonions.py
1 from sage.misc.cachefunc import cached_method
2 from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra
3 from sage.combinat.free_module import CombinatorialFreeModule
4 from sage.modules.with_basis.indexed_element import IndexedFreeModuleElement
5 from sage.categories.magmatic_algebras import MagmaticAlgebras
6 from sage.rings.all import AA, ZZ
7 from sage.matrix.matrix_space import MatrixSpace
8 from sage.misc.table import table
9
10 class Octonion(IndexedFreeModuleElement):
11 def conjugate(self):
12 r"""
13 SETUP::
14
15 sage: from mjo.octonions import Octonions
16
17 EXAMPLES::
18
19 sage: O = Octonions()
20 sage: x = sum(O.gens())
21 sage: x.conjugate()
22 e0 - e1 - e2 - e3 - e4 - e5 - e6 - e7
23
24 TESTS::
25
26 Conjugating twice gets you the original element::
27
28 sage: set_random_seed()
29 sage: O = Octonions()
30 sage: x = O.random_element()
31 sage: x.conjugate().conjugate() == x
32 True
33
34 """
35 C = MatrixSpace(ZZ,8).diagonal_matrix((1,-1,-1,-1,-1,-1,-1,-1))
36 return self.parent().from_vector(C*self.to_vector())
37
38 def real(self):
39 r"""
40 Return the real part of this octonion.
41
42 The real part of an octonion is its projection onto the span
43 of the first generator. In other words, the "first dimension"
44 is real and the others are imaginary.
45
46 SETUP::
47
48 sage: from mjo.octonions import Octonions
49
50 EXAMPLES::
51
52 sage: O = Octonions()
53 sage: x = sum(O.gens())
54 sage: x.real()
55 e0
56
57 TESTS:
58
59 This method is idempotent::
60
61 sage: set_random_seed()
62 sage: O = Octonions()
63 sage: x = O.random_element()
64 sage: x.real().real() == x.real()
65 True
66
67 """
68 return (self + self.conjugate())/2
69
70 def imag(self):
71 r"""
72 Return the imaginary part of this octonion.
73
74 The imaginary part of an octonion is its projection onto the
75 orthogonal complement of the span of the first generator. In
76 other words, the "first dimension" is real and the others are
77 imaginary.
78
79 SETUP::
80
81 sage: from mjo.octonions import Octonions
82
83 EXAMPLES::
84
85 sage: O = Octonions()
86 sage: x = sum(O.gens())
87 sage: x.imag()
88 e1 + e2 + e3 + e4 + e5 + e6 + e7
89
90 TESTS:
91
92 This method is idempotent::
93
94 sage: set_random_seed()
95 sage: O = Octonions()
96 sage: x = O.random_element()
97 sage: x.imag().imag() == x.imag()
98 True
99
100 """
101 return (self - self.conjugate())/2
102
103 def _norm_squared(self):
104 return (self*self.conjugate()).coefficient(0)
105
106 def norm(self):
107 r"""
108 Return the norm of this octonion.
109
110 SETUP::
111
112 sage: from mjo.octonions import Octonions
113
114 EXAMPLES::
115
116 sage: O = Octonions()
117 sage: O.one().norm()
118 1
119
120 TESTS:
121
122 The norm is nonnegative and belongs to the base field::
123
124 sage: set_random_seed()
125 sage: O = Octonions()
126 sage: n = O.random_element().norm()
127 sage: n >= 0 and n in O.base_ring()
128 True
129
130 The norm is homogeneous::
131
132 sage: set_random_seed()
133 sage: O = Octonions()
134 sage: x = O.random_element()
135 sage: alpha = O.base_ring().random_element()
136 sage: (alpha*x).norm() == alpha.abs()*x.norm()
137 True
138
139 """
140 return self._norm_squared().sqrt()
141
142 def inverse(self):
143 r"""
144 Return the inverse of this element if it exists.
145
146 SETUP::
147
148 sage: from mjo.octonions import Octonions
149
150 EXAMPLES::
151
152 sage: O = Octonions()
153 sage: x = sum(O.gens())
154 sage: x*x.inverse() == O.one()
155 True
156
157 ::
158
159 sage: O = Octonions()
160 sage: O.one().inverse() == O.one()
161 True
162
163 TESTS::
164
165 sage: set_random_seed()
166 sage: O = Octonions()
167 sage: x = O.random_element()
168 sage: x.is_zero() or ( x*x.inverse() == O.one() )
169 True
170
171 """
172 if self.is_zero():
173 raise ValueError("zero is not invertible")
174 return self.conjugate()/self._norm_squared()
175
176
177 def cayley_dickson(self, Q=None):
178 r"""
179 Return the Cayley-Dickson representation of this element in terms
180 of the quaternion algebra ``Q``.
181
182 The Cayley-Dickson representation is an identification of
183 octionions `x` and `y` with pairs of quaternions `(a,b)` and
184 `(c,d)` respectively such that:
185
186 * `x + y = (a+b, c+d)`
187 * `xy` = (ac - \bar{d}*b, da + b\bar{c})`
188 * `\bar{x} = (a,-b)`
189
190 where `\bar{x}` denotes the conjugate of `x`.
191
192 SETUP::
193
194 sage: from mjo.octonions import Octonions
195
196 EXAMPLES::
197
198 sage: O = Octonions()
199 sage: x = sum(O.gens())
200 sage: x.cayley_dickson()
201 (1 + i + j + k, 1 + i + j + k)
202
203 """
204 if Q is None:
205 Q = QuaternionAlgebra(self.base_ring(), -1, -1)
206
207 i,j,k = Q.gens()
208 a = (self.coefficient(0)*Q.one() +
209 self.coefficient(1)*i +
210 self.coefficient(2)*j +
211 self.coefficient(3)*k )
212 b = (self.coefficient(4)*Q.one() +
213 self.coefficient(5)*i +
214 self.coefficient(6)*j +
215 self.coefficient(7)*k )
216
217 from sage.categories.sets_cat import cartesian_product
218 P = cartesian_product([Q,Q])
219 return P((a,b))
220
221
222 class Octonions(CombinatorialFreeModule):
223 r"""
224 SETUP::
225
226 sage: from mjo.octonions import Octonions
227
228 EXAMPLES::
229
230 sage: Octonions()
231 Octonion algebra with base ring Algebraic Real Field
232 sage: Octonions(field=QQ)
233 Octonion algebra with base ring Rational Field
234
235 """
236 def __init__(self,
237 field=AA,
238 prefix="e"):
239
240 # Not associative, not commutative
241 category = MagmaticAlgebras(field).FiniteDimensional()
242 category = category.WithBasis().Unital()
243
244 super().__init__(field,
245 range(8),
246 element_class=Octonion,
247 category=category,
248 prefix=prefix,
249 bracket=False)
250
251 # The product of each basis element is plus/minus another
252 # basis element that can simply be looked up on
253 # https://en.wikipedia.org/wiki/Octonion
254 e0, e1, e2, e3, e4, e5, e6, e7 = self.gens()
255 self._multiplication_table = (
256 (e0, e1, e2, e3, e4, e5, e6, e7),
257 (e1,-e0, e3,-e2, e5,-e4,-e7, e6),
258 (e2,-e3,-e0, e1, e6, e7,-e4,-e5),
259 (e3, e2,-e1,-e0, e7,-e6, e5,-e4),
260 (e4,-e5,-e6,-e7,-e0, e1, e2, e3),
261 (e5, e4,-e7, e6,-e1,-e0,-e3, e2),
262 (e6, e7, e4,-e5,-e2, e3,-e0,-e1),
263 (e7,-e6, e5, e4,-e3,-e2, e1,-e0),
264 )
265
266 def product_on_basis(self, i, j):
267 return self._multiplication_table[i][j]
268
269 def one_basis(self):
270 r"""
271 Return the monomial index (basis element) corresponding to the
272 octonion unit element.
273
274 SETUP::
275
276 sage: from mjo.octonions import Octonions
277
278 TESTS:
279
280 This gives the correct unit element::
281
282 sage: set_random_seed()
283 sage: O = Octonions()
284 sage: x = O.random_element()
285 sage: x*O.one() == x and O.one()*x == x
286 True
287
288 """
289 return 0
290
291 def _repr_(self):
292 return ("Octonion algebra with base ring %s" % self.base_ring())
293
294 def multiplication_table(self):
295 """
296 Return a visual representation of this algebra's multiplication
297 table (on basis elements).
298
299 SETUP::
300
301 sage: from mjo.octonions import Octonions
302
303 EXAMPLES:
304
305 The multiplication table is what Wikipedia says it is::
306
307 sage: Octonions().multiplication_table()
308 +----++----+-----+-----+-----+-----+-----+-----+-----+
309 | * || e0 | e1 | e2 | e3 | e4 | e5 | e6 | e7 |
310 +====++====+=====+=====+=====+=====+=====+=====+=====+
311 | e0 || e0 | e1 | e2 | e3 | e4 | e5 | e6 | e7 |
312 +----++----+-----+-----+-----+-----+-----+-----+-----+
313 | e1 || e1 | -e0 | e3 | -e2 | e5 | -e4 | -e7 | e6 |
314 +----++----+-----+-----+-----+-----+-----+-----+-----+
315 | e2 || e2 | -e3 | -e0 | e1 | e6 | e7 | -e4 | -e5 |
316 +----++----+-----+-----+-----+-----+-----+-----+-----+
317 | e3 || e3 | e2 | -e1 | -e0 | e7 | -e6 | e5 | -e4 |
318 +----++----+-----+-----+-----+-----+-----+-----+-----+
319 | e4 || e4 | -e5 | -e6 | -e7 | -e0 | e1 | e2 | e3 |
320 +----++----+-----+-----+-----+-----+-----+-----+-----+
321 | e5 || e5 | e4 | -e7 | e6 | -e1 | -e0 | -e3 | e2 |
322 +----++----+-----+-----+-----+-----+-----+-----+-----+
323 | e6 || e6 | e7 | e4 | -e5 | -e2 | e3 | -e0 | -e1 |
324 +----++----+-----+-----+-----+-----+-----+-----+-----+
325 | e7 || e7 | -e6 | e5 | e4 | -e3 | -e2 | e1 | -e0 |
326 +----++----+-----+-----+-----+-----+-----+-----+-----+
327
328 """
329 n = self.dimension()
330 # Prepend the header row.
331 M = [["*"] + list(self.gens())]
332
333 # And to each subsequent row, prepend an entry that belongs to
334 # the left-side "header column."
335 M += [ [self.monomial(i)] + [ self.monomial(i)*self.monomial(j)
336 for j in range(n) ]
337 for i in range(n) ]
338
339 return table(M, header_row=True, header_column=True, frame=True)
340
341
342 class OctonionMatrix(IndexedFreeModuleElement):
343 def nrows(self):
344 return self.parent().nrows()
345 ncols = nrows
346
347 @cached_method
348 def to_list(self):
349 r"""
350 SETUP::
351
352 sage: from mjo.octonions import OctonionMatrixAlgebra
353
354 EXAMPLES::
355
356 sage: MS = OctonionMatrixAlgebra(3)
357 sage: E00e0 = MS.gens()[0]
358 sage: E00e0
359 +----+---+---+
360 | e0 | 0 | 0 |
361 +----+---+---+
362 | 0 | 0 | 0 |
363 +----+---+---+
364 | 0 | 0 | 0 |
365 +----+---+---+
366 sage: E00e3 = MS.gens()[3]
367 sage: E00e3
368 +----+---+---+
369 | e3 | 0 | 0 |
370 +----+---+---+
371 | 0 | 0 | 0 |
372 +----+---+---+
373 | 0 | 0 | 0 |
374 +----+---+---+
375 sage: (E00e0 + 2*E00e3).to_list()
376 [[e0 + 2*e3, 0, 0], [0, 0, 0], [0, 0, 0]]
377
378 """
379 zero = self.parent().octonions().zero()
380 l = [[zero for j in range(self.ncols())] for i in range(self.nrows())]
381 for (k,v) in self.monomial_coefficients().items():
382 (i,j,e) = k
383 l[i][j] += v*e
384 return l
385
386 def __repr__(self):
387 r"""
388 SETUP::
389
390 sage: from mjo.octonions import OctonionMatrixAlgebra
391
392 EXAMPLES::
393
394 sage: OctonionMatrixAlgebra(3).one()
395 +----+----+----+
396 | e0 | 0 | 0 |
397 +----+----+----+
398 | 0 | e0 | 0 |
399 +----+----+----+
400 | 0 | 0 | e0 |
401 +----+----+----+
402
403 """
404 return table(self.to_list(), frame=True)._repr_()
405
406
407 def __getitem__(self, indices):
408 r"""
409
410 SETUP::
411
412 sage: from mjo.octonions import OctonionMatrixAlgebra
413
414 EXAMPLES::
415
416 sage: MS = OctonionMatrixAlgebra(2)
417 sage: I = MS.one()
418 sage: I[0,0]
419 e0
420 sage: I[0,1]
421 0
422 sage: I[1,0]
423 0
424 sage: I[1,1]
425 e0
426
427 """
428 i,j = indices
429 return self.to_list()[i][j]
430
431 def trace(self):
432 r"""
433
434 SETUP::
435
436 sage: from mjo.octonions import OctonionMatrixAlgebra
437
438 EXAMPLES::
439
440 sage: MS = OctonionMatrixAlgebra(3)
441 sage: MS.one().trace()
442 3*e0
443
444 """
445 zero = self.parent().octonions().zero()
446 return sum( (self[i,i] for i in range(self.nrows())), zero )
447
448
449 class OctonionMatrixAlgebra(CombinatorialFreeModule):
450 r"""
451 The algebra of ``n``-by-``n`` matrices with octonion entries over
452 (a subfield of) the real numbers.
453
454 The usual matrix spaces in SageMath don't support octonion entries
455 because they assume that the entries of the matrix come from a
456 commutative and associative ring (i.e. very NOT the octonions).
457 """
458 Element = OctonionMatrix
459
460 def __init__(self, n, field=AA, prefix="E", **kwargs):
461 # Not associative, not commutative
462 category = MagmaticAlgebras(field).FiniteDimensional()
463 category = category.WithBasis().Unital()
464
465 self._nrows = n
466
467 # Since the scalar field is real but the entries are octonion,
468 # sticking a "1" in each position doesn't give us a basis for
469 # the space. We actually need to stick each of e0, e1, ..., e7
470 # (a basis for the Octonions themselves) into each position.
471 from sage.sets.finite_enumerated_set import FiniteEnumeratedSet
472 from sage.categories.sets_cat import cartesian_product
473
474 I = FiniteEnumeratedSet(range(n))
475 J = FiniteEnumeratedSet(range(n))
476 self._octonions = Octonions(field=field)
477 entry_basis = self._octonions.gens()
478
479 basis_indices = cartesian_product([I,J,entry_basis])
480 super().__init__(field,
481 basis_indices,
482 category=category,
483 prefix=prefix,
484 bracket='(')
485
486 def octonions(self):
487 r"""
488 Return the Octonion algebra that our elements' entries come from.
489 """
490 return self._octonions
491
492 def nrows(self):
493 return self._nrows
494 ncols = nrows
495
496 def product_on_basis(self, mon1, mon2):
497 (i,j,oct1) = mon1
498 (k,l,oct2) = mon2
499 if j == k:
500 return self.monomial((i,l,oct1*oct2))
501 else:
502 return self.zero()
503
504 def one(self):
505 r"""
506 SETUP::
507
508 sage: from mjo.octonions import OctonionMatrixAlgebra
509
510 TESTS::
511
512 sage: set_random_seed()
513 sage: MS = OctonionMatrixAlgebra(ZZ.random_element(10))
514 sage: x = MS.random_element()
515 sage: x*MS.one() == x and MS.one()*x == x
516 True
517
518 """
519 return sum( (self.monomial((i,i,self._octonions.one()))
520 for i in range(self.nrows()) ),
521 self.zero() )
522
523 def from_list(self, entries):
524 r"""
525 Construct an element of this algebra from a list of lists of
526 octonions.
527
528 SETUP::
529
530 sage: from mjo.octonions import Octonions, OctonionMatrixAlgebra
531
532 EXAMPLES::
533
534 sage: O = Octonions(QQ)
535 sage: e0,e1,e2,e3,e4,e5,e6,e7 = O.gens()
536 sage: MS = OctonionMatrixAlgebra(2)
537 sage: MS.from_list([ [e0+e4, e1+e5],
538 ....: [e2-e6, e3-e7] ])
539 +---------+---------+
540 | e0 + e4 | e1 + e5 |
541 +---------+---------+
542 | e2 - e6 | e3 - e7 |
543 +---------+---------+
544
545 """
546 nrows = len(entries)
547 ncols = 0
548 if nrows > 0:
549 ncols = len(entries[0])
550
551 if (not all( len(r) == ncols for r in entries )) or (ncols != nrows):
552 raise ValueError("list must be square")
553
554 def convert(e_ij):
555 # We have to pass through vectors to convert from the
556 # given octonion algebra to ours. Otherwise we can fail
557 # to convert an element of (for example) Octonions(QQ)
558 # to Octonions(AA).
559 return self.octonions().from_vector(e_ij.to_vector())
560
561 return sum( (self.monomial( (i,j, convert(entries[i][j])) )
562 for i in range(nrows)
563 for j in range(ncols) ),
564 self.zero() )