]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/octonions.py
octonions: add list() method for octonion matrices.
[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_nested_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_nested_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_nested_list(), frame=True)._repr_()
405
406
407 def list(self):
408 r"""
409 Return one long list of this matrix's entries.
410
411 SETUP::
412
413 sage: from mjo.octonions import OctonionMatrixAlgebra
414
415 EXAMPLES::
416
417 sage: MS = OctonionMatrixAlgebra(3)
418 sage: E00e0 = MS.gens()[0]
419 sage: E00e3 = MS.gens()[3]
420 sage: (E00e0 + 2*E00e3).to_nested_list()
421 [[e0 + 2*e3, 0, 0], [0, 0, 0], [0, 0, 0]]
422 sage: (E00e0 + 2*E00e3).list()
423 [e0 + 2*e3, 0, 0, 0, 0, 0, 0, 0, 0]
424
425 """
426 return sum( self.to_nested_list(), [] )
427
428
429 def __getitem__(self, indices):
430 r"""
431
432 SETUP::
433
434 sage: from mjo.octonions import OctonionMatrixAlgebra
435
436 EXAMPLES::
437
438 sage: MS = OctonionMatrixAlgebra(2)
439 sage: I = MS.one()
440 sage: I[0,0]
441 e0
442 sage: I[0,1]
443 0
444 sage: I[1,0]
445 0
446 sage: I[1,1]
447 e0
448
449 """
450 i,j = indices
451 return self.to_nested_list()[i][j]
452
453 def trace(self):
454 r"""
455
456 SETUP::
457
458 sage: from mjo.octonions import OctonionMatrixAlgebra
459
460 EXAMPLES::
461
462 sage: MS = OctonionMatrixAlgebra(3)
463 sage: MS.one().trace()
464 3*e0
465
466 """
467 zero = self.parent().octonions().zero()
468 return sum( (self[i,i] for i in range(self.nrows())), zero )
469
470 def matrix_space(self):
471 r"""
472
473 SETUP::
474
475 sage: from mjo.octonions import OctonionMatrixAlgebra
476
477 TESTS::
478
479 sage: set_random_seed()
480 sage: MS = OctonionMatrixAlgebra(2)
481 sage: MS.random_element().matrix_space()
482 Module of 2 by 2 matrices with octonion entries over the
483 scalar ring Algebraic Real Field
484
485 """
486 return self.parent()
487
488 def is_hermitian(self):
489 r"""
490
491 SETUP::
492
493 sage: from mjo.octonions import OctonionMatrixAlgebra
494
495 EXAMPLES::
496
497 sage: MS = OctonionMatrixAlgebra(3)
498 sage: MS.one().is_hermitian()
499 True
500
501 """
502 return all( self[i,j] == self[j,i].conjugate()
503 for i in range(self.nrows())
504 for j in range(self.ncols()) )
505
506 class OctonionMatrixAlgebra(CombinatorialFreeModule):
507 r"""
508 The algebra of ``n``-by-``n`` matrices with octonion entries over
509 (a subfield of) the real numbers.
510
511 The usual matrix spaces in SageMath don't support octonion entries
512 because they assume that the entries of the matrix come from a
513 commutative and associative ring (i.e. very NOT the octonions).
514 """
515 Element = OctonionMatrix
516
517 def __init__(self, n, field=AA, prefix="E", **kwargs):
518 # Not associative, not commutative
519 category = MagmaticAlgebras(field).FiniteDimensional()
520 category = category.WithBasis().Unital()
521
522 self._nrows = n
523
524 # Since the scalar field is real but the entries are octonion,
525 # sticking a "1" in each position doesn't give us a basis for
526 # the space. We actually need to stick each of e0, e1, ..., e7
527 # (a basis for the Octonions themselves) into each position.
528 from sage.sets.finite_enumerated_set import FiniteEnumeratedSet
529 from sage.categories.sets_cat import cartesian_product
530
531 I = FiniteEnumeratedSet(range(n))
532 J = FiniteEnumeratedSet(range(n))
533 self._octonions = Octonions(field=field)
534 entry_basis = self._octonions.gens()
535
536 basis_indices = cartesian_product([I,J,entry_basis])
537 super().__init__(field,
538 basis_indices,
539 category=category,
540 prefix=prefix,
541 bracket='(')
542
543 def _repr_(self):
544 return ("Module of %d by %d matrices with octonion entries"
545 " over the scalar ring %s" %
546 (self.nrows(), self.ncols(), self.base_ring()) )
547
548 def octonions(self):
549 r"""
550 Return the Octonion algebra that our elements' entries come from.
551 """
552 return self._octonions
553
554 def nrows(self):
555 return self._nrows
556 ncols = nrows
557
558 def product_on_basis(self, mon1, mon2):
559 (i,j,oct1) = mon1
560 (k,l,oct2) = mon2
561 if j == k:
562 return self.monomial((i,l,oct1*oct2))
563 else:
564 return self.zero()
565
566 def one(self):
567 r"""
568 SETUP::
569
570 sage: from mjo.octonions import OctonionMatrixAlgebra
571
572 TESTS::
573
574 sage: set_random_seed()
575 sage: MS = OctonionMatrixAlgebra(ZZ.random_element(10))
576 sage: x = MS.random_element()
577 sage: x*MS.one() == x and MS.one()*x == x
578 True
579
580 """
581 return sum( (self.monomial((i,i,self._octonions.one()))
582 for i in range(self.nrows()) ),
583 self.zero() )
584
585 def from_list(self, entries):
586 r"""
587 Construct an element of this algebra from a list of lists of
588 octonions.
589
590 SETUP::
591
592 sage: from mjo.octonions import Octonions, OctonionMatrixAlgebra
593
594 EXAMPLES::
595
596 sage: O = Octonions(QQ)
597 sage: e0,e1,e2,e3,e4,e5,e6,e7 = O.gens()
598 sage: MS = OctonionMatrixAlgebra(2)
599 sage: MS.from_list([ [e0+e4, e1+e5],
600 ....: [e2-e6, e3-e7] ])
601 +---------+---------+
602 | e0 + e4 | e1 + e5 |
603 +---------+---------+
604 | e2 - e6 | e3 - e7 |
605 +---------+---------+
606
607 """
608 nrows = len(entries)
609 ncols = 0
610 if nrows > 0:
611 ncols = len(entries[0])
612
613 if (not all( len(r) == ncols for r in entries )) or (ncols != nrows):
614 raise ValueError("list must be square")
615
616 def convert(e_ij):
617 # We have to pass through vectors to convert from the
618 # given octonion algebra to ours. Otherwise we can fail
619 # to convert an element of (for example) Octonions(QQ)
620 # to Octonions(AA).
621 return self.octonions().from_vector(e_ij.to_vector())
622
623 return sum( (self.monomial( (i,j, convert(entries[i][j])) )
624 for i in range(nrows)
625 for j in range(ncols) ),
626 self.zero() )