return fmt.format(self.degree(), self.base_ring())
- def characteristic_polynomial(self):
+ def _a_regular_element(self):
+ """
+ Guess a regular element. Needed to compute the basis for our
+ characteristic polynomial coefficients.
+ """
+ gs = self.gens()
+ z = self.sum( (i+1)*gs[i] for i in range(len(gs)) )
+ if not z.is_regular():
+ raise ValueError("don't know a regular element")
+ return z
+
+
+ @cached_method
+ def _charpoly_basis_space(self):
+ """
+ Return the vector space spanned by the basis used in our
+ characteristic polynomial coefficients. This is used not only to
+ compute those coefficients, but also any time we need to
+ evaluate the coefficients (like when we compute the trace or
+ determinant).
+ """
+ z = self._a_regular_element()
+ V = z.vector().parent().ambient_vector_space()
+ V1 = V.span_of_basis( (z**k).vector() for k in range(self.rank()) )
+ b = (V1.basis() + V1.complement().basis())
+ return V.span_of_basis(b)
+
+
+ @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, xr, detA) = self._charpoly_matrix_system()
+ R = A_of_x.base_ring()
+ if i >= self.rank():
+ # Guaranteed by theory
+ return R.zero()
+
+ # Danger: the in-place modification is done for performance
+ # reasons (reconstructing a matrix with huge polynomial
+ # entries is slow), but I don't know how cached_method works,
+ # so it's highly possible that we're modifying some global
+ # list variable by reference, here. In other words, you
+ # probably shouldn't call this method twice on the same
+ # algebra, at the same time, in two threads
+ Ai_orig = A_of_x.column(i)
+ A_of_x.set_column(i,xr)
+ numerator = A_of_x.det()
+ A_of_x.set_column(i,Ai_orig)
+
+ # 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/detA)
+
+
+ @cached_method
+ def _charpoly_matrix_system(self):
+ """
+ Compute the matrix whose entries A_ij are polynomials in
+ X1,...,XN, the vector ``x`` of variables X1,...,XN, the vector
+ corresponding to `x^r` and the determinent of the matrix A =
+ [A_ij]. In other words, all of the fixed (cachable) data needed
+ to compute the coefficients of the characteristic polynomial.
+ """
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)
- x0 = J.zero()
- c = 1
- for g in J.gens():
- x0 += c*g
- c +=1
- if not x0.is_regular():
- raise ValueError("don't know a regular element")
- # Get the vector space (as opposed to module) so that
- # span_of_basis() works.
- V = x0.vector().parent().ambient_vector_space()
- V1 = V.span_of_basis( (x0**k).vector() for k in range(r) )
- B = V1.basis() + V1.complement().basis()
- W = V.span_of_basis(B)
+ idmat = identity_matrix(J.base_ring(), n)
- 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()
+ W = self._charpoly_basis_space()
+ W = W.change_ring(R.fraction_field())
- # A matrix implementation 1
+ # Starting with the standard coordinates x = (X1,X2,...,Xn)
+ # and then converting the entries to W-coordinates allows us
+ # to pass in the standard coordinates to the charpoly and get
+ # back the right answer. Specifically, with x = (X1,X2,...,Xn),
+ # we have
+ #
+ # W.coordinates(x^2) eval'd at (standard z-coords)
+ # =
+ # W-coords of (z^2)
+ # =
+ # W-coords of (standard coords of x^2 eval'd at std-coords of z)
+ #
+ # We want the middle equivalent thing in our matrix, but use
+ # the first equivalent thing instead so that we can pass in
+ # standard coordinates.
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))
+ l2 = [idmat.column(k-1).column() for k in range(r+1, n+1)]
+ A_of_x = block_matrix(R, 1, n, (l1 + l2))
xr = W.coordinates((x**r).vector())
- a = []
- for i in range(n):
- A_cols = A.columns()
- A_cols[i] = xr
- numerator = column_matrix(A.base_ring(), A_cols).det()
- denominator = A.det()
- ai = numerator/denominator
- a.append(ai)
+ return (A_of_x, x, xr, A_of_x.det())
+
+
+ @cached_method
+ def characteristic_polynomial(self):
+ """
+
+ .. WARNING::
+
+ This implementation doesn't guarantee that the polynomial
+ denominator in the coefficients is not identically zero, so
+ theoretically it could crash. The way that this is handled
+ in e.g. Faraut and Koranyi is to use a basis that guarantees
+ the denominator is non-zero. But, doing so requires knowledge
+ of at least one regular element, and we don't even know how
+ to do that. The trade-off is that, if we use the standard basis,
+ the resulting polynomial will accept the "usual" coordinates. In
+ other words, we don't have to do a change of basis before e.g.
+ computing the trace or determinant.
+
+ EXAMPLES:
+
+ The characteristic polynomial in the spin algebra is given in
+ Alizadeh, Example 11.11::
+
+ sage: J = JordanSpinEJA(3)
+ sage: p = J.characteristic_polynomial(); p
+ X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
+ sage: xvec = J.one().vector()
+ sage: p(*xvec)
+ t^2 - 2*t + 1
+
+ """
+ r = self.rank()
+ n = self.dimension()
+
+ # 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)
+
+ # 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
+ # for the other powers (to make them sum to x^r).
+ if (r < n):
+ a[r] = 1 # corresponds to x^r
+ else:
+ # When the rank is equal to the dimension, trying to
+ # assign a[r] goes out-of-bounds.
+ a.append(1) # corresponds to x^r
- # Note: all entries past the rth should be zero.
- return a
+ return sum( a[k]*(t**k) for k in range(len(a)) )
def inner_product(self, x, y):
return A( (self.operator_matrix()**(n-1))*self.vector() )
+ def apply_univariate_polynomial(self, p):
+ """
+ Apply the univariate polynomial ``p`` to this element.
+
+ A priori, SageMath won't allow us to apply a univariate
+ polynomial to an element of an EJA, because we don't know
+ that EJAs are rings (they are usually not associative). Of
+ course, we know that EJAs are power-associative, so the
+ operation is ultimately kosher. This function sidesteps
+ the CAS to get the answer we want and expect.
+
+ EXAMPLES::
+
+ sage: R = PolynomialRing(QQ, 't')
+ sage: t = R.gen(0)
+ sage: p = t^4 - t^3 + 5*t - 2
+ sage: J = RealCartesianProductEJA(5)
+ sage: J.one().apply_univariate_polynomial(p) == 3*J.one()
+ True
+
+ TESTS:
+
+ We should always get back an element of the algebra::
+
+ sage: set_random_seed()
+ sage: p = PolynomialRing(QQ, 't').random_element()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: x.apply_univariate_polynomial(p) in J
+ True
+
+ """
+ if len(p.variables()) > 1:
+ raise ValueError("not a univariate polynomial")
+ P = self.parent()
+ R = P.base_ring()
+ # Convert the coeficcients to the parent's base ring,
+ # because a priori they might live in an (unnecessarily)
+ # larger ring for which P.sum() would fail below.
+ cs = [ R(c) for c in p.coefficients(sparse=False) ]
+ return P.sum( cs[k]*(self**k) for k in range(len(cs)) )
+
+
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):
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):
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
"""
+ if not self.is_invertible():
+ raise ValueError("element not invertible")
+
if self.parent().is_associative():
elt = FiniteDimensionalAlgebraElement(self.parent(), self)
- return elt.inverse()
-
- # TODO: we can do better once the call to is_invertible()
- # doesn't crash on irregular elements.
- #if not self.is_invertible():
- # raise ValueError('element is not invertible')
+ # elt is in the right coordinates, but has the wrong class.
+ return self.parent()(elt.inverse().vector())
# We do this a little different than the usual recursive
# call to a finite-dimensional algebra element, because we
sage: bool(actual == expected)
True
+ The minimal polynomial should always kill its element::
+
+ sage: set_random_seed()
+ sage: x = random_eja().random_element()
+ sage: p = x.minimal_polynomial()
+ sage: x.apply_univariate_polynomial(p)
+ 0
+
"""
V = self.span_of_powers()
assoc_subalg = self.subalgebra_generated_by()
TESTS::
sage: set_random_seed()
- sage: J = RealCartesianProductEJA(5)
- sage: c = J.random_element().subalgebra_idempotent()
- sage: c^2 == c
- True
- sage: J = JordanSpinEJA(5)
- sage: c = J.random_element().subalgebra_idempotent()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: while x.is_nilpotent():
+ ....: x = J.random_element()
+ sage: c = x.subalgebra_idempotent()
sage: c^2 == c
True
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")
Euclidean Jordan algebra of degree...
"""
- n = ZZ.random_element(1,5)
- constructor = choice([RealCartesianProductEJA,
- JordanSpinEJA,
- RealSymmetricEJA,
- ComplexHermitianEJA,
- QuaternionHermitianEJA])
+
+ # The max_n component lets us choose different upper bounds on the
+ # value "n" that gets passed to the constructor. This is needed
+ # because e.g. R^{10} is reasonable to test, while the Hermitian
+ # 10-by-10 quaternion matrices are not.
+ (constructor, max_n) = choice([(RealCartesianProductEJA, 6),
+ (JordanSpinEJA, 6),
+ (RealSymmetricEJA, 5),
+ (ComplexHermitianEJA, 4),
+ (QuaternionHermitianEJA, 3)])
+ n = ZZ.random_element(1, max_n)
return constructor(n, field=QQ)