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