]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/eja_algebra.py
eja: fix tests and pre-cache ranks.
[sage.d.git] / mjo / eja / eja_algebra.py
index 34fc0c3bc7b1dfe250fd5f7ea5de4fbbc52c66e0..19db8b0ccefef297623d81721d4a774f99a9d3d1 100644 (file)
@@ -54,7 +54,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
     def __init__(self,
                  field,
                  mult_table,
-                 rank,
                  prefix='e',
                  category=None,
                  natural_basis=None,
@@ -91,7 +90,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
                 # a real embedding.
                 raise ValueError('field is not real')
 
-        self._rank = rank
         self._natural_basis = natural_basis
 
         if category is None:
@@ -791,33 +789,21 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             True
 
         """
-        return  tuple( self.random_element() for idx in range(count) )
+        return tuple( self.random_element() for idx in range(count) )
 
-
-    def _rank_computation1(self):
-        r"""
-        Compute the rank of this algebra using highly suspicious voodoo.
+    @cached_method
+    def rank(self):
+        """
+        Return the rank of this EJA.
 
         ALGORITHM:
 
-        We first compute the basis representation of the operator L_x
-        using polynomial indeterminates are placeholders for the
-        coordinates of "x", which is arbitrary. We then use that
-        matrix to compute the (polynomial) entries of x^0, x^1, ...,
-        x^d,... for increasing values of "d", starting at zero. The
-        idea is that. If we also add "coefficient variables" a_0,
-        a_1,...  to the ring, we can form the linear combination
-        a_0*x^0 + ... + a_d*x^d = 0, and ask what dimension the
-        solution space has as an affine variety. When "d" is smaller
-        than the rank, we expect that dimension to be the number of
-        coordinates of "x", since we can set *those* to whatever we
-        want, but linear independence forces the coefficients a_i to
-        be zero. Eventually, when "d" passes the rank, the dimension
-        of the solution space begins to grow, because we can *still*
-        set the coordinates of "x" arbitrarily, but now there are some
-        coefficients that make the sum zero as well. So, when the
-        dimension of the variety jumps, we return the corresponding
-        "d" as the rank of the algebra. This appears to work.
+        We first compute the polynomial "column matrices" `p_{k}` that
+        evaluate to `x^k` on the coordinates of `x`. Then, we begin
+        adding them to a matrix one at a time, and trying to solve the
+        system that makes `p_{0}`,`p_{1}`,..., `p_{s-1}` add up to
+        `p_{s}`. This will succeed only when `s` is the rank of the
+        algebra, as proven in a recent draft paper of mine.
 
         SETUP::
 
@@ -825,81 +811,80 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             ....:                                  JordanSpinEJA,
             ....:                                  RealSymmetricEJA,
             ....:                                  ComplexHermitianEJA,
-            ....:                                  QuaternionHermitianEJA)
+            ....:                                  QuaternionHermitianEJA,
+            ....:                                  random_eja)
 
-        EXAMPLES::
+        EXAMPLES:
 
-            sage: J = HadamardEJA(5)
-            sage: J._rank_computation() == J.rank()
-            True
-            sage: J = JordanSpinEJA(5)
-            sage: J._rank_computation() == J.rank()
-            True
-            sage: J = RealSymmetricEJA(4)
-            sage: J._rank_computation() == J.rank()
-            True
-            sage: J = ComplexHermitianEJA(3)
-            sage: J._rank_computation() == J.rank()
+        The rank of the Jordan spin algebra is always two::
+
+            sage: JordanSpinEJA(2).rank()
+            2
+            sage: JordanSpinEJA(3).rank()
+            2
+            sage: JordanSpinEJA(4).rank()
+            2
+
+        The rank of the `n`-by-`n` Hermitian real, complex, or
+        quaternion matrices is `n`::
+
+            sage: RealSymmetricEJA(4).rank()
+            4
+            sage: ComplexHermitianEJA(3).rank()
+            3
+            sage: QuaternionHermitianEJA(2).rank()
+            2
+
+        TESTS:
+
+        Ensure that every EJA that we know how to construct has a
+        positive integer rank, unless the algebra is trivial in
+        which case its rank will be zero::
+
+            sage: set_random_seed()
+            sage: J = random_eja()
+            sage: r = J.rank()
+            sage: r in ZZ
             True
-            sage: J = QuaternionHermitianEJA(2)
-            sage: J._rank_computation() == J.rank()
+            sage: r > 0 or (r == 0 and J.is_trivial())
             True
 
-        """
-        n = self.dimension()
-        var_names = [ "X" + str(z) for z in range(1,n+1) ]
-        d = 0
-        ideal_dim = len(var_names)
-        def L_x_i_j(i,j):
-            # From a result in my book, these are the entries of the
-            # basis representation of L_x.
-            return sum( vars[d+k]*self.monomial(k).operator().matrix()[i,j]
-                        for k in range(n) )
+        Ensure that computing the rank actually works, since the ranks
+        of all simple algebras are known and will be cached by default::
 
-        while ideal_dim == len(var_names):
-            coeff_names = [ "a" + str(z) for z in range(d) ]
-            R = PolynomialRing(self.base_ring(), coeff_names + var_names)
-            vars = R.gens()
-            L_x = matrix(R, n, n, L_x_i_j)
-            x_powers = [ vars[k]*(L_x**k)*self.one().to_vector()
-                         for k in range(d) ]
-            eqs = [ sum(x_powers[k][j] for k in range(d)) for j in range(n) ]
-            ideal_dim = R.ideal(eqs).dimension()
-            d += 1
-
-        # Subtract one because we increment one too many times, and
-        # subtract another one because "d" is one greater than the
-        # answer anyway; when d=3, we go up to x^2.
-        return d-2
-
-    def _rank_computation2(self):
-        r"""
-        Instead of using the dimension of an ideal, find the rank of a
-        matrix containing polynomials.
-        """
-        n = self.dimension()
-        var_names = [ "X" + str(z) for z in range(1,n+1) ]
-        R = PolynomialRing(self.base_ring(), var_names)
-        vars = R.gens()
+            sage: J = HadamardEJA(4)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            4
 
-        def L_x_i_j(i,j):
-            # From a result in my book, these are the entries of the
-            # basis representation of L_x.
-            return sum( vars[k]*self.monomial(k).operator().matrix()[i,j]
-                        for k in range(n) )
+        ::
 
-        L_x = matrix(R, n, n, L_x_i_j)
-        x_powers = [ (vars[k]*(L_x**k)*self.one().to_vector()).row()
-                     for k in range(n) ]
+            sage: J = JordanSpinEJA(4)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            2
 
-        from sage.matrix.constructor import block_matrix
-        M = block_matrix(n,1,x_powers)
-        return M.rank()
+        ::
+
+            sage: J = RealSymmetricEJA(3)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            3
+
+        ::
+
+            sage: J = ComplexHermitianEJA(2)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            2
+
+        ::
+
+            sage: J = QuaternionHermitianEJA(2)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            2
 
-    def _rank_computation3(self):
-        r"""
-        Similar to :meth:`_rank_computation2`, but it stops echelonizing
-        as soon as it hits the first zero row.
         """
         n = self.dimension()
         if n == 0:
@@ -922,76 +907,22 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
                      for k in range(n) ]
 
         # Can assume n >= 2
-        rows = [x_powers[0]]
-        M = matrix(rows)
+        M = matrix([x_powers[0]])
         old_rank = 1
 
         for d in range(1,n):
-            rows = M.rows() + [x_powers[d]]
-            M = matrix(rows)
+            M = matrix(M.rows() + [x_powers[d]])
             M.echelonize()
+            # TODO: we've basically solved the system here.
+            # We should save the echelonized matrix somehow
+            # so that it can be reused in the charpoly method.
             new_rank = M.rank()
             if new_rank == old_rank:
                 return new_rank
             else:
                 old_rank = new_rank
 
-    def rank(self):
-        """
-        Return the rank of this EJA.
-
-        ALGORITHM:
-
-        The author knows of no algorithm to compute the rank of an EJA
-        where only the multiplication table is known. In lieu of one, we
-        require the rank to be specified when the algebra is created,
-        and simply pass along that number here.
-
-        SETUP::
-
-            sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
-            ....:                                  RealSymmetricEJA,
-            ....:                                  ComplexHermitianEJA,
-            ....:                                  QuaternionHermitianEJA,
-            ....:                                  random_eja)
-
-        EXAMPLES:
-
-        The rank of the Jordan spin algebra is always two::
-
-            sage: JordanSpinEJA(2).rank()
-            2
-            sage: JordanSpinEJA(3).rank()
-            2
-            sage: JordanSpinEJA(4).rank()
-            2
-
-        The rank of the `n`-by-`n` Hermitian real, complex, or
-        quaternion matrices is `n`::
-
-            sage: RealSymmetricEJA(4).rank()
-            4
-            sage: ComplexHermitianEJA(3).rank()
-            3
-            sage: QuaternionHermitianEJA(2).rank()
-            2
-
-        TESTS:
-
-        Ensure that every EJA that we know how to construct has a
-        positive integer rank, unless the algebra is trivial in
-        which case its rank will be zero::
-
-            sage: set_random_seed()
-            sage: J = random_eja()
-            sage: r = J.rank()
-            sage: r in ZZ
-            True
-            sage: r > 0 or (r == 0 and J.is_trivial())
-            True
-
-        """
-        return self._rank
+        return n
 
 
     def vector_space(self):
@@ -1115,7 +1046,8 @@ class HadamardEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
                        for i in range(n) ]
 
         fdeja = super(HadamardEJA, self)
-        return fdeja.__init__(field, mult_table, rank=n, **kwargs)
+        fdeja.__init__(field, mult_table, **kwargs)
+        self.rank.set_cache(n)
 
     def inner_product(self, x, y):
         """
@@ -1174,7 +1106,7 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
         # field can have dimension 4 (quaternions) too.
         return 2
 
-    def __init__(self, field, basis, rank, normalize_basis=True, **kwargs):
+    def __init__(self, field, basis, normalize_basis=True, **kwargs):
         """
         Compared to the superclass constructor, we take a basis instead of
         a multiplication table because the latter can be computed in terms
@@ -1187,7 +1119,7 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
         # time to ensure that it isn't a generator expression.
         basis = tuple(basis)
 
-        if rank > 1 and normalize_basis:
+        if len(basis) > 1 and normalize_basis:
             # We'll need sqrt(2) to normalize the basis, and this
             # winds up in the multiplication table, so the whole
             # algebra needs to be over the field extension.
@@ -1204,12 +1136,31 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
         Qs = self.multiplication_table_from_matrix_basis(basis)
 
         fdeja = super(MatrixEuclideanJordanAlgebra, self)
-        return fdeja.__init__(field,
-                              Qs,
-                              rank=rank,
-                              natural_basis=basis,
-                              **kwargs)
+        fdeja.__init__(field, Qs, natural_basis=basis, **kwargs)
+        return
+
 
+    @cached_method
+    def rank(self):
+        r"""
+        Override the parent method with something that tries to compute
+        over a faster (non-extension) field.
+        """
+        if self._basis_normalizers is None:
+            # We didn't normalize, so assume that the basis we started
+            # with had entries in a nice field.
+            return super(MatrixEuclideanJordanAlgebra, self).rank()
+        else:
+            basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
+                                             self._basis_normalizers) )
+
+            # Do this over the rationals and convert back at the end.
+            # Only works because we know the entries of the basis are
+            # integers.
+            J = MatrixEuclideanJordanAlgebra(QQ,
+                                             basis,
+                                             normalize_basis=False)
+            return J.rank()
 
     @cached_method
     def _charpoly_coeff(self, i):
@@ -1228,7 +1179,6 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
             # Do this over the rationals and convert back at the end.
             J = MatrixEuclideanJordanAlgebra(QQ,
                                              basis,
-                                             self.rank(),
                                              normalize_basis=False)
             (_,x,_,_) = J._charpoly_matrix_system()
             p = J._charpoly_coeff(i)
@@ -1457,7 +1407,8 @@ class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra, KnownRankEJA):
 
     def __init__(self, n, field=AA, **kwargs):
         basis = self._denormalized_basis(n, field)
-        super(RealSymmetricEJA, self).__init__(field, basis, n, **kwargs)
+        super(RealSymmetricEJA, self).__init__(field, basis, **kwargs)
+        self.rank.set_cache(n)
 
 
 class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
@@ -1747,7 +1698,8 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra, KnownRankEJA):
 
     def __init__(self, n, field=AA, **kwargs):
         basis = self._denormalized_basis(n,field)
-        super(ComplexHermitianEJA,self).__init__(field, basis, n, **kwargs)
+        super(ComplexHermitianEJA,self).__init__(field, basis, **kwargs)
+        self.rank.set_cache(n)
 
 
 class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
@@ -2043,7 +1995,8 @@ class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra,
 
     def __init__(self, n, field=AA, **kwargs):
         basis = self._denormalized_basis(n,field)
-        super(QuaternionHermitianEJA,self).__init__(field, basis, n, **kwargs)
+        super(QuaternionHermitianEJA,self).__init__(field, basis, **kwargs)
+        self.rank.set_cache(n)
 
 
 class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
@@ -2126,7 +2079,8 @@ class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
         # one-dimensional ambient space (because the rank is bounded
         # by the ambient dimension).
         fdeja = super(BilinearFormEJA, self)
-        return fdeja.__init__(field, mult_table, rank=min(n,2), **kwargs)
+        fdeja.__init__(field, mult_table, **kwargs)
+        self.rank.set_cache(min(n,2))
 
     def inner_product(self, x, y):
         r"""
@@ -2254,4 +2208,5 @@ class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
         fdeja = super(TrivialEJA, self)
         # The rank is zero using my definition, namely the dimension of the
         # largest subalgebra generated by any element.
-        return fdeja.__init__(field, mult_table, rank=0, **kwargs)
+        fdeja.__init__(field, mult_table, **kwargs)
+        self.rank.set_cache(0)