]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/eja_algebra.py
eja: use fuzzy equality test with inexact base rings.
[sage.d.git] / mjo / eja / eja_algebra.py
index ed2e2587282e91d2672cbcbc851d37fcddf7ae74..7f7b1f9f1c09703a2d8f48e295b203604fa5556c 100644 (file)
@@ -61,7 +61,10 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         """
         SETUP::
 
-            sage: from mjo.eja.eja_algebra import (JordanSpinEJA, random_eja)
+            sage: from mjo.eja.eja_algebra import (
+            ....:   FiniteDimensionalEuclideanJordanAlgebra,
+            ....:   JordanSpinEJA,
+            ....:   random_eja)
 
         EXAMPLES:
 
@@ -75,13 +78,20 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
         TESTS:
 
-        The ``field`` we're given must be real::
+        The ``field`` we're given must be real with ``check=True``::
 
             sage: JordanSpinEJA(2,QQbar)
             Traceback (most recent call last):
             ...
             ValueError: field is not real
 
+        The multiplication table must be square with ``check=True``::
+
+            sage: FiniteDimensionalEuclideanJordanAlgebra(QQ,((),()))
+            Traceback (most recent call last):
+            ...
+            ValueError: multiplication table is not square
+
         """
         if check:
             if not field.is_subring(RR):
@@ -96,9 +106,15 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             category = MagmaticAlgebras(field).FiniteDimensional()
             category = category.WithBasis().Unital()
 
+        # The multiplication table had better be square
+        n = len(mult_table)
+        if check:
+            if not all( len(l) == n for l in mult_table ):
+                raise ValueError("multiplication table is not square")
+
         fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
         fda.__init__(field,
-                     range(len(mult_table)),
+                     range(n),
                      prefix=prefix,
                      category=category)
         self.print_options(bracket='')
@@ -114,6 +130,13 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             for ls in mult_table
         ]
 
+        if check:
+            if not self._is_commutative():
+                raise ValueError("algebra is not commutative")
+            if not self._is_jordanian():
+                raise ValueError("Jordan identity does not hold")
+            if not self._inner_product_is_associative():
+                raise ValueError("inner product is not associative")
 
     def _element_constructor_(self, elt):
         """
@@ -235,6 +258,67 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
     def product_on_basis(self, i, j):
         return self._multiplication_table[i][j]
 
+    def _is_commutative(self):
+        r"""
+        Whether or not this algebra's multiplication table is commutative.
+
+        This method should of course always return ``True``, unless
+        this algebra was constructed with ``check=False`` and passed
+        an invalid multiplication table.
+        """
+        return all( self.product_on_basis(i,j) == self.product_on_basis(i,j)
+                    for i in range(self.dimension())
+                    for j in range(self.dimension()) )
+
+    def _is_jordanian(self):
+        r"""
+        Whether or not this algebra's multiplication table respects the
+        Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
+
+        We only check one arrangement of `x` and `y`, so for a
+        ``True`` result to be truly true, you should also check
+        :meth:`_is_commutative`. This method should of course always
+        return ``True``, unless this algebra was constructed with
+        ``check=False`` and passed an invalid multiplication table.
+        """
+        return all( (self.monomial(i)**2)*(self.monomial(i)*self.monomial(j))
+                    ==
+                    (self.monomial(i))*((self.monomial(i)**2)*self.monomial(j))
+                    for i in range(self.dimension())
+                    for j in range(self.dimension()) )
+
+    def _inner_product_is_associative(self):
+        r"""
+        Return whether or not this algebra's inner product `B` is
+        associative; that is, whether or not `B(xy,z) = B(x,yz)`.
+
+        This method should of course always return ``True``, unless
+        this algebra was constructed with ``check=False`` and passed
+        an invalid multiplication table.
+        """
+
+        # Used to check whether or not something is zero in an inexact
+        # ring. This number is sufficient to allow the construction of
+        # QuaternionHermitianEJA(2, RDF) with check=True.
+        epsilon = 1e-16
+
+        for i in range(self.dimension()):
+            for j in range(self.dimension()):
+                for k in range(self.dimension()):
+                    x = self.monomial(i)
+                    y = self.monomial(j)
+                    z = self.monomial(k)
+                    diff = (x*y).inner_product(z) - x.inner_product(y*z)
+
+                    if self.base_ring().is_exact():
+                        if diff != 0:
+                            return False
+                    else:
+                        if diff.abs() > epsilon:
+                            return False
+
+        return True
+
     @cached_method
     def characteristic_polynomial_of(self):
         """
@@ -672,10 +756,61 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         return (J0, J5, J1)
 
 
-    def random_elements(self, count):
+    def random_element(self, thorough=False):
+        r"""
+        Return a random element of this algebra.
+
+        Our algebra superclass method only returns a linear
+        combination of at most two basis elements. We instead
+        want the vector space "random element" method that
+        returns a more diverse selection.
+
+        INPUT:
+
+        - ``thorough`` -- (boolean; default False) whether or not we
+          should generate irrational coefficients for the random
+          element when our base ring is irrational; this slows the
+          algebra operations to a crawl, but any truly random method
+          should include them
+
+        """
+        # For a general base ring... maybe we can trust this to do the
+        # right thing? Unlikely, but.
+        V = self.vector_space()
+        v = V.random_element()
+
+        if self.base_ring() is AA:
+            # The "random element" method of the algebraic reals is
+            # stupid at the moment, and only returns integers between
+            # -2 and 2, inclusive:
+            #
+            #   https://trac.sagemath.org/ticket/30875
+            #
+            # Instead, we implement our own "random vector" method,
+            # and then coerce that into the algebra. We use the vector
+            # space degree here instead of the dimension because a
+            # subalgebra could (for example) be spanned by only two
+            # vectors, each with five coordinates.  We need to
+            # generate all five coordinates.
+            if thorough:
+                v *= QQbar.random_element().real()
+            else:
+                v *= QQ.random_element()
+
+        return self.from_vector(V.coordinate_vector(v))
+
+    def random_elements(self, count, thorough=False):
         """
         Return ``count`` random elements as a tuple.
 
+        INPUT:
+
+        - ``thorough`` -- (boolean; default False) whether or not we
+          should generate irrational coefficients for the random
+          elements when our base ring is irrational; this slows the
+          algebra operations to a crawl, but any truly random method
+          should include them
+
         SETUP::
 
             sage: from mjo.eja.eja_algebra import JordanSpinEJA
@@ -690,7 +825,8 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             True
 
         """
-        return tuple( self.random_element() for idx in range(count) )
+        return tuple( self.random_element(thorough)
+                      for idx in range(count) )
 
     @classmethod
     def random_instance(cls, field=AA, **kwargs):
@@ -2082,3 +2218,50 @@ class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
         # largest subalgebra generated by any element.
         fdeja.__init__(field, mult_table, **kwargs)
         self.rank.set_cache(0)
+
+
+class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
+    r"""
+    The external (orthogonal) direct sum of two other Euclidean Jordan
+    algebras. Essentially the Cartesian product of its two factors.
+    Every Euclidean Jordan algebra decomposes into an orthogonal
+    direct sum of simple Euclidean Jordan algebras, so no generality
+    is lost by providing only this construction.
+
+    SETUP::
+
+        sage: from mjo.eja.eja_algebra import (HadamardEJA,
+        ....:                                  RealSymmetricEJA,
+        ....:                                  DirectSumEJA)
+
+    EXAMPLES::
+
+        sage: J1 = HadamardEJA(2)
+        sage: J2 = RealSymmetricEJA(3)
+        sage: J = DirectSumEJA(J1,J2)
+        sage: J.dimension()
+        8
+        sage: J.rank()
+        5
+
+    """
+    def __init__(self, J1, J2, field=AA, **kwargs):
+        n1 = J1.dimension()
+        n2 = J2.dimension()
+        n = n1+n2
+        V = VectorSpace(field, n)
+        mult_table = [ [ V.zero() for j in range(n) ]
+                       for i in range(n) ]
+        for i in range(n1):
+            for j in range(n1):
+                p = (J1.monomial(i)*J1.monomial(j)).to_vector()
+                mult_table[i][j] = V(p.list() + [field.zero()]*n2)
+
+        for i in range(n2):
+            for j in range(n2):
+                p = (J2.monomial(i)*J2.monomial(j)).to_vector()
+                mult_table[n1+i][n1+j] = V([field.zero()]*n1 + p.list())
+
+        fdeja = super(DirectSumEJA, self)
+        fdeja.__init__(field, mult_table, **kwargs)
+        self.rank.set_cache(J1.rank() + J2.rank())