]> gitweb.michael.orlitzky.com - sage.d.git/commitdiff
octonions: replace yesterday's hacky octonion matrices with the real thing.
authorMichael Orlitzky <michael@orlitzky.com>
Tue, 2 Mar 2021 15:12:56 +0000 (10:12 -0500)
committerMichael Orlitzky <michael@orlitzky.com>
Tue, 2 Mar 2021 15:12:56 +0000 (10:12 -0500)
mjo/octonions.py

index 3c3f47418b1186147ddcf3efc47f3b5dc5b969ee..352c4c586e7506a218bcc9e7f9cb6332c1cd72c7 100644 (file)
@@ -1,3 +1,4 @@
+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
@@ -338,202 +339,226 @@ class Octonions(CombinatorialFreeModule):
         return table(M, header_row=True, header_column=True, frame=True)
 
 
-class OctonionMatrix:
-    r"""
-    A pseudo-matrix class that supports octonion entries.
-
-    Matrices in SageMath can't have base rings that are
-    non-commutative, much less non-associative. The "matrix" scaling,
-    addition, and multiplication operations for this class are all
-    wholly inefficient, but are hand-written to guarantee that they
-    are performed in the correct order. Of course, it can't guarantee
-    that you won't write something visually ambiguous like
-    `A*B*C`... but you already have that problem with the
-    non-associative octonions themselves.
-
-    This class is only as sophisticated as it need to be to implement
-    the Jordan and inner-products in the space of Hermitian matrices
-    with octonion entries, namely ``(X*Y+Y*X)/2`` and
-    ``(X*Y).trace().real()`` for two octonion matrices ``X`` and
-    ``Y``.
-
-    .. WARNING:
-
-        These are not matrices in the usual sense! Matrix
-        multiplication is associative. Multiplication of octonion
-        "matrices" cannot be, since the multiplication of the
-        underlying octonions is not (consider two 1-by-1 matrices each
-        containing a single octonion).
-    """
-    def __init__(self, entries):
-        r"""
-        Initialize this matrix with a list of lists in (row,column) order,
-        just like in SageMath.
-        """
-        self._nrows = len(entries)
-
-        if self._nrows == 0:
-            self._ncols = 0
-        else:
-            # We don't check that you haven't supplied two rows (or
-            # columns) of different lengths!
-            self._ncols = len(entries[0])
-
-        self._entries = entries
+class OctonionMatrix(IndexedFreeModuleElement):
+    def nrows(self):
+        return self.parent().nrows()
+    ncols = nrows
 
-    def __getitem__(self, indices):
+    @cached_method
+    def to_list(self):
         r"""
         SETUP::
 
-            sage: from mjo.octonions import Octonions, OctonionMatrix
+            sage: from mjo.octonions import OctonionMatrixAlgebra
 
         EXAMPLES::
 
-            sage: O = Octonions(field=QQ)
-            sage: M = OctonionMatrix([ [O.one(),  O.zero()],
-            ....:                      [O.zero(), O.one() ] ])
-            sage: M[0,0]
-            e0
-            sage: M[0,1]
-            0
-            sage: M[1,0]
-            0
-            sage: M[1,1]
-            e0
+            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_list()
+            [[e0 + 2*e3, 0, 0], [0, 0, 0], [0, 0, 0]]
+
         """
-        i,j = indices
-        return self._entries[i][j]
+        zero = self.parent().octonions().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 nrows(self):
+    def __repr__(self):
         r"""
         SETUP::
 
-            sage: from mjo.octonions import Octonions, OctonionMatrix
+            sage: from mjo.octonions import OctonionMatrixAlgebra
 
         EXAMPLES::
 
-            sage: O = Octonions(field=QQ)
-            sage: M = OctonionMatrix([ [O.one(),  O.zero()],
-            ....:                      [O.zero(), O.one() ],
-            ....:                      [O.zero(), O.zero()] ])
-            sage: M.nrows()
-            3
+            sage: OctonionMatrixAlgebra(3).one()
+            +----+----+----+
+            | e0 | 0  | 0  |
+             +----+----+----+
+            | 0  | e0 | 0  |
+            +----+----+----+
+            | 0  | 0  | e0 |
+            +----+----+----+
 
         """
-        return self._nrows
+        return table(self.to_list(), frame=True)._repr_()
+
 
-    def ncols(self):
+    def __getitem__(self, indices):
         r"""
 
         SETUP::
 
-            sage: from mjo.octonions import Octonions, OctonionMatrix
+            sage: from mjo.octonions import OctonionMatrixAlgebra
 
         EXAMPLES::
 
-            sage: O = Octonions(field=QQ)
-            sage: M = OctonionMatrix([ [O.one(),  O.zero()],
-            ....:                      [O.zero(), O.one() ],
-            ....:                      [O.zero(), O.zero()] ])
-            sage: M.ncols()
-            2
+            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
 
         """
-        return self._ncols
-
-    def __repr__(self):
-        return table(self._entries, frame=True)._repr_()
+        i,j = indices
+        return self.to_list()[i][j]
 
-    def __mul__(self,rhs):
+    def trace(self):
         r"""
 
         SETUP::
 
-            sage: from mjo.octonions import Octonions, OctonionMatrix
+            sage: from mjo.octonions import OctonionMatrixAlgebra
 
         EXAMPLES::
 
-            sage: O = Octonions(QQ)
-            sage: e1 = O.monomial(1)
-            sage: e2 = O.monomial(2)
-            sage: e1*e2
-            e3
-            sage: e2*e1
-            -e3
-            sage: E1 = OctonionMatrix([[e1]])
-            sage: E2 = OctonionMatrix([[e2]])
-            sage: E1*E2
-            +----+
-            | e3 |
-            +----+
-            sage: E2*E1
-            +-----+
-            | -e3 |
-            +-----+
+            sage: MS = OctonionMatrixAlgebra(3)
+            sage: MS.one().trace()
+            3*e0
 
         """
-        if not self.ncols() == rhs.nrows():
-            raise ValueError("dimension mismatch")
+        zero = self.parent().octonions().zero()
+        return sum( (self[i,i] for i in range(self.nrows())), zero )
+
 
-        m = self.nrows()
-        n = self.ncols()
-        p = rhs.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, field=AA, prefix="E", **kwargs):
+        # Not associative, not commutative
+        category = MagmaticAlgebras(field).FiniteDimensional()
+        category = category.WithBasis().Unital()
 
-        C = lambda i,j: sum( self[i,k]*rhs[k,j] for k in range(n) )
-        return OctonionMatrix([ [C(i,j) for j in range(m)]
-                                for i in range(p) ] )
+        self._nrows = n
 
-    def __rmul__(self,scalar):
+        # Since the scalar field is real but the entries are octonion,
+        # sticking a "1" in each position doesn't give us a basis for
+        # the space. We actually need to stick each of e0, e1, ..., e7
+        # (a basis for the Octonions themselves) 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._octonions = Octonions(field=field)
+        entry_basis = self._octonions.gens()
+
+        basis_indices = cartesian_product([I,J,entry_basis])
+        super().__init__(field,
+                         basis_indices,
+                         category=category,
+                         prefix=prefix,
+                         bracket='(')
+
+    def octonions(self):
         r"""
+        Return the Octonion algebra that our elements' entries come from.
+        """
+        return self._octonions
 
+    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 Octonions, OctonionMatrix
+            sage: from mjo.octonions import OctonionMatrixAlgebra
 
-        EXAMPLES::
+        TESTS::
 
-            sage: O = Octonions(QQ)
-            sage: M = OctonionMatrix([[O.one(), O.zero()],
-            ....:                     [O.zero(),O.one() ] ])
-            sage: 2*M
-            +------+------+
-            | 2*e0 | 0    |
-            +------+------+
-            | 0    | 2*e0 |
-            +------+------+
+            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
 
-        r"""
-        # SCALAR GOES ON THE LEFT HERE
-        return OctonionMatrix([ [scalar*self[i,j]
-                                 for j in range(self.ncols())]
-                                for i in range(self.nrows()) ])
+        """
+        return sum( (self.monomial((i,i,self._octonions.one()))
+                     for i in range(self.nrows()) ),
+                    self.zero() )
 
-    def __add__(self,rhs):
+    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, OctonionMatrix
+            sage: from mjo.octonions import Octonions, OctonionMatrixAlgebra
 
         EXAMPLES::
 
             sage: O = Octonions(QQ)
             sage: e0,e1,e2,e3,e4,e5,e6,e7 = O.gens()
-            sage: A = OctonionMatrix([ [e0,e1],
-            ....:                      [e2,e3] ])
-            sage: B = OctonionMatrix([ [e4,e5],
-            ....:                      [e6,e7] ])
-            sage: A+B
+            sage: MS = OctonionMatrixAlgebra(2)
+            sage: MS.from_list([ [e0+e4, e1+e5],
+            ....:                [e2-e6, e3-e7] ])
             +---------+---------+
             | e0 + e4 | e1 + e5 |
             +---------+---------+
-            | e2 + e6 | e3 + e7 |
+            | e2 - e6 | e3 - e7 |
             +---------+---------+
 
         """
-        if not self.ncols() == rhs.ncols():
-            raise ValueError("column dimension mismatch")
-        if not self.nrows() == rhs.nrows():
-            raise ValueError("row dimension mismatch")
-        return OctonionMatrix([ [self[i,j] + rhs[i,j]
-                                 for j in range(self.ncols())]
-                                for i in range(self.nrows()) ])
+        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.octonions().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() )