from sage.misc.cachefunc import cached_method from sage.combinat.free_module import CombinatorialFreeModule from sage.modules.with_basis.indexed_element import IndexedFreeModuleElement from sage.rings.all import AA from mjo.matrix_algebra import MatrixAlgebra, MatrixAlgebraElement class Octonion(IndexedFreeModuleElement): def conjugate(self): r""" SETUP:: sage: from mjo.hurwitz 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: O = Octonions() sage: x = O.random_element() sage: x.conjugate().conjugate() == x True """ from sage.rings.all import ZZ from sage.matrix.matrix_space import MatrixSpace 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.hurwitz import Octonions EXAMPLES:: sage: O = Octonions() sage: x = sum(O.gens()) sage: x.real() e0 TESTS: This method is idempotent:: 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.hurwitz 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: 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.hurwitz import Octonions EXAMPLES:: sage: O = Octonions() sage: O.one().norm() 1 TESTS: The norm is nonnegative and belongs to the base field:: sage: O = Octonions() sage: n = O.random_element().norm() sage: n >= 0 and n in O.base_ring() True The norm is homogeneous:: 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() # The absolute value notation is typically used for complex numbers... # and norm() isn't supported in AA, so this lets us use abs() in all # of the division algebras we need. abs = norm def inverse(self): r""" Return the inverse of this element if it exists. SETUP:: sage: from mjo.hurwitz 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: 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() class Octonions(CombinatorialFreeModule): r""" SETUP:: sage: from mjo.hurwitz 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 from sage.categories.magmatic_algebras import MagmaticAlgebras 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.hurwitz import Octonions TESTS: This gives the correct unit element:: 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.hurwitz 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) ] from sage.misc.table import table return table(M, header_row=True, header_column=True, frame=True) class HurwitzMatrixAlgebraElement(MatrixAlgebraElement): def conjugate(self): r""" Return the entrywise conjugate of this matrix. SETUP:: sage: from mjo.hurwitz import ComplexMatrixAlgebra EXAMPLES:: sage: A = ComplexMatrixAlgebra(2, QQbar, ZZ) sage: M = A([ [ I, 1 + 2*I], ....: [ 3*I, 4*I] ]) sage: M.conjugate() +------+----------+ | -I | -2*I + 1 | +------+----------+ | -3*I | -4*I | +------+----------+ :: sage: A = ComplexMatrixAlgebra(2, QQbar, QQ) sage: M = A([ [ 1, 2], ....: [ 3, 4] ]) sage: M.conjugate() == M True sage: M.to_vector() (1, 0, 2, 0, 3, 0, 4, 0) """ d = self.monomial_coefficients() A = self.parent() new_terms = ( A._conjugate_term((k,v)) for (k,v) in d.items() ) return self.parent().sum_of_terms(new_terms) def conjugate_transpose(self): r""" Return the conjugate-transpose of this matrix. SETUP:: sage: from mjo.hurwitz import ComplexMatrixAlgebra EXAMPLES:: sage: A = ComplexMatrixAlgebra(2, QQbar, ZZ) sage: M = A([ [ I, 2*I], ....: [ 3*I, 4*I] ]) sage: M.conjugate_transpose() +------+------+ | -I | -3*I | +------+------+ | -2*I | -4*I | +------+------+ sage: M.conjugate_transpose().to_vector() (0, -1, 0, -3, 0, -2, 0, -4) """ d = self.monomial_coefficients() A = self.parent() new_terms = ( A._conjugate_term( ((k[1],k[0],k[2]), v) ) for (k,v) in d.items() ) return self.parent().sum_of_terms(new_terms) def is_hermitian(self): r""" SETUP:: sage: from mjo.hurwitz import (ComplexMatrixAlgebra, ....: HurwitzMatrixAlgebra) EXAMPLES:: sage: A = ComplexMatrixAlgebra(2, QQbar, ZZ) sage: M = A([ [ 0,I], ....: [-I,0] ]) sage: M.is_hermitian() True :: sage: A = ComplexMatrixAlgebra(2, QQbar, ZZ) sage: M = A([ [ 0,0], ....: [-I,0] ]) sage: M.is_hermitian() False :: sage: A = HurwitzMatrixAlgebra(2, AA, QQ) sage: M = A([ [1, 1], ....: [1, 1] ]) sage: M.is_hermitian() True """ # A tiny bit faster than checking equality with the conjugate # transpose. return all( self[i,j] == self[j,i].conjugate() for i in range(self.nrows()) for j in range(i+1) ) def is_skew_symmetric(self): r""" Return whether or not this matrix is skew-symmetric. SETUP:: sage: from mjo.hurwitz import (ComplexMatrixAlgebra, ....: HurwitzMatrixAlgebra) EXAMPLES:: sage: A = ComplexMatrixAlgebra(2, QQbar, ZZ) sage: M = A([ [ 0,I], ....: [-I,1] ]) sage: M.is_skew_symmetric() False :: sage: A = ComplexMatrixAlgebra(2, QQbar, ZZ) sage: M = A([ [ 0, 1+I], ....: [-1-I, 0] ]) sage: M.is_skew_symmetric() True :: sage: A = HurwitzMatrixAlgebra(2, AA, QQ) sage: M = A([ [1, 1], ....: [1, 1] ]) sage: M.is_skew_symmetric() False :: sage: A = ComplexMatrixAlgebra(2, QQbar, ZZ) sage: M = A([ [2*I , 1 + I], ....: [-1 + I, -2*I] ]) sage: M.is_skew_symmetric() False """ # A tiny bit faster than checking equality with the negation # of the transpose. return all( self[i,j] == -self[j,i] for i in range(self.nrows()) for j in range(i+1) ) class HurwitzMatrixAlgebra(MatrixAlgebra): r""" A class of matrix algebras whose entries come from a Hurwitz algebra. For our purposes, we consider "a Hurwitz" algebra to be the real or complex numbers, the quaternions, or the octonions. These are typically also referred to as the Euclidean Hurwitz algebras, or the normed division algebras. By the Cayley-Dickson construction, each Hurwitz algebra is an algebra over the real numbers, so we restrict the scalar field in this case to be real. This also allows us to more accurately produce the generators of the matrix algebra. """ Element = HurwitzMatrixAlgebraElement def __init__(self, n, entry_algebra, scalars, **kwargs): from sage.rings.all import RR if not scalars.is_subring(RR): # Not perfect, but it's what we're using. raise ValueError("scalar field is not real") super().__init__(n, entry_algebra, scalars, **kwargs) @staticmethod def _conjugate_term(t): r""" Conjugate the given ``(index, coefficient)`` term, returning another such term. Given a term ``((i,j,e), c)``, it's straightforward to conjugate the entry ``e``, but if ``e``-conjugate is ``-e``, then the resulting ``((i,j,-e), c)`` is not a term, since ``(i,j,-e)`` is not a monomial index! So when we build a sum of these conjugates we can wind up with a nonsense object. This function handles the case where ``e``-conjugate is ``-e``, but nothing more complicated. Thus it makes sense in Hurwitz matrix algebras, but not more generally. SETUP:: sage: from mjo.hurwitz import ComplexMatrixAlgebra EXAMPLES:: sage: A = ComplexMatrixAlgebra(2, QQbar, ZZ) sage: M = A([ [ I, 1 + 2*I], ....: [ 3*I, 4*I] ]) sage: t = list(M.monomial_coefficients().items())[1] sage: t ((1, 0, I), 3) sage: A._conjugate_term(t) ((1, 0, I), -3) """ if t[0][2].conjugate() == t[0][2]: return t else: return (t[0], -t[1]) def entry_algebra_gens(self): r""" Return a tuple of the generators of (that is, a basis for) the entries of this matrix algebra. This works around the inconsistency in the ``gens()`` methods of the real/complex numbers, quaternions, and octonions. SETUP:: sage: from mjo.hurwitz import Octonions, HurwitzMatrixAlgebra EXAMPLES: The inclusion of the unit element is inconsistent across (subalgebras of) Hurwitz algebras:: sage: AA.gens() (1,) sage: QQbar.gens() (I,) sage: QuaternionAlgebra(AA,1,-1).gens() [i, j, k] sage: Octonions().gens() (e0, e1, e2, e3, e4, e5, e6, e7) The unit element is always returned by this method, so the sets of generators have cartinality 1,2,4, and 8 as you'd expect:: sage: HurwitzMatrixAlgebra(2, AA, AA).entry_algebra_gens() (1,) sage: HurwitzMatrixAlgebra(2, QQbar, AA).entry_algebra_gens() (1, I) sage: Q = QuaternionAlgebra(AA,-1,-1) sage: HurwitzMatrixAlgebra(2, Q, AA).entry_algebra_gens() (1, i, j, k) sage: O = Octonions() sage: HurwitzMatrixAlgebra(2, O, AA).entry_algebra_gens() (e0, e1, e2, e3, e4, e5, e6, e7) """ gs = self.entry_algebra().gens() one = self.entry_algebra().one() if one in gs: return gs else: return (one,) + tuple(gs) class OctonionMatrixAlgebra(HurwitzMatrixAlgebra): 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, and the octonions are neither. SETUP:: sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra EXAMPLES:: sage: OctonionMatrixAlgebra(3) Module of 3 by 3 matrices with entries in Octonion algebra with base ring Algebraic Real Field over the scalar ring Algebraic Real Field :: sage: OctonionMatrixAlgebra(3,scalars=QQ) Module of 3 by 3 matrices with entries in Octonion algebra with base ring Rational Field over the scalar ring Rational Field :: sage: O = Octonions(RR) sage: A = OctonionMatrixAlgebra(1,O) sage: A Module of 1 by 1 matrices with entries in Octonion algebra with base ring Real Field with 53 bits of precision over the scalar ring Algebraic Real Field sage: A.one() +---------------------+ | 1.00000000000000*e0 | +---------------------+ sage: A.gens() (+---------------------+ | 1.00000000000000*e0 | +---------------------+, +---------------------+ | 1.00000000000000*e1 | +---------------------+, +---------------------+ | 1.00000000000000*e2 | +---------------------+, +---------------------+ | 1.00000000000000*e3 | +---------------------+, +---------------------+ | 1.00000000000000*e4 | +---------------------+, +---------------------+ | 1.00000000000000*e5 | +---------------------+, +---------------------+ | 1.00000000000000*e6 | +---------------------+, +---------------------+ | 1.00000000000000*e7 | +---------------------+) :: sage: A = OctonionMatrixAlgebra(2) sage: e0,e1,e2,e3,e4,e5,e6,e7 = A.entry_algebra().gens() sage: A([ [e0+e4, e1+e5], ....: [e2-e6, e3-e7] ]) +---------+---------+ | e0 + e4 | e1 + e5 | +---------+---------+ | e2 - e6 | e3 - e7 | +---------+---------+ :: sage: A1 = OctonionMatrixAlgebra(1,scalars=QQ) sage: A2 = OctonionMatrixAlgebra(1,scalars=QQ) sage: cartesian_product([A1,A2]) Module of 1 by 1 matrices with entries in Octonion algebra with base ring Rational Field over the scalar ring Rational Field (+) Module of 1 by 1 matrices with entries in Octonion algebra with base ring Rational Field over the scalar ring Rational Field TESTS:: sage: A = OctonionMatrixAlgebra(ZZ.random_element(10)) sage: x = A.random_element() sage: x*A.one() == x and A.one()*x == x True """ def __init__(self, n, entry_algebra=None, scalars=AA, **kwargs): if entry_algebra is None: entry_algebra = Octonions(field=scalars) super().__init__(n, entry_algebra, scalars, **kwargs) class QuaternionMatrixAlgebra(HurwitzMatrixAlgebra): r""" The algebra of ``n``-by-``n`` matrices with quaternion entries over (a subfield of) the real numbers. The usual matrix spaces in SageMath don't support quaternion entries because they assume that the entries of the matrix come from a commutative ring, and the quaternions are not commutative. SETUP:: sage: from mjo.hurwitz import QuaternionMatrixAlgebra EXAMPLES:: sage: QuaternionMatrixAlgebra(3) Module of 3 by 3 matrices with entries in Quaternion Algebra (-1, -1) with base ring Algebraic Real Field over the scalar ring Algebraic Real Field :: sage: QuaternionMatrixAlgebra(3,scalars=QQ) Module of 3 by 3 matrices with entries in Quaternion Algebra (-1, -1) with base ring Rational Field over the scalar ring Rational Field :: sage: Q = QuaternionAlgebra(RDF, -1, -1) sage: A = QuaternionMatrixAlgebra(1,Q) sage: A Module of 1 by 1 matrices with entries in Quaternion Algebra (-1.0, -1.0) with base ring Real Double Field over the scalar ring Algebraic Real Field sage: A.one() +-----+ | 1.0 | +-----+ sage: A.gens() (+-----+ | 1.0 | +-----+, +---+ | i | +---+, +---+ | j | +---+, +---+ | k | +---+) :: sage: A = QuaternionMatrixAlgebra(2) sage: i,j,k = A.entry_algebra().gens() sage: A([ [1+i, j-2], ....: [k, k+j] ]) +-------+--------+ | 1 + i | -2 + j | +-------+--------+ | k | j + k | +-------+--------+ :: sage: A1 = QuaternionMatrixAlgebra(1,scalars=QQ) sage: A2 = QuaternionMatrixAlgebra(2,scalars=QQ) sage: cartesian_product([A1,A2]) Module of 1 by 1 matrices with entries in Quaternion Algebra (-1, -1) with base ring Rational Field over the scalar ring Rational Field (+) Module of 2 by 2 matrices with entries in Quaternion Algebra (-1, -1) with base ring Rational Field over the scalar ring Rational Field TESTS:: sage: A = QuaternionMatrixAlgebra(ZZ.random_element(10)) sage: x = A.random_element() sage: x*A.one() == x and A.one()*x == x True """ def __init__(self, n, entry_algebra=None, scalars=AA, **kwargs): if entry_algebra is None: # The -1,-1 gives us the "usual" definition of quaternion from sage.algebras.quatalg.quaternion_algebra import ( QuaternionAlgebra ) entry_algebra = QuaternionAlgebra(scalars,-1,-1) super().__init__(n, entry_algebra, scalars, **kwargs) def _entry_algebra_element_to_vector(self, entry): r""" SETUP:: sage: from mjo.hurwitz import QuaternionMatrixAlgebra EXAMPLES:: sage: A = QuaternionMatrixAlgebra(2) sage: u = A.entry_algebra().one() sage: A._entry_algebra_element_to_vector(u) (1, 0, 0, 0) sage: i,j,k = A.entry_algebra().gens() sage: A._entry_algebra_element_to_vector(i) (0, 1, 0, 0) sage: A._entry_algebra_element_to_vector(j) (0, 0, 1, 0) sage: A._entry_algebra_element_to_vector(k) (0, 0, 0, 1) """ from sage.modules.free_module import FreeModule d = len(self.entry_algebra_gens()) V = FreeModule(self.entry_algebra().base_ring(), d) return V(entry.coefficient_tuple()) class ComplexMatrixAlgebra(HurwitzMatrixAlgebra): r""" The algebra of ``n``-by-``n`` matrices with complex entries over (a subfield of) the real numbers. These differ from the usual complex matrix spaces in SageMath because the scalar field is real (and not assumed to be the same as the space from which the entries are drawn). The space of `1`-by-`1` complex matrices will have dimension two, for example. SETUP:: sage: from mjo.hurwitz import ComplexMatrixAlgebra EXAMPLES:: sage: ComplexMatrixAlgebra(3) Module of 3 by 3 matrices with entries in Algebraic Field over the scalar ring Algebraic Real Field :: sage: ComplexMatrixAlgebra(3,scalars=QQ) Module of 3 by 3 matrices with entries in Algebraic Field over the scalar ring Rational Field :: sage: A = ComplexMatrixAlgebra(1,CC) sage: A Module of 1 by 1 matrices with entries in Complex Field with 53 bits of precision over the scalar ring Algebraic Real Field sage: A.one() +------------------+ | 1.00000000000000 | +------------------+ sage: A.gens() (+------------------+ | 1.00000000000000 | +------------------+, +--------------------+ | 1.00000000000000*I | +--------------------+) :: sage: A = ComplexMatrixAlgebra(2) sage: (I,) = A.entry_algebra().gens() sage: A([ [1+I, 1], ....: [-1, -I] ]) +---------+------+ | 1 + 1*I | 1 | +---------+------+ | -1 | -1*I | +---------+------+ :: sage: A1 = ComplexMatrixAlgebra(1,scalars=QQ) sage: A2 = ComplexMatrixAlgebra(2,scalars=QQ) sage: cartesian_product([A1,A2]) Module of 1 by 1 matrices with entries in Algebraic Field over the scalar ring Rational Field (+) Module of 2 by 2 matrices with entries in Algebraic Field over the scalar ring Rational Field TESTS:: sage: A = ComplexMatrixAlgebra(ZZ.random_element(10)) sage: x = A.random_element() sage: x*A.one() == x and A.one()*x == x True """ def __init__(self, n, entry_algebra=None, scalars=AA, **kwargs): if entry_algebra is None: from sage.rings.all import QQbar entry_algebra = QQbar super().__init__(n, entry_algebra, scalars, **kwargs) def _entry_algebra_element_to_vector(self, entry): r""" SETUP:: sage: from mjo.hurwitz import ComplexMatrixAlgebra EXAMPLES:: sage: A = ComplexMatrixAlgebra(2, QQbar, QQ) sage: A._entry_algebra_element_to_vector(QQbar(1)) (1, 0) sage: A._entry_algebra_element_to_vector(QQbar(I)) (0, 1) """ from sage.modules.free_module import FreeModule d = len(self.entry_algebra_gens()) V = FreeModule(self.entry_algebra().base_ring(), d) return V((entry.real(), entry.imag()))