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