From 282312431417150844934782b2431602704d8b89 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Tue, 2 Mar 2021 17:25:03 -0500 Subject: [PATCH] matrix_algebra: factor out a generic matrix algebra class. --- mjo/matrix_algebra.py | 276 +++++++++++++++++++++++++++++++++++++ mjo/octonions.py | 310 ++++++------------------------------------ 2 files changed, 315 insertions(+), 271 deletions(-) create mode 100644 mjo/matrix_algebra.py diff --git a/mjo/matrix_algebra.py b/mjo/matrix_algebra.py new file mode 100644 index 0000000..8f75ef2 --- /dev/null +++ b/mjo/matrix_algebra.py @@ -0,0 +1,276 @@ +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.Unital() + if "Associative" in entry_algebra.category().axioms(): + 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. + 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 = entry_algebra + entry_basis = 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 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,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.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() ) diff --git a/mjo/octonions.py b/mjo/octonions.py index 04aa318..073765c 100644 --- a/mjo/octonions.py +++ b/mjo/octonions.py @@ -7,6 +7,8 @@ from sage.rings.all import AA, ZZ from sage.matrix.matrix_space import MatrixSpace from sage.misc.table import table +from mjo.matrix_algebra import MatrixAlgebra + class Octonion(IndexedFreeModuleElement): def conjugate(self): r""" @@ -339,171 +341,8 @@ class Octonions(CombinatorialFreeModule): 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): +class OctonionMatrixAlgebra(MatrixAlgebra): r""" The algebra of ``n``-by-``n`` matrices with octonion entries over (a subfield of) the real numbers. @@ -511,116 +350,45 @@ class OctonionMatrixAlgebra(CombinatorialFreeModule): 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() + SETUP:: - self._nrows = n + sage: from mjo.octonions import Octonions, OctonionMatrixAlgebra - # 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 + EXAMPLES:: - I = FiniteEnumeratedSet(range(n)) - J = FiniteEnumeratedSet(range(n)) - self._entry_algebra = Octonions(field=scalars) - entry_basis = self._entry_algebra.gens() + 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,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(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 | + +---------+---------+ + + 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 - basis_indices = cartesian_product([I,J,entry_basis]) - super().__init__(scalars, - basis_indices, - category=category, + """ + def __init__(self, n, scalars=AA, prefix="E", **kwargs): + super().__init__(Octonions(field=scalars), + scalars, + n, 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() ) + **kwargs) -- 2.44.2