]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/euclidean_jordan_algebra.py
eja: finally enable tests for the trace inner product.
[sage.d.git] / mjo / eja / euclidean_jordan_algebra.py
index b5cef0a8ba16f8a2afb0a66a0f38a6ac6daaa6ae..c37ace0b2bbf1ce19f20e3127a4a5c80b7191b4e 100644 (file)
@@ -69,7 +69,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             True
 
         """
-        self._charpoly = None # for caching
         self._rank = rank
         self._natural_basis = natural_basis
         self._multiplication_table = mult_table
@@ -89,6 +88,56 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
 
 
+    @cached_method
+    def _charpoly_coeff(self, i):
+        """
+        Return the coefficient polynomial "a_{i}" of this algebra's
+        general characteristic polynomial.
+
+        Having this be a separate cached method lets us compute and
+        store the trace/determinant (a_{r-1} and a_{0} respectively)
+        separate from the entire characteristic polynomial.
+        """
+        (A_of_x, x) = self._charpoly_matrix()
+        R = A_of_x.base_ring()
+        A_cols = A_of_x.columns()
+        A_cols[i] = (x**self.rank()).vector()
+        numerator = column_matrix(A_of_x.base_ring(), A_cols).det()
+        denominator = A_of_x.det()
+
+        # We're relying on the theory here to ensure that each a_i is
+        # indeed back in R, and the added negative signs are to make
+        # the whole charpoly expression sum to zero.
+        return R(-numerator/denominator)
+
+
+    @cached_method
+    def _charpoly_matrix(self):
+        """
+        Compute the matrix whose entries A_ij are polynomials in
+        X1,...,XN. This same matrix is used in more than one method and
+        it's not so fast to construct.
+        """
+        r = self.rank()
+        n = self.dimension()
+
+        # Construct a new algebra over a multivariate polynomial ring...
+        names = ['X' + str(i) for i in range(1,n+1)]
+        R = PolynomialRing(self.base_ring(), names)
+        J = FiniteDimensionalEuclideanJordanAlgebra(R,
+                                                    self._multiplication_table,
+                                                    rank=r)
+
+        idmat = identity_matrix(J.base_ring(), n)
+
+        x = J(vector(R, R.gens()))
+        l1 = [column_matrix((x**k).vector()) for k in range(r)]
+        l2 = [idmat.column(k-1).column() for k in range(r+1, n+1)]
+        A_of_x = block_matrix(R, 1, n, (l1 + l2))
+        return (A_of_x, x)
+
+
+    @cached_method
     def characteristic_polynomial(self):
         """
         EXAMPLES:
@@ -104,72 +153,22 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             t^2 - 2*t + 1
 
         """
-        if self._charpoly is not None:
-            return self._charpoly
-
         r = self.rank()
         n = self.dimension()
 
-        # First, compute the basis B...
-        x0 = self.zero()
-        c = 1
-        for g in self.gens():
-            x0 += c*g
-            c +=1
-        if not x0.is_regular():
-            raise ValueError("don't know a regular element")
-
-        V = x0.vector().parent().ambient_vector_space()
-        V1 = V.span_of_basis( (x0**k).vector() for k in range(self.rank()) )
-        B =  (V1.basis() + V1.complement().basis())
-
-        # Now switch to the polynomial rings.
-
-        names = ['X' + str(i) for i in range(1,n+1)]
-        R = PolynomialRing(self.base_ring(), names)
-        J = FiniteDimensionalEuclideanJordanAlgebra(R,
-                                                    self._multiplication_table,
-                                                    rank=r)
-        B = [ b.change_ring(R.fraction_field()) for b in B ]
-        # Get the vector space (as opposed to module) so that
-        # span_of_basis() works.
-        V = J.zero().vector().parent().ambient_vector_space()
-        W = V.span_of_basis(B)
-
-        def e(k):
-            # The coordinates of e_k with respect to the basis B.
-            # But, the e_k are elements of B...
-            return identity_matrix(J.base_ring(), n).column(k-1).column()
-
-        # A matrix implementation 1
-        x = J(vector(R, R.gens()))
-        l1 = [column_matrix(W.coordinates((x**k).vector())) for k in range(r)]
-        l2 = [e(k) for k in range(r+1, n+1)]
-        A_of_x = block_matrix(1, n, (l1 + l2))
-        xr = W.coordinates((x**r).vector())
-        a = []
-        denominator = A_of_x.det() # This is constant
-        for i in range(n):
-            A_cols = A_of_x.columns()
-            A_cols[i] = xr
-            numerator = column_matrix(A_of_x.base_ring(), A_cols).det()
-            ai = numerator/denominator
-            a.append(ai)
+        # The list of coefficient polynomials a_1, a_2, ..., a_n.
+        a = [ self._charpoly_coeff(i) for i in range(n) ]
 
         # We go to a bit of trouble here to reorder the
         # indeterminates, so that it's easier to evaluate the
         # characteristic polynomial at x's coordinates and get back
         # something in terms of t, which is what we want.
+        R = a[0].parent()
         S = PolynomialRing(self.base_ring(),'t')
         t = S.gen(0)
         S = PolynomialRing(S, R.variable_names())
         t = S(t)
 
-        # We're relying on the theory here to ensure that each entry
-        # a[i] is indeed back in R, and the added negative signs are
-        # to make the whole expression sum to zero.
-        a = [R(-ai) for ai in a] # corresponds to powerx x^0 through x^(r-1)
-
         # Note: all entries past the rth should be zero. The
         # coefficient of the highest power (x^r) is 1, but it doesn't
         # appear in the solution vector which contains coefficients
@@ -181,8 +180,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             # assign a[r] goes out-of-bounds.
             a.append(1) # corresponds to x^r
 
-        self._charpoly = sum( a[k]*(t**k) for k in range(len(a)) )
-        return self._charpoly
+        return sum( a[k]*(t**k) for k in range(len(a)) )
 
 
     def inner_product(self, x, y):
@@ -410,17 +408,37 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
         def characteristic_polynomial(self):
             """
-            Return my characteristic polynomial (if I'm a regular
-            element).
+            Return the characteristic polynomial of this element.
+
+            EXAMPLES:
+
+            The rank of `R^3` is three, and the minimal polynomial of
+            the identity element is `(t-1)` from which it follows that
+            the characteristic polynomial should be `(t-1)^3`::
+
+                sage: J = RealCartesianProductEJA(3)
+                sage: J.one().characteristic_polynomial()
+                t^3 - 3*t^2 + 3*t - 1
+
+            Likewise, the characteristic of the zero element in the
+            rank-three algebra `R^{n}` should be `t^{3}`::
+
+                sage: J = RealCartesianProductEJA(3)
+                sage: J.zero().characteristic_polynomial()
+                t^3
+
+            The characteristic polynomial of an element should evaluate
+            to zero on that element::
+
+                sage: set_random_seed()
+                sage: x = RealCartesianProductEJA(3).random_element()
+                sage: p = x.characteristic_polynomial()
+                sage: x.apply_univariate_polynomial(p)
+                0
 
-            Eventually this should be implemented in terms of the parent
-            algebra's characteristic polynomial that works for ALL
-            elements.
             """
-            if self.is_regular():
-                return self.minimal_polynomial()
-            else:
-                raise NotImplementedError('irregular element')
+            p = self.parent().characteristic_polynomial()
+            return p(*self.vector())
 
 
         def inner_product(self, other):
@@ -530,22 +548,37 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
                 sage: J = JordanSpinEJA(2)
                 sage: e0,e1 = J.gens()
-                sage: x = e0 + e1
+                sage: x = sum( J.gens() )
                 sage: x.det()
                 0
+
+            ::
+
                 sage: J = JordanSpinEJA(3)
                 sage: e0,e1,e2 = J.gens()
-                sage: x = e0 + e1 + e2
+                sage: x = sum( J.gens() )
                 sage: x.det()
                 -1
 
+            TESTS:
+
+            An element is invertible if and only if its determinant is
+            non-zero::
+
+                sage: set_random_seed()
+                sage: x = random_eja().random_element()
+                sage: x.is_invertible() == (x.det() != 0)
+                True
+
             """
-            cs = self.characteristic_polynomial().coefficients(sparse=False)
-            r = len(cs) - 1
-            if r >= 0:
-                return cs[0] * (-1)**r
-            else:
-                raise ValueError('charpoly had no coefficients')
+            P = self.parent()
+            r = P.rank()
+            p = P._charpoly_coeff(0)
+            # The _charpoly_coeff function already adds the factor of
+            # -1 to ensure that _charpoly_coeff(0) is really what
+            # appears in front of t^{0} in the charpoly. However,
+            # we want (-1)^r times THAT for the determinant.
+            return ((-1)**r)*p(*self.vector())
 
 
         def inverse(self):
@@ -584,17 +617,12 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                 sage: J.one().inverse() == J.one()
                 True
 
-            If an element has an inverse, it acts like one. TODO: this
-            can be a lot less ugly once ``is_invertible`` doesn't crash
-            on irregular elements::
+            If an element has an inverse, it acts like one::
 
                 sage: set_random_seed()
                 sage: J = random_eja()
                 sage: x = J.random_element()
-                sage: try:
-                ....:     x.inverse()*x == J.one()
-                ....: except:
-                ....:     True
+                sage: (not x.is_invertible()) or (x.inverse()*x == J.one())
                 True
 
             """
@@ -1172,22 +1200,80 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             EXAMPLES::
 
                 sage: J = JordanSpinEJA(3)
-                sage: e0,e1,e2 = J.gens()
-                sage: x = e0 + e1 + e2
+                sage: x = sum(J.gens())
                 sage: x.trace()
                 2
 
+            ::
+
+                sage: J = RealCartesianProductEJA(5)
+                sage: J.one().trace()
+                5
+
+            TESTS:
+
+            The trace of an element is a real number::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: J.random_element().trace() in J.base_ring()
+                True
+
             """
-            cs = self.characteristic_polynomial().coefficients(sparse=False)
-            if len(cs) >= 2:
-                return -1*cs[-2]
-            else:
-                raise ValueError('charpoly had fewer than 2 coefficients')
+            P = self.parent()
+            r = P.rank()
+            p = P._charpoly_coeff(r-1)
+            # The _charpoly_coeff function already adds the factor of
+            # -1 to ensure that _charpoly_coeff(r-1) is really what
+            # appears in front of t^{r-1} in the charpoly. However,
+            # we want the negative of THAT for the trace.
+            return -p(*self.vector())
 
 
         def trace_inner_product(self, other):
             """
             Return the trace inner product of myself and ``other``.
+
+            TESTS:
+
+            The trace inner product is commutative::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: x = J.random_element(); y = J.random_element()
+                sage: x.trace_inner_product(y) == y.trace_inner_product(x)
+                True
+
+            The trace inner product is bilinear::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: x = J.random_element()
+                sage: y = J.random_element()
+                sage: z = J.random_element()
+                sage: a = QQ.random_element();
+                sage: actual = (a*(x+z)).trace_inner_product(y)
+                sage: expected = ( a*x.trace_inner_product(y) +
+                ....:              a*z.trace_inner_product(y) )
+                sage: actual == expected
+                True
+                sage: actual = x.trace_inner_product(a*(y+z))
+                sage: expected = ( a*x.trace_inner_product(y) +
+                ....:              a*x.trace_inner_product(z) )
+                sage: actual == expected
+                True
+
+            The trace inner product satisfies the compatibility
+            condition in the definition of a Euclidean Jordan algebra::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: x = J.random_element()
+                sage: y = J.random_element()
+                sage: z = J.random_element()
+                sage: (x*y).trace_inner_product(z) == y.trace_inner_product(x*z)
+                True
+
             """
             if not other in self.parent():
                 raise TypeError("'other' must live in the same algebra")