from sage.misc.cachefunc import cached_method from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra from sage.combinat.free_module import CombinatorialFreeModule from sage.modules.with_basis.indexed_element import IndexedFreeModuleElement from sage.categories.magmatic_algebras import MagmaticAlgebras from sage.rings.all import AA, ZZ from sage.matrix.matrix_space import MatrixSpace from sage.misc.table import table class Octonion(IndexedFreeModuleElement): def conjugate(self): r""" SETUP:: sage: from mjo.octonions import Octonions EXAMPLES:: sage: O = Octonions() sage: x = sum(O.gens()) sage: x.conjugate() e0 - e1 - e2 - e3 - e4 - e5 - e6 - e7 TESTS:: Conjugating twice gets you the original element:: sage: set_random_seed() sage: O = Octonions() sage: x = O.random_element() sage: x.conjugate().conjugate() == x True """ C = MatrixSpace(ZZ,8).diagonal_matrix((1,-1,-1,-1,-1,-1,-1,-1)) return self.parent().from_vector(C*self.to_vector()) def real(self): r""" Return the real part of this octonion. The real part of an octonion is its projection onto the span of the first generator. In other words, the "first dimension" is real and the others are imaginary. SETUP:: sage: from mjo.octonions import Octonions EXAMPLES:: sage: O = Octonions() sage: x = sum(O.gens()) sage: x.real() e0 TESTS: This method is idempotent:: sage: set_random_seed() sage: O = Octonions() sage: x = O.random_element() sage: x.real().real() == x.real() True """ return (self + self.conjugate())/2 def imag(self): r""" Return the imaginary part of this octonion. The imaginary part of an octonion is its projection onto the orthogonal complement of the span of the first generator. In other words, the "first dimension" is real and the others are imaginary. SETUP:: sage: from mjo.octonions import Octonions EXAMPLES:: sage: O = Octonions() sage: x = sum(O.gens()) sage: x.imag() e1 + e2 + e3 + e4 + e5 + e6 + e7 TESTS: This method is idempotent:: sage: set_random_seed() sage: O = Octonions() sage: x = O.random_element() sage: x.imag().imag() == x.imag() True """ return (self - self.conjugate())/2 def _norm_squared(self): return (self*self.conjugate()).coefficient(0) def norm(self): r""" Return the norm of this octonion. SETUP:: sage: from mjo.octonions import Octonions EXAMPLES:: sage: O = Octonions() sage: O.one().norm() 1 TESTS: The norm is nonnegative and belongs to the base field:: sage: set_random_seed() sage: O = Octonions() sage: n = O.random_element().norm() sage: n >= 0 and n in O.base_ring() True The norm is homogeneous:: sage: set_random_seed() sage: O = Octonions() sage: x = O.random_element() sage: alpha = O.base_ring().random_element() sage: (alpha*x).norm() == alpha.abs()*x.norm() True """ return self._norm_squared().sqrt() def inverse(self): r""" Return the inverse of this element if it exists. SETUP:: sage: from mjo.octonions import Octonions EXAMPLES:: sage: O = Octonions() sage: x = sum(O.gens()) sage: x*x.inverse() == O.one() True :: sage: O = Octonions() sage: O.one().inverse() == O.one() True TESTS:: sage: set_random_seed() sage: O = Octonions() sage: x = O.random_element() sage: x.is_zero() or ( x*x.inverse() == O.one() ) True """ if self.is_zero(): raise ValueError("zero is not invertible") return self.conjugate()/self._norm_squared() def cayley_dickson(self, Q=None): r""" Return the Cayley-Dickson representation of this element in terms of the quaternion algebra ``Q``. The Cayley-Dickson representation is an identification of octionions `x` and `y` with pairs of quaternions `(a,b)` and `(c,d)` respectively such that: * `x + y = (a+b, c+d)` * `xy` = (ac - \bar{d}*b, da + b\bar{c})` * `\bar{x} = (a,-b)` where `\bar{x}` denotes the conjugate of `x`. SETUP:: sage: from mjo.octonions import Octonions EXAMPLES:: sage: O = Octonions() sage: x = sum(O.gens()) sage: x.cayley_dickson() (1 + i + j + k, 1 + i + j + k) """ if Q is None: Q = QuaternionAlgebra(self.base_ring(), -1, -1) i,j,k = Q.gens() a = (self.coefficient(0)*Q.one() + self.coefficient(1)*i + self.coefficient(2)*j + self.coefficient(3)*k ) b = (self.coefficient(4)*Q.one() + self.coefficient(5)*i + self.coefficient(6)*j + self.coefficient(7)*k ) from sage.categories.sets_cat import cartesian_product P = cartesian_product([Q,Q]) return P((a,b)) class Octonions(CombinatorialFreeModule): r""" SETUP:: sage: from mjo.octonions import Octonions EXAMPLES:: sage: Octonions() Octonion algebra with base ring Algebraic Real Field sage: Octonions(field=QQ) Octonion algebra with base ring Rational Field """ def __init__(self, field=AA, prefix="e"): # Not associative, not commutative category = MagmaticAlgebras(field).FiniteDimensional() category = category.WithBasis().Unital() super().__init__(field, range(8), element_class=Octonion, category=category, prefix=prefix, bracket=False) # The product of each basis element is plus/minus another # basis element that can simply be looked up on # https://en.wikipedia.org/wiki/Octonion e0, e1, e2, e3, e4, e5, e6, e7 = self.gens() self._multiplication_table = ( (e0, e1, e2, e3, e4, e5, e6, e7), (e1,-e0, e3,-e2, e5,-e4,-e7, e6), (e2,-e3,-e0, e1, e6, e7,-e4,-e5), (e3, e2,-e1,-e0, e7,-e6, e5,-e4), (e4,-e5,-e6,-e7,-e0, e1, e2, e3), (e5, e4,-e7, e6,-e1,-e0,-e3, e2), (e6, e7, e4,-e5,-e2, e3,-e0,-e1), (e7,-e6, e5, e4,-e3,-e2, e1,-e0), ) def product_on_basis(self, i, j): return self._multiplication_table[i][j] def one_basis(self): r""" Return the monomial index (basis element) corresponding to the octonion unit element. SETUP:: sage: from mjo.octonions import Octonions TESTS: This gives the correct unit element:: sage: set_random_seed() sage: O = Octonions() sage: x = O.random_element() sage: x*O.one() == x and O.one()*x == x True """ return 0 def _repr_(self): return ("Octonion algebra with base ring %s" % self.base_ring()) def multiplication_table(self): """ Return a visual representation of this algebra's multiplication table (on basis elements). SETUP:: sage: from mjo.octonions import Octonions EXAMPLES: The multiplication table is what Wikipedia says it is:: sage: Octonions().multiplication_table() +----++----+-----+-----+-----+-----+-----+-----+-----+ | * || e0 | e1 | e2 | e3 | e4 | e5 | e6 | e7 | +====++====+=====+=====+=====+=====+=====+=====+=====+ | e0 || e0 | e1 | e2 | e3 | e4 | e5 | e6 | e7 | +----++----+-----+-----+-----+-----+-----+-----+-----+ | e1 || e1 | -e0 | e3 | -e2 | e5 | -e4 | -e7 | e6 | +----++----+-----+-----+-----+-----+-----+-----+-----+ | e2 || e2 | -e3 | -e0 | e1 | e6 | e7 | -e4 | -e5 | +----++----+-----+-----+-----+-----+-----+-----+-----+ | e3 || e3 | e2 | -e1 | -e0 | e7 | -e6 | e5 | -e4 | +----++----+-----+-----+-----+-----+-----+-----+-----+ | e4 || e4 | -e5 | -e6 | -e7 | -e0 | e1 | e2 | e3 | +----++----+-----+-----+-----+-----+-----+-----+-----+ | e5 || e5 | e4 | -e7 | e6 | -e1 | -e0 | -e3 | e2 | +----++----+-----+-----+-----+-----+-----+-----+-----+ | e6 || e6 | e7 | e4 | -e5 | -e2 | e3 | -e0 | -e1 | +----++----+-----+-----+-----+-----+-----+-----+-----+ | e7 || e7 | -e6 | e5 | e4 | -e3 | -e2 | e1 | -e0 | +----++----+-----+-----+-----+-----+-----+-----+-----+ """ n = self.dimension() # Prepend the header row. M = [["*"] + list(self.gens())] # And to each subsequent row, prepend an entry that belongs to # the left-side "header column." M += [ [self.monomial(i)] + [ self.monomial(i)*self.monomial(j) for j in range(n) ] for i in range(n) ] return table(M, header_row=True, header_column=True, frame=True) class OctonionMatrix(IndexedFreeModuleElement): def nrows(self): return self.parent().nrows() ncols = nrows @cached_method def to_nested_list(self): r""" SETUP:: sage: from mjo.octonions import OctonionMatrixAlgebra EXAMPLES:: sage: MS = OctonionMatrixAlgebra(3) sage: E00e0 = MS.gens()[0] sage: E00e0 +----+---+---+ | e0 | 0 | 0 | +----+---+---+ | 0 | 0 | 0 | +----+---+---+ | 0 | 0 | 0 | +----+---+---+ sage: E00e3 = MS.gens()[3] sage: E00e3 +----+---+---+ | e3 | 0 | 0 | +----+---+---+ | 0 | 0 | 0 | +----+---+---+ | 0 | 0 | 0 | +----+---+---+ sage: (E00e0 + 2*E00e3).to_nested_list() [[e0 + 2*e3, 0, 0], [0, 0, 0], [0, 0, 0]] """ zero = self.parent().entry_algebra().zero() l = [[zero for j in range(self.ncols())] for i in range(self.nrows())] for (k,v) in self.monomial_coefficients().items(): (i,j,e) = k l[i][j] += v*e return l def __repr__(self): r""" SETUP:: sage: from mjo.octonions import OctonionMatrixAlgebra EXAMPLES:: sage: OctonionMatrixAlgebra(3).one() +----+----+----+ | e0 | 0 | 0 | +----+----+----+ | 0 | e0 | 0 | +----+----+----+ | 0 | 0 | e0 | +----+----+----+ """ return table(self.to_nested_list(), frame=True)._repr_() def list(self): r""" Return one long list of this matrix's entries. SETUP:: sage: from mjo.octonions import OctonionMatrixAlgebra EXAMPLES:: sage: MS = OctonionMatrixAlgebra(3) sage: E00e0 = MS.gens()[0] sage: E00e3 = MS.gens()[3] sage: (E00e0 + 2*E00e3).to_nested_list() [[e0 + 2*e3, 0, 0], [0, 0, 0], [0, 0, 0]] sage: (E00e0 + 2*E00e3).list() [e0 + 2*e3, 0, 0, 0, 0, 0, 0, 0, 0] """ return sum( self.to_nested_list(), [] ) def __getitem__(self, indices): r""" SETUP:: sage: from mjo.octonions import OctonionMatrixAlgebra EXAMPLES:: sage: MS = OctonionMatrixAlgebra(2) sage: I = MS.one() sage: I[0,0] e0 sage: I[0,1] 0 sage: I[1,0] 0 sage: I[1,1] e0 """ i,j = indices return self.to_nested_list()[i][j] def trace(self): r""" SETUP:: sage: from mjo.octonions import OctonionMatrixAlgebra EXAMPLES:: sage: MS = OctonionMatrixAlgebra(3) sage: MS.one().trace() 3*e0 """ zero = self.parent().entry_algebra().zero() return sum( (self[i,i] for i in range(self.nrows())), zero ) def matrix_space(self): r""" SETUP:: sage: from mjo.octonions import OctonionMatrixAlgebra TESTS:: sage: set_random_seed() sage: MS = OctonionMatrixAlgebra(2) sage: MS.random_element().matrix_space() Module of 2 by 2 matrices with octonion entries over the scalar ring Algebraic Real Field """ return self.parent() def is_hermitian(self): r""" SETUP:: sage: from mjo.octonions import OctonionMatrixAlgebra EXAMPLES:: sage: MS = OctonionMatrixAlgebra(3) sage: MS.one().is_hermitian() True """ return all( self[i,j] == self[j,i].conjugate() for i in range(self.nrows()) for j in range(self.ncols()) ) class OctonionMatrixAlgebra(CombinatorialFreeModule): r""" The algebra of ``n``-by-``n`` matrices with octonion entries over (a subfield of) the real numbers. The usual matrix spaces in SageMath don't support octonion entries because they assume that the entries of the matrix come from a commutative and associative ring (i.e. very NOT the octonions). """ Element = OctonionMatrix def __init__(self, n, scalars=AA, prefix="E", **kwargs): # Not associative, not commutative category = MagmaticAlgebras(scalars).FiniteDimensional() category = category.WithBasis().Unital() self._nrows = n # Since the scalar ring is real but the entries are not, # sticking a "1" in each position doesn't give us a basis for # the space. We actually need to stick each of e0, e1, ... (a # basis for the entry algebra itself) into each position. from sage.sets.finite_enumerated_set import FiniteEnumeratedSet from sage.categories.sets_cat import cartesian_product I = FiniteEnumeratedSet(range(n)) J = FiniteEnumeratedSet(range(n)) self._entry_algebra = Octonions(field=scalars) entry_basis = self._entry_algebra.gens() basis_indices = cartesian_product([I,J,entry_basis]) super().__init__(scalars, basis_indices, category=category, prefix=prefix, bracket='(') def _repr_(self): return ("Module of %d by %d matrices with octonion entries" " over the scalar ring %s" % (self.nrows(), self.ncols(), self.base_ring()) ) def entry_algebra(self): r""" Return the algebra that our elements' entries come from. """ return self._entry_algebra def nrows(self): return self._nrows ncols = nrows def product_on_basis(self, mon1, mon2): (i,j,oct1) = mon1 (k,l,oct2) = mon2 if j == k: return self.monomial((i,l,oct1*oct2)) else: return self.zero() def one(self): r""" SETUP:: sage: from mjo.octonions import OctonionMatrixAlgebra TESTS:: sage: set_random_seed() sage: MS = OctonionMatrixAlgebra(ZZ.random_element(10)) sage: x = MS.random_element() sage: x*MS.one() == x and MS.one()*x == x True """ return sum( (self.monomial((i,i,self.entry_algebra().one())) for i in range(self.nrows()) ), self.zero() ) def from_list(self, entries): r""" Construct an element of this algebra from a list of lists of octonions. SETUP:: sage: from mjo.octonions import Octonions, OctonionMatrixAlgebra EXAMPLES:: sage: O = Octonions(QQ) sage: e0,e1,e2,e3,e4,e5,e6,e7 = O.gens() sage: MS = OctonionMatrixAlgebra(2) sage: MS.from_list([ [e0+e4, e1+e5], ....: [e2-e6, e3-e7] ]) +---------+---------+ | e0 + e4 | e1 + e5 | +---------+---------+ | e2 - e6 | e3 - e7 | +---------+---------+ """ nrows = len(entries) ncols = 0 if nrows > 0: ncols = len(entries[0]) if (not all( len(r) == ncols for r in entries )) or (ncols != nrows): raise ValueError("list must be square") def convert(e_ij): # We have to pass through vectors to convert from the # given octonion algebra to ours. Otherwise we can fail # to convert an element of (for example) Octonions(QQ) # to Octonions(AA). return self.entry_algebra().from_vector(e_ij.to_vector()) return sum( (self.monomial( (i,j, convert(entries[i][j])) ) for i in range(nrows) for j in range(ncols) ), self.zero() )