from sage.misc.table import table from sage.categories.magmatic_algebras import MagmaticAlgebras from sage.misc.cachefunc import cached_method from sage.combinat.free_module import CombinatorialFreeModule from sage.modules.with_basis.indexed_element import IndexedFreeModuleElement class MatrixAlgebraElement(IndexedFreeModuleElement): def nrows(self): return self.parent().nrows() ncols = nrows @cached_method def rows(self): r""" SETUP:: sage: from mjo.matrix_algebra import MatrixAlgebra EXAMPLES:: sage: M = MatrixAlgebra(QQbar,RDF,2) sage: A = M.monomial((0,0,1)) + 4*M.monomial((0,1,1)) sage: A +-----+-----+ | 1.0 | 4.0 | +-----+-----+ | 0 | 0 | +-----+-----+ sage: A.rows() [[1.0, 4.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""" Display this matrix as a table. The SageMath Matrix class representation is not easily reusable, but using a table fakes it. SETUP:: sage: from mjo.matrix_algebra import MatrixAlgebra EXAMPLES:: sage: MatrixAlgebra(ZZ,ZZ,2).one() +---+---+ | 1 | 0 | +---+---+ | 0 | 1 | +---+---+ """ return table(self.rows(), frame=True)._repr_() def list(self): r""" Return one long list of this matrix's entries. SETUP:: sage: from mjo.matrix_algebra import MatrixAlgebra EXAMPLES:: sage: MatrixAlgebra(ZZ,ZZ,2).one().list() [1, 0, 0, 1] """ return sum( self.rows(), [] ) def __getitem__(self, indices): r""" SETUP:: sage: from mjo.matrix_algebra import MatrixAlgebra EXAMPLES:: sage: M = MatrixAlgebra(ZZ,ZZ,2).one() sage: M[0,0] 1 sage: M[0,1] 0 sage: M[1,0] 0 sage: M[1,1] 1 """ i,j = indices return self.rows()[i][j] def trace(self): r""" Return the sum of this matrix's diagonal entries. SETUP:: sage: from mjo.matrix_algebra import MatrixAlgebra EXAMPLES: The trace (being a sum of entries) belongs to the same algebra as those entries, and NOT the scalar ring:: sage: entries = MatrixSpace(ZZ,2) sage: scalars = ZZ sage: M = MatrixAlgebra(entries, scalars, 2) sage: M.one().trace() [2 0] [0 2] """ 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.matrix_algebra import MatrixAlgebra TESTS:: sage: set_random_seed() sage: entries = QuaternionAlgebra(QQ,-1,-1) sage: M = MatrixAlgebra(entries, QQ, 3) sage: M.random_element().matrix_space() == M True """ return self.parent() # onlt valid in HurwitzMatrixAlgebra subclass # 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 MatrixAlgebra(CombinatorialFreeModule): r""" An algebra of ``n``-by-``n`` matrices over an arbitrary scalar ring whose entries come from a magmatic algebra that need not be the same as the scalars. The usual matrix spaces in SageMath don't support separate spaces for the entries and the scalars; in particular they assume that the entries come from a commutative and associative ring. This is problematic in several interesting matrix algebras, like those where the entries are quaternions or octonions. """ Element = MatrixAlgebraElement def __init__(self, entry_algebra, scalars, n, prefix="A", **kwargs): category = MagmaticAlgebras(scalars).FiniteDimensional() category = category.WithBasis() if "Unital" in entry_algebra.category().axioms(): category = category.Unital() if "Associative" in entry_algebra.category().axioms(): category = category.Associative() 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. I = range(n) J = range(n) self._entry_algebra = entry_algebra entry_basis = entry_algebra.gens() basis_indices = [(i,j,e) for i in range(n) for j in range(n) for e in entry_algebra.gens()] super().__init__(scalars, basis_indices, category=category, prefix=prefix, bracket='(') def _repr_(self): return ("Module of %d by %d matrices with entries in %s" " over the scalar ring %s" % (self.nrows(), self.ncols(), self.entry_algebra(), 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,e1) = mon1 (k,l,e2) = mon2 if j == k: return self.monomial((i,l,e1*e2)) else: return self.zero() # TODO: only makes sense if I'm unital. def one(self): r""" SETUP:: sage: from mjo.matrix_algebra import MatrixAlgebra """ 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 entries. SETUP:: sage: from mjo.matrix_algebra import MatrixAlgebra """ 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 entry 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() )