X-Git-Url: http://gitweb.michael.orlitzky.com/?a=blobdiff_plain;f=mjo%2Feja%2Feja_algebra.py;h=02ed966540132c07af408a1a9fb4e6eebc220c31;hb=b05705ab04692f738a57a6ef387662ba5ea46ceb;hp=413128c843647e038e8869471daf29d84f1470f2;hpb=16825a1ceedeb8363b025cda56dc9f65f639f726;p=sage.d.git diff --git a/mjo/eja/eja_algebra.py b/mjo/eja/eja_algebra.py index 413128c..02ed966 100644 --- a/mjo/eja/eja_algebra.py +++ b/mjo/eja/eja_algebra.py @@ -5,6 +5,8 @@ are used in optimization, and have some additional nice methods beyond what can be supported in a general Jordan Algebra. """ +from itertools import izip, repeat + from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra from sage.categories.magmatic_algebras import MagmaticAlgebras from sage.combinat.free_module import CombinatorialFreeModule @@ -51,8 +53,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): sage: set_random_seed() sage: J = random_eja() - sage: x = J.random_element() - sage: y = J.random_element() + sage: x,y = J.random_elements(2) sage: x*y == y*x True @@ -60,9 +61,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): self._rank = rank self._natural_basis = natural_basis - # TODO: HACK for the charpoly.. needs redesign badly. - self._basis_normalizers = None - if category is None: category = MagmaticAlgebras(field).FiniteDimensional() category = category.WithBasis().Unital() @@ -155,26 +153,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): return self.from_vector(coords) - @staticmethod - def _max_test_case_size(): - """ - Return an integer "size" that is an upper bound on the size of - this algebra when it is used in a random test - case. Unfortunately, the term "size" is quite vague -- when - dealing with `R^n` under either the Hadamard or Jordan spin - product, the "size" refers to the dimension `n`. When dealing - with a matrix algebra (real symmetric or complex/quaternion - Hermitian), it refers to the size of the matrix, which is - far less than the dimension of the underlying vector space. - - We default to five in this class, which is safe in `R^n`. The - matrix algebra subclasses (or any class where the "size" is - interpreted to be far less than the dimension) should override - with a smaller number. - """ - return 5 - - def _repr_(self): """ Return a string representation of ``self``. @@ -258,19 +236,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): store the trace/determinant (a_{r-1} and a_{0} respectively) separate from the entire characteristic polynomial. """ - if self._basis_normalizers is not None: - # Must be a matrix class? - # WARNING/TODO: this whole mess is mis-designed. - n = self.natural_basis_space().nrows() - field = self.base_ring().base_ring() # yeeeeaaaahhh - J = self.__class__(n, field, False) - (_,x,_,_) = J._charpoly_matrix_system() - p = J._charpoly_coeff(i) - # p might be missing some vars, have to substitute "optionally" - pairs = zip(x.base_ring().gens(), self._basis_normalizers) - substitutions = { v: v*c for (v,c) in pairs } - return p.subs(substitutions) - (A_of_x, x, xr, detA) = self._charpoly_matrix_system() R = A_of_x.base_ring() if i >= self.rank(): @@ -424,7 +389,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): # assign a[r] goes out-of-bounds. a.append(1) # corresponds to x^r - return sum( a[k]*(t**k) for k in range(len(a)) ) + return sum( a[k]*(t**k) for k in xrange(len(a)) ) def inner_product(self, x, y): @@ -441,14 +406,12 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): EXAMPLES: - The inner product must satisfy its axiom for this algebra to truly - be a Euclidean Jordan Algebra:: + Our inner product is "associative," which means the following for + a symmetric bilinear form:: 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,z = J.random_elements(3) sage: (x*y).inner_product(z) == y.inner_product(x*z) True @@ -508,7 +471,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): """ M = list(self._multiplication_table) # copy - for i in range(len(M)): + for i in xrange(len(M)): # M had better be "square" M[i] = [self.monomial(i)] + M[i] M = [["*"] + list(self.gens())] + M @@ -657,27 +620,25 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): s = super(FiniteDimensionalEuclideanJordanAlgebra, self) return s.random_element() - - @classmethod - def random_instance(cls, field=QQ, **kwargs): + def random_elements(self, count): """ - Return a random instance of this type of algebra. + Return ``count`` random elements as a tuple. - In subclasses for algebras that we know how to construct, this - is a shortcut for constructing test cases and examples. - """ - if cls is FiniteDimensionalEuclideanJordanAlgebra: - # Red flag! But in theory we could do this I guess. The - # only finite-dimensional exceptional EJA is the - # octononions. So, we could just create an EJA from an - # associative matrix algebra (generated by a subset of - # elements) with the symmetric product. Or, we could punt - # to random_eja() here, override it in our subclasses, and - # not worry about it. - raise NotImplementedError + SETUP:: - n = ZZ.random_element(1, cls._max_test_case_size()) - return cls(n, field, **kwargs) + sage: from mjo.eja.eja_algebra import JordanSpinEJA + + EXAMPLES:: + + sage: J = JordanSpinEJA(3) + sage: x,y,z = J.random_elements(3) + sage: all( [ x in J, y in J, z in J ]) + True + sage: len( J.random_elements(10) ) == 10 + True + + """ + return tuple( self.random_element() for idx in xrange(count) ) def rank(self): @@ -713,18 +674,12 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): The rank of the `n`-by-`n` Hermitian real, complex, or quaternion matrices is `n`:: - sage: RealSymmetricEJA(2).rank() - 2 - sage: ComplexHermitianEJA(2).rank() - 2 + sage: RealSymmetricEJA(4).rank() + 4 + sage: ComplexHermitianEJA(3).rank() + 3 sage: QuaternionHermitianEJA(2).rank() 2 - sage: RealSymmetricEJA(5).rank() - 5 - sage: ComplexHermitianEJA(5).rank() - 5 - sage: QuaternionHermitianEJA(5).rank() - 5 TESTS: @@ -761,7 +716,57 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): Element = FiniteDimensionalEuclideanJordanAlgebraElement -class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra): +class KnownRankEJA(object): + """ + A class for algebras that we actually know we can construct. The + main issue is that, for most of our methods to make sense, we need + to know the rank of our algebra. Thus we can't simply generate a + "random" algebra, or even check that a given basis and product + satisfy the axioms; because even if everything looks OK, we wouldn't + know the rank we need to actuallty build the thing. + + Not really a subclass of FDEJA because doing that causes method + resolution errors, e.g. + + TypeError: Error when calling the metaclass bases + Cannot create a consistent method resolution + order (MRO) for bases FiniteDimensionalEuclideanJordanAlgebra, + KnownRankEJA + + """ + @staticmethod + def _max_test_case_size(): + """ + Return an integer "size" that is an upper bound on the size of + this algebra when it is used in a random test + case. Unfortunately, the term "size" is quite vague -- when + dealing with `R^n` under either the Hadamard or Jordan spin + product, the "size" refers to the dimension `n`. When dealing + with a matrix algebra (real symmetric or complex/quaternion + Hermitian), it refers to the size of the matrix, which is + far less than the dimension of the underlying vector space. + + We default to five in this class, which is safe in `R^n`. The + matrix algebra subclasses (or any class where the "size" is + interpreted to be far less than the dimension) should override + with a smaller number. + """ + return 5 + + @classmethod + def random_instance(cls, field=QQ, **kwargs): + """ + Return a random instance of this type of algebra. + + Beware, this will crash for "most instances" because the + constructor below looks wrong. + """ + n = ZZ.random_element(cls._max_test_case_size()) + 1 + return cls(n, field, **kwargs) + + +class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra, + KnownRankEJA): """ Return the Euclidean Jordan Algebra corresponding to the set `R^n` under the Hadamard product. @@ -800,21 +805,11 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: RealCartesianProductEJA(3, prefix='r').gens() (r0, r1, r2) - Our inner product satisfies the Jordan axiom:: - - sage: set_random_seed() - sage: J = RealCartesianProductEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() - sage: z = J.random_element() - sage: (x*y).inner_product(z) == y.inner_product(x*z) - True - """ def __init__(self, n, field=QQ, **kwargs): V = VectorSpace(field, n) - mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ] - for i in range(n) ] + mult_table = [ [ V.gen(i)*(i == j) for j in xrange(n) ] + for i in xrange(n) ] fdeja = super(RealCartesianProductEJA, self) return fdeja.__init__(field, mult_table, rank=n, **kwargs) @@ -834,8 +829,7 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: set_random_seed() sage: J = RealCartesianProductEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() + sage: x,y = J.random_elements(2) sage: X = x.natural_representation() sage: Y = y.natural_representation() sage: x.inner_product(y) == J.natural_inner_product(X,Y) @@ -881,431 +875,178 @@ def random_eja(): Euclidean Jordan algebra of dimension... """ - classname = choice([RealCartesianProductEJA, - JordanSpinEJA, - RealSymmetricEJA, - ComplexHermitianEJA, - QuaternionHermitianEJA]) + classname = choice(KnownRankEJA.__subclasses__()) return classname.random_instance() -def _real_symmetric_basis(n, field): - """ - Return a basis for the space of real symmetric n-by-n matrices. - - SETUP:: - - sage: from mjo.eja.eja_algebra import _real_symmetric_basis - - TESTS:: - - sage: set_random_seed() - sage: n = ZZ.random_element(1,5) - sage: B = _real_symmetric_basis(n, QQ) - sage: all( M.is_symmetric() for M in B) - True - - """ - # The basis of symmetric matrices, as matrices, in their R^(n-by-n) - # coordinates. - S = [] - for i in xrange(n): - for j in xrange(i+1): - Eij = matrix(field, n, lambda k,l: k==i and l==j) - if i == j: - Sij = Eij - else: - Sij = Eij + Eij.transpose() - S.append(Sij) - return tuple(S) - - -def _complex_hermitian_basis(n, field): - """ - Returns a basis for the space of complex Hermitian n-by-n matrices. - - Why do we embed these? Basically, because all of numerical linear - algebra assumes that you're working with vectors consisting of `n` - entries from a field and scalars from the same field. There's no way - to tell SageMath that (for example) the vectors contain complex - numbers, while the scalar field is real. - SETUP:: - - sage: from mjo.eja.eja_algebra import _complex_hermitian_basis - TESTS:: - - sage: set_random_seed() - sage: n = ZZ.random_element(1,5) - sage: field = QuadraticField(2, 'sqrt2') - sage: B = _complex_hermitian_basis(n, field) - sage: all( M.is_symmetric() for M in B) - True - - """ - R = PolynomialRing(field, 'z') - z = R.gen() - F = NumberField(z**2 + 1, 'I', embedding=CLF(-1).sqrt()) - I = F.gen() - - # This is like the symmetric case, but we need to be careful: - # - # * We want conjugate-symmetry, not just symmetry. - # * The diagonal will (as a result) be real. - # - S = [] - for i in xrange(n): - for j in xrange(i+1): - Eij = matrix(F, n, lambda k,l: k==i and l==j) - if i == j: - Sij = _embed_complex_matrix(Eij) - S.append(Sij) - else: - # The second one has a minus because it's conjugated. - Sij_real = _embed_complex_matrix(Eij + Eij.transpose()) - S.append(Sij_real) - Sij_imag = _embed_complex_matrix(I*Eij - I*Eij.transpose()) - S.append(Sij_imag) - # Since we embedded these, we can drop back to the "field" that we - # started with instead of the complex extension "F". - return tuple( s.change_ring(field) for s in S ) - - - -def _quaternion_hermitian_basis(n, field): - """ - Returns a basis for the space of quaternion Hermitian n-by-n matrices. - - Why do we embed these? Basically, because all of numerical linear - algebra assumes that you're working with vectors consisting of `n` - entries from a field and scalars from the same field. There's no way - to tell SageMath that (for example) the vectors contain complex - numbers, while the scalar field is real. - - SETUP:: - - sage: from mjo.eja.eja_algebra import _quaternion_hermitian_basis - - TESTS:: - - sage: set_random_seed() - sage: n = ZZ.random_element(1,5) - sage: B = _quaternion_hermitian_basis(n, QQ) - sage: all( M.is_symmetric() for M in B ) - True - - """ - Q = QuaternionAlgebra(QQ,-1,-1) - I,J,K = Q.gens() - - # This is like the symmetric case, but we need to be careful: - # - # * We want conjugate-symmetry, not just symmetry. - # * The diagonal will (as a result) be real. - # - S = [] - for i in xrange(n): - for j in xrange(i+1): - Eij = matrix(Q, n, lambda k,l: k==i and l==j) - if i == j: - Sij = _embed_quaternion_matrix(Eij) - S.append(Sij) - else: - # Beware, orthogonal but not normalized! The second, - # third, and fourth ones have a minus because they're - # conjugated. - Sij_real = _embed_quaternion_matrix(Eij + Eij.transpose()) - S.append(Sij_real) - Sij_I = _embed_quaternion_matrix(I*Eij - I*Eij.transpose()) - S.append(Sij_I) - Sij_J = _embed_quaternion_matrix(J*Eij - J*Eij.transpose()) - S.append(Sij_J) - Sij_K = _embed_quaternion_matrix(K*Eij - K*Eij.transpose()) - S.append(Sij_K) - return tuple(S) - - - -def _multiplication_table_from_matrix_basis(basis): - """ - At least three of the five simple Euclidean Jordan algebras have the - symmetric multiplication (A,B) |-> (AB + BA)/2, where the - multiplication on the right is matrix multiplication. Given a basis - for the underlying matrix space, this function returns a - multiplication table (obtained by looping through the basis - elements) for an algebra of those matrices. - """ - # In S^2, for example, we nominally have four coordinates even - # though the space is of dimension three only. The vector space V - # is supposed to hold the entire long vector, and the subspace W - # of V will be spanned by the vectors that arise from symmetric - # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3. - field = basis[0].base_ring() - dimension = basis[0].nrows() - - V = VectorSpace(field, dimension**2) - W = V.span_of_basis( _mat2vec(s) for s in basis ) - n = len(basis) - mult_table = [[W.zero() for j in range(n)] for i in range(n)] - for i in range(n): - for j in range(n): - mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2 - mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry)) - - return mult_table - - -def _embed_complex_matrix(M): - """ - Embed the n-by-n complex matrix ``M`` into the space of real - matrices of size 2n-by-2n via the map the sends each entry `z = a + - bi` to the block matrix ``[[a,b],[-b,a]]``. - - SETUP:: - - sage: from mjo.eja.eja_algebra import (_embed_complex_matrix, - ....: ComplexHermitianEJA) - - EXAMPLES:: - - sage: F = QuadraticField(-1, 'i') - sage: x1 = F(4 - 2*i) - sage: x2 = F(1 + 2*i) - sage: x3 = F(-i) - sage: x4 = F(6) - sage: M = matrix(F,2,[[x1,x2],[x3,x4]]) - sage: _embed_complex_matrix(M) - [ 4 -2| 1 2] - [ 2 4|-2 1] - [-----+-----] - [ 0 -1| 6 0] - [ 1 0| 0 6] - - TESTS: - - Embedding is a homomorphism (isomorphism, in fact):: - - sage: set_random_seed() - sage: n_max = ComplexHermitianEJA._max_test_case_size() - sage: n = ZZ.random_element(n_max) - sage: F = QuadraticField(-1, 'i') - sage: X = random_matrix(F, n) - sage: Y = random_matrix(F, n) - sage: actual = _embed_complex_matrix(X) * _embed_complex_matrix(Y) - sage: expected = _embed_complex_matrix(X*Y) - sage: actual == expected - True - - """ - n = M.nrows() - if M.ncols() != n: - raise ValueError("the matrix 'M' must be square") - field = M.base_ring() - blocks = [] - for z in M.list(): - a = z.vector()[0] # real part, I guess - b = z.vector()[1] # imag part, I guess - blocks.append(matrix(field, 2, [[a,b],[-b,a]])) - - # We can drop the imaginaries here. - return matrix.block(field.base_ring(), n, blocks) - - -def _unembed_complex_matrix(M): - """ - The inverse of _embed_complex_matrix(). - - SETUP:: - - sage: from mjo.eja.eja_algebra import (_embed_complex_matrix, - ....: _unembed_complex_matrix) +class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra): + @staticmethod + def _max_test_case_size(): + # Play it safe, since this will be squared and the underlying + # field can have dimension 4 (quaternions) too. + return 2 - EXAMPLES:: + def __init__(self, field, basis, rank, 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 + of the former when the product is known (like it is here). + """ + # Used in this class's fast _charpoly_coeff() override. + self._basis_normalizers = None - sage: A = matrix(QQ,[ [ 1, 2, 3, 4], - ....: [-2, 1, -4, 3], - ....: [ 9, 10, 11, 12], - ....: [-10, 9, -12, 11] ]) - sage: _unembed_complex_matrix(A) - [ 2*i + 1 4*i + 3] - [ 10*i + 9 12*i + 11] + # We're going to loop through this a few times, so now's a good + # time to ensure that it isn't a generator expression. + basis = tuple(basis) - TESTS: + if rank > 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. + R = PolynomialRing(field, 'z') + z = R.gen() + p = z**2 - 2 + if p.is_irreducible(): + field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt()) + basis = tuple( s.change_ring(field) for s in basis ) + self._basis_normalizers = tuple( + ~(self.natural_inner_product(s,s).sqrt()) for s in basis ) + basis = tuple(s*c for (s,c) in izip(basis,self._basis_normalizers)) - Unembedding is the inverse of embedding:: + Qs = self.multiplication_table_from_matrix_basis(basis) - sage: set_random_seed() - sage: F = QuadraticField(-1, 'i') - sage: M = random_matrix(F, 3) - sage: _unembed_complex_matrix(_embed_complex_matrix(M)) == M - True + fdeja = super(MatrixEuclideanJordanAlgebra, self) + return fdeja.__init__(field, + Qs, + rank=rank, + natural_basis=basis, + **kwargs) - """ - n = ZZ(M.nrows()) - if M.ncols() != n: - raise ValueError("the matrix 'M' must be square") - if not n.mod(2).is_zero(): - raise ValueError("the matrix 'M' must be a complex embedding") - - field = M.base_ring() # This should already have sqrt2 - R = PolynomialRing(field, 'z') - z = R.gen() - F = NumberField(z**2 + 1,'i', embedding=CLF(-1).sqrt()) - i = F.gen() - - # Go top-left to bottom-right (reading order), converting every - # 2-by-2 block we see to a single complex element. - elements = [] - for k in xrange(n/2): - for j in xrange(n/2): - submat = M[2*k:2*k+2,2*j:2*j+2] - if submat[0,0] != submat[1,1]: - raise ValueError('bad on-diagonal submatrix') - if submat[0,1] != -submat[1,0]: - raise ValueError('bad off-diagonal submatrix') - z = submat[0,0] + submat[0,1]*i - elements.append(z) - - return matrix(F, n/2, elements) - - -def _embed_quaternion_matrix(M): - """ - Embed the n-by-n quaternion matrix ``M`` into the space of real - matrices of size 4n-by-4n by first sending each quaternion entry - `z = a + bi + cj + dk` to the block-complex matrix - ``[[a + bi, c+di],[-c + di, a-bi]]`, and then embedding those into - a real matrix. - SETUP:: + @cached_method + def _charpoly_coeff(self, i): + """ + 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)._charpoly_coeff(i) + else: + basis = ( (b/n) for (b,n) in izip(self.natural_basis(), + self._basis_normalizers) ) + field = self.base_ring().base_ring() # yeeeaahhhhhhh + J = MatrixEuclideanJordanAlgebra(field, + basis, + self.rank(), + normalize_basis=False) + (_,x,_,_) = J._charpoly_matrix_system() + p = J._charpoly_coeff(i) + # p might be missing some vars, have to substitute "optionally" + pairs = izip(x.base_ring().gens(), self._basis_normalizers) + substitutions = { v: v*c for (v,c) in pairs } + return p.subs(substitutions) - sage: from mjo.eja.eja_algebra import (_embed_quaternion_matrix, - ....: QuaternionHermitianEJA) - EXAMPLES:: + @staticmethod + def multiplication_table_from_matrix_basis(basis): + """ + At least three of the five simple Euclidean Jordan algebras have the + symmetric multiplication (A,B) |-> (AB + BA)/2, where the + multiplication on the right is matrix multiplication. Given a basis + for the underlying matrix space, this function returns a + multiplication table (obtained by looping through the basis + elements) for an algebra of those matrices. + """ + # In S^2, for example, we nominally have four coordinates even + # though the space is of dimension three only. The vector space V + # is supposed to hold the entire long vector, and the subspace W + # of V will be spanned by the vectors that arise from symmetric + # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3. + field = basis[0].base_ring() + dimension = basis[0].nrows() + + V = VectorSpace(field, dimension**2) + W = V.span_of_basis( _mat2vec(s) for s in basis ) + n = len(basis) + mult_table = [[W.zero() for j in xrange(n)] for i in xrange(n)] + for i in xrange(n): + for j in xrange(n): + mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2 + mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry)) + + return mult_table - sage: Q = QuaternionAlgebra(QQ,-1,-1) - sage: i,j,k = Q.gens() - sage: x = 1 + 2*i + 3*j + 4*k - sage: M = matrix(Q, 1, [[x]]) - sage: _embed_quaternion_matrix(M) - [ 1 2 3 4] - [-2 1 -4 3] - [-3 4 1 -2] - [-4 -3 2 1] - Embedding is a homomorphism (isomorphism, in fact):: + @staticmethod + def real_embed(M): + """ + Embed the matrix ``M`` into a space of real matrices. - sage: set_random_seed() - sage: n_max = QuaternionHermitianEJA._max_test_case_size() - sage: n = ZZ.random_element(n_max) - sage: Q = QuaternionAlgebra(QQ,-1,-1) - sage: X = random_matrix(Q, n) - sage: Y = random_matrix(Q, n) - sage: actual = _embed_quaternion_matrix(X)*_embed_quaternion_matrix(Y) - sage: expected = _embed_quaternion_matrix(X*Y) - sage: actual == expected - True + The matrix ``M`` can have entries in any field at the moment: + the real numbers, complex numbers, or quaternions. And although + they are not a field, we can probably support octonions at some + point, too. This function returns a real matrix that "acts like" + the original with respect to matrix multiplication; i.e. - """ - quaternions = M.base_ring() - n = M.nrows() - if M.ncols() != n: - raise ValueError("the matrix 'M' must be square") - - F = QuadraticField(-1, 'i') - i = F.gen() - - blocks = [] - for z in M.list(): - t = z.coefficient_tuple() - a = t[0] - b = t[1] - c = t[2] - d = t[3] - cplx_matrix = matrix(F, 2, [[ a + b*i, c + d*i], - [-c + d*i, a - b*i]]) - blocks.append(_embed_complex_matrix(cplx_matrix)) - - # We should have real entries by now, so use the realest field - # we've got for the return value. - return matrix.block(quaternions.base_ring(), n, blocks) - - -def _unembed_quaternion_matrix(M): - """ - The inverse of _embed_quaternion_matrix(). + real_embed(M*N) = real_embed(M)*real_embed(N) - SETUP:: + """ + raise NotImplementedError - sage: from mjo.eja.eja_algebra import (_embed_quaternion_matrix, - ....: _unembed_quaternion_matrix) - EXAMPLES:: + @staticmethod + def real_unembed(M): + """ + The inverse of :meth:`real_embed`. + """ + raise NotImplementedError - sage: M = matrix(QQ, [[ 1, 2, 3, 4], - ....: [-2, 1, -4, 3], - ....: [-3, 4, 1, -2], - ....: [-4, -3, 2, 1]]) - sage: _unembed_quaternion_matrix(M) - [1 + 2*i + 3*j + 4*k] - TESTS: + @classmethod + def natural_inner_product(cls,X,Y): + Xu = cls.real_unembed(X) + Yu = cls.real_unembed(Y) + tr = (Xu*Yu).trace() + if tr in RLF: + # It's real already. + return tr + + # Otherwise, try the thing that works for complex numbers; and + # if that doesn't work, the thing that works for quaternions. + try: + return tr.vector()[0] # real part, imag part is index 1 + except AttributeError: + # A quaternions doesn't have a vector() method, but does + # have coefficient_tuple() method that returns the + # coefficients of 1, i, j, and k -- in that order. + return tr.coefficient_tuple()[0] + + +class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): + @staticmethod + def real_embed(M): + """ + The identity function, for embedding real matrices into real + matrices. + """ + return M - Unembedding is the inverse of embedding:: + @staticmethod + def real_unembed(M): + """ + The identity function, for unembedding real matrices from real + matrices. + """ + return M - sage: set_random_seed() - sage: Q = QuaternionAlgebra(QQ, -1, -1) - sage: M = random_matrix(Q, 3) - sage: _unembed_quaternion_matrix(_embed_quaternion_matrix(M)) == M - True - """ - n = ZZ(M.nrows()) - if M.ncols() != n: - raise ValueError("the matrix 'M' must be square") - if not n.mod(4).is_zero(): - raise ValueError("the matrix 'M' must be a complex embedding") - - # Use the base ring of the matrix to ensure that its entries can be - # multiplied by elements of the quaternion algebra. - field = M.base_ring() - Q = QuaternionAlgebra(field,-1,-1) - i,j,k = Q.gens() - - # Go top-left to bottom-right (reading order), converting every - # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1 - # quaternion block. - elements = [] - for l in xrange(n/4): - for m in xrange(n/4): - submat = _unembed_complex_matrix(M[4*l:4*l+4,4*m:4*m+4]) - if submat[0,0] != submat[1,1].conjugate(): - raise ValueError('bad on-diagonal submatrix') - if submat[0,1] != -submat[1,0].conjugate(): - raise ValueError('bad off-diagonal submatrix') - z = submat[0,0].vector()[0] # real part - z += submat[0,0].vector()[1]*i # imag part - z += submat[0,1].vector()[0]*j # real part - z += submat[0,1].vector()[1]*k # imag part - elements.append(z) - - return matrix(Q, n/4, elements) - - -# The inner product used for the real symmetric simple EJA. -# We keep it as a separate function because e.g. the complex -# algebra uses the same inner product, except divided by 2. -def _matrix_ip(X,Y): - X_mat = X.natural_representation() - Y_mat = Y.natural_representation() - return (X_mat*Y_mat).trace() - - -class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra): +class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra, KnownRankEJA): """ The rank-n simple EJA consisting of real symmetric n-by-n matrices, the usual symmetric Jordan product, and the trace inner @@ -1341,8 +1082,7 @@ class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: set_random_seed() sage: J = RealSymmetricEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() + sage: x,y = J.random_elements(2) sage: actual = (x*y).natural_representation() sage: X = x.natural_representation() sage: Y = y.natural_representation() @@ -1357,16 +1097,6 @@ class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: RealSymmetricEJA(3, prefix='q').gens() (q0, q1, q2, q3, q4, q5) - Our inner product satisfies the Jordan axiom:: - - sage: set_random_seed() - sage: J = RealSymmetricEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() - sage: z = J.random_element() - sage: (x*y).inner_product(z) == y.inner_product(x*z) - True - Our natural basis is normalized with respect to the natural inner product unless we specify otherwise:: @@ -1387,38 +1117,199 @@ class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra): True """ - def __init__(self, n, field=QQ, normalize_basis=True, **kwargs): - S = _real_symmetric_basis(n, field) + @classmethod + def _denormalized_basis(cls, n, field): + """ + Return a basis for the space of real symmetric n-by-n matrices. - if n > 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. - R = PolynomialRing(field, 'z') - z = R.gen() - p = z**2 - 2 - if p.is_irreducible(): - field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt()) - S = [ s.change_ring(field) for s in S ] - self._basis_normalizers = tuple( - ~(self.natural_inner_product(s,s).sqrt()) for s in S ) - S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) ) + SETUP:: - Qs = _multiplication_table_from_matrix_basis(S) + sage: from mjo.eja.eja_algebra import RealSymmetricEJA + + TESTS:: + + sage: set_random_seed() + sage: n = ZZ.random_element(1,5) + sage: B = RealSymmetricEJA._denormalized_basis(n,QQ) + sage: all( M.is_symmetric() for M in B) + True + + """ + # The basis of symmetric matrices, as matrices, in their R^(n-by-n) + # coordinates. + S = [] + for i in xrange(n): + for j in xrange(i+1): + Eij = matrix(field, n, lambda k,l: k==i and l==j) + if i == j: + Sij = Eij + else: + Sij = Eij + Eij.transpose() + S.append(Sij) + return S - fdeja = super(RealSymmetricEJA, self) - return fdeja.__init__(field, - Qs, - rank=n, - natural_basis=S, - **kwargs) @staticmethod def _max_test_case_size(): - return 5 + return 4 # Dimension 10 + + + def __init__(self, n, field=QQ, **kwargs): + basis = self._denormalized_basis(n, field) + super(RealSymmetricEJA, self).__init__(field, basis, n, **kwargs) + + +class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): + @staticmethod + def real_embed(M): + """ + Embed the n-by-n complex matrix ``M`` into the space of real + matrices of size 2n-by-2n via the map the sends each entry `z = a + + bi` to the block matrix ``[[a,b],[-b,a]]``. + + SETUP:: + + sage: from mjo.eja.eja_algebra import \ + ....: ComplexMatrixEuclideanJordanAlgebra + + EXAMPLES:: + sage: F = QuadraticField(-1, 'i') + sage: x1 = F(4 - 2*i) + sage: x2 = F(1 + 2*i) + sage: x3 = F(-i) + sage: x4 = F(6) + sage: M = matrix(F,2,[[x1,x2],[x3,x4]]) + sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M) + [ 4 -2| 1 2] + [ 2 4|-2 1] + [-----+-----] + [ 0 -1| 6 0] + [ 1 0| 0 6] + + TESTS: + + Embedding is a homomorphism (isomorphism, in fact):: + + sage: set_random_seed() + sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size() + sage: n = ZZ.random_element(n_max) + sage: F = QuadraticField(-1, 'i') + sage: X = random_matrix(F, n) + sage: Y = random_matrix(F, n) + sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X) + sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y) + sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y) + sage: Xe*Ye == XYe + True + + """ + n = M.nrows() + if M.ncols() != n: + raise ValueError("the matrix 'M' must be square") + field = M.base_ring() + blocks = [] + for z in M.list(): + a = z.vector()[0] # real part, I guess + b = z.vector()[1] # imag part, I guess + blocks.append(matrix(field, 2, [[a,b],[-b,a]])) + + # We can drop the imaginaries here. + return matrix.block(field.base_ring(), n, blocks) + + + @staticmethod + def real_unembed(M): + """ + The inverse of _embed_complex_matrix(). -class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra): + SETUP:: + + sage: from mjo.eja.eja_algebra import \ + ....: ComplexMatrixEuclideanJordanAlgebra + + EXAMPLES:: + + sage: A = matrix(QQ,[ [ 1, 2, 3, 4], + ....: [-2, 1, -4, 3], + ....: [ 9, 10, 11, 12], + ....: [-10, 9, -12, 11] ]) + sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A) + [ 2*i + 1 4*i + 3] + [ 10*i + 9 12*i + 11] + + TESTS: + + Unembedding is the inverse of embedding:: + + sage: set_random_seed() + sage: F = QuadraticField(-1, 'i') + sage: M = random_matrix(F, 3) + sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M) + sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M + True + + """ + n = ZZ(M.nrows()) + if M.ncols() != n: + raise ValueError("the matrix 'M' must be square") + if not n.mod(2).is_zero(): + raise ValueError("the matrix 'M' must be a complex embedding") + + field = M.base_ring() # This should already have sqrt2 + R = PolynomialRing(field, 'z') + z = R.gen() + F = NumberField(z**2 + 1,'i', embedding=CLF(-1).sqrt()) + i = F.gen() + + # Go top-left to bottom-right (reading order), converting every + # 2-by-2 block we see to a single complex element. + elements = [] + for k in xrange(n/2): + for j in xrange(n/2): + submat = M[2*k:2*k+2,2*j:2*j+2] + if submat[0,0] != submat[1,1]: + raise ValueError('bad on-diagonal submatrix') + if submat[0,1] != -submat[1,0]: + raise ValueError('bad off-diagonal submatrix') + z = submat[0,0] + submat[0,1]*i + elements.append(z) + + return matrix(F, n/2, elements) + + + @classmethod + def natural_inner_product(cls,X,Y): + """ + Compute a natural inner product in this algebra directly from + its real embedding. + + SETUP:: + + sage: from mjo.eja.eja_algebra import ComplexHermitianEJA + + TESTS: + + This gives the same answer as the slow, default method implemented + in :class:`MatrixEuclideanJordanAlgebra`:: + + sage: set_random_seed() + sage: J = ComplexHermitianEJA.random_instance() + sage: x,y = J.random_elements(2) + sage: Xe = x.natural_representation() + sage: Ye = y.natural_representation() + sage: X = ComplexHermitianEJA.real_unembed(Xe) + sage: Y = ComplexHermitianEJA.real_unembed(Ye) + sage: expected = (X*Y).trace().vector()[0] + sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye) + sage: actual == expected + True + + """ + return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2 + + +class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra, KnownRankEJA): """ The rank-n simple EJA consisting of complex Hermitian n-by-n matrices over the real numbers, the usual symmetric Jordan product, @@ -1444,8 +1335,7 @@ class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: set_random_seed() sage: J = ComplexHermitianEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() + sage: x,y = J.random_elements(2) sage: actual = (x*y).natural_representation() sage: X = x.natural_representation() sage: Y = y.natural_representation() @@ -1460,16 +1350,6 @@ class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: ComplexHermitianEJA(2, prefix='z').gens() (z0, z1, z2, z3) - Our inner product satisfies the Jordan axiom:: - - sage: set_random_seed() - sage: J = ComplexHermitianEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() - sage: z = J.random_element() - sage: (x*y).inner_product(z) == y.inner_product(x*z) - True - Our natural basis is normalized with respect to the natural inner product unless we specify otherwise:: @@ -1490,47 +1370,231 @@ class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra): True """ - def __init__(self, n, field=QQ, normalize_basis=True, **kwargs): - S = _complex_hermitian_basis(n, field) - if n > 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. - R = PolynomialRing(field, 'z') - z = R.gen() - p = z**2 - 2 - if p.is_irreducible(): - field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt()) - S = [ s.change_ring(field) for s in S ] - self._basis_normalizers = tuple( - ~(self.natural_inner_product(s,s).sqrt()) for s in S ) - S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) ) + @classmethod + def _denormalized_basis(cls, n, field): + """ + Returns a basis for the space of complex Hermitian n-by-n matrices. - Qs = _multiplication_table_from_matrix_basis(S) + Why do we embed these? Basically, because all of numerical linear + algebra assumes that you're working with vectors consisting of `n` + entries from a field and scalars from the same field. There's no way + to tell SageMath that (for example) the vectors contain complex + numbers, while the scalar field is real. + + SETUP:: + + sage: from mjo.eja.eja_algebra import ComplexHermitianEJA + + TESTS:: + + sage: set_random_seed() + sage: n = ZZ.random_element(1,5) + sage: field = QuadraticField(2, 'sqrt2') + sage: B = ComplexHermitianEJA._denormalized_basis(n, field) + sage: all( M.is_symmetric() for M in B) + True + + """ + R = PolynomialRing(field, 'z') + z = R.gen() + F = NumberField(z**2 + 1, 'I', embedding=CLF(-1).sqrt()) + I = F.gen() + + # This is like the symmetric case, but we need to be careful: + # + # * We want conjugate-symmetry, not just symmetry. + # * The diagonal will (as a result) be real. + # + S = [] + for i in xrange(n): + for j in xrange(i+1): + Eij = matrix(F, n, lambda k,l: k==i and l==j) + if i == j: + Sij = cls.real_embed(Eij) + S.append(Sij) + else: + # The second one has a minus because it's conjugated. + Sij_real = cls.real_embed(Eij + Eij.transpose()) + S.append(Sij_real) + Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose()) + S.append(Sij_imag) + + # Since we embedded these, we can drop back to the "field" that we + # started with instead of the complex extension "F". + return ( s.change_ring(field) for s in S ) - fdeja = super(ComplexHermitianEJA, self) - return fdeja.__init__(field, - Qs, - rank=n, - natural_basis=S, - **kwargs) + + def __init__(self, n, field=QQ, **kwargs): + basis = self._denormalized_basis(n,field) + super(ComplexHermitianEJA,self).__init__(field, basis, n, **kwargs) +class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): @staticmethod - def _max_test_case_size(): - return 4 + def real_embed(M): + """ + Embed the n-by-n quaternion matrix ``M`` into the space of real + matrices of size 4n-by-4n by first sending each quaternion entry `z + = a + bi + cj + dk` to the block-complex matrix ``[[a + bi, + c+di],[-c + di, a-bi]]`, and then embedding those into a real + matrix. + + SETUP:: + + sage: from mjo.eja.eja_algebra import \ + ....: QuaternionMatrixEuclideanJordanAlgebra + + EXAMPLES:: + + sage: Q = QuaternionAlgebra(QQ,-1,-1) + sage: i,j,k = Q.gens() + sage: x = 1 + 2*i + 3*j + 4*k + sage: M = matrix(Q, 1, [[x]]) + sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M) + [ 1 2 3 4] + [-2 1 -4 3] + [-3 4 1 -2] + [-4 -3 2 1] + + Embedding is a homomorphism (isomorphism, in fact):: + + sage: set_random_seed() + sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size() + sage: n = ZZ.random_element(n_max) + sage: Q = QuaternionAlgebra(QQ,-1,-1) + sage: X = random_matrix(Q, n) + sage: Y = random_matrix(Q, n) + sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X) + sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y) + sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y) + sage: Xe*Ye == XYe + True + + """ + quaternions = M.base_ring() + n = M.nrows() + if M.ncols() != n: + raise ValueError("the matrix 'M' must be square") + + F = QuadraticField(-1, 'i') + i = F.gen() + + blocks = [] + for z in M.list(): + t = z.coefficient_tuple() + a = t[0] + b = t[1] + c = t[2] + d = t[3] + cplxM = matrix(F, 2, [[ a + b*i, c + d*i], + [-c + d*i, a - b*i]]) + realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM) + blocks.append(realM) + + # We should have real entries by now, so use the realest field + # we've got for the return value. + return matrix.block(quaternions.base_ring(), n, blocks) + + @staticmethod - def natural_inner_product(X,Y): - Xu = _unembed_complex_matrix(X) - Yu = _unembed_complex_matrix(Y) - # The trace need not be real; consider Xu = (i*I) and Yu = I. - return ((Xu*Yu).trace()).vector()[0] # real part, I guess + def real_unembed(M): + """ + The inverse of _embed_quaternion_matrix(). + + SETUP:: + + sage: from mjo.eja.eja_algebra import \ + ....: QuaternionMatrixEuclideanJordanAlgebra + EXAMPLES:: + + sage: M = matrix(QQ, [[ 1, 2, 3, 4], + ....: [-2, 1, -4, 3], + ....: [-3, 4, 1, -2], + ....: [-4, -3, 2, 1]]) + sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M) + [1 + 2*i + 3*j + 4*k] + + TESTS: + Unembedding is the inverse of embedding:: + + sage: set_random_seed() + sage: Q = QuaternionAlgebra(QQ, -1, -1) + sage: M = random_matrix(Q, 3) + sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M) + sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M + True + + """ + n = ZZ(M.nrows()) + if M.ncols() != n: + raise ValueError("the matrix 'M' must be square") + if not n.mod(4).is_zero(): + raise ValueError("the matrix 'M' must be a complex embedding") + + # Use the base ring of the matrix to ensure that its entries can be + # multiplied by elements of the quaternion algebra. + field = M.base_ring() + Q = QuaternionAlgebra(field,-1,-1) + i,j,k = Q.gens() + + # Go top-left to bottom-right (reading order), converting every + # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1 + # quaternion block. + elements = [] + for l in xrange(n/4): + for m in xrange(n/4): + submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed( + M[4*l:4*l+4,4*m:4*m+4] ) + if submat[0,0] != submat[1,1].conjugate(): + raise ValueError('bad on-diagonal submatrix') + if submat[0,1] != -submat[1,0].conjugate(): + raise ValueError('bad off-diagonal submatrix') + z = submat[0,0].vector()[0] # real part + z += submat[0,0].vector()[1]*i # imag part + z += submat[0,1].vector()[0]*j # real part + z += submat[0,1].vector()[1]*k # imag part + elements.append(z) + + return matrix(Q, n/4, elements) + + + @classmethod + def natural_inner_product(cls,X,Y): + """ + Compute a natural inner product in this algebra directly from + its real embedding. -class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra): + SETUP:: + + sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA + + TESTS: + + This gives the same answer as the slow, default method implemented + in :class:`MatrixEuclideanJordanAlgebra`:: + + sage: set_random_seed() + sage: J = QuaternionHermitianEJA.random_instance() + sage: x,y = J.random_elements(2) + sage: Xe = x.natural_representation() + sage: Ye = y.natural_representation() + sage: X = QuaternionHermitianEJA.real_unembed(Xe) + sage: Y = QuaternionHermitianEJA.real_unembed(Ye) + sage: expected = (X*Y).trace().coefficient_tuple()[0] + sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye) + sage: actual == expected + True + + """ + return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4 + + +class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra, + KnownRankEJA): """ The rank-n simple EJA consisting of self-adjoint n-by-n quaternion matrices, the usual symmetric Jordan product, and the @@ -1556,8 +1620,7 @@ class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: set_random_seed() sage: J = QuaternionHermitianEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() + sage: x,y = J.random_elements(2) sage: actual = (x*y).natural_representation() sage: X = x.natural_representation() sage: Y = y.natural_representation() @@ -1572,16 +1635,6 @@ class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: QuaternionHermitianEJA(2, prefix='a').gens() (a0, a1, a2, a3, a4, a5) - Our inner product satisfies the Jordan axiom:: - - sage: set_random_seed() - sage: J = QuaternionHermitianEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() - sage: z = J.random_element() - sage: (x*y).inner_product(z) == y.inner_product(x*z) - True - Our natural basis is normalized with respect to the natural inner product unless we specify otherwise:: @@ -1602,49 +1655,65 @@ class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra): True """ - def __init__(self, n, field=QQ, normalize_basis=True, **kwargs): - S = _quaternion_hermitian_basis(n, field) + @classmethod + def _denormalized_basis(cls, n, field): + """ + Returns a basis for the space of quaternion Hermitian n-by-n matrices. - if n > 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. - R = PolynomialRing(field, 'z') - z = R.gen() - p = z**2 - 2 - if p.is_irreducible(): - field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt()) - S = [ s.change_ring(field) for s in S ] - self._basis_normalizers = tuple( - ~(self.natural_inner_product(s,s).sqrt()) for s in S ) - S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) ) + Why do we embed these? Basically, because all of numerical + linear algebra assumes that you're working with vectors consisting + of `n` entries from a field and scalars from the same field. There's + no way to tell SageMath that (for example) the vectors contain + complex numbers, while the scalar field is real. - Qs = _multiplication_table_from_matrix_basis(S) + SETUP:: - fdeja = super(QuaternionHermitianEJA, self) - return fdeja.__init__(field, - Qs, - rank=n, - natural_basis=S, - **kwargs) + sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA - @staticmethod - def _max_test_case_size(): - return 3 + TESTS:: - @staticmethod - def natural_inner_product(X,Y): - Xu = _unembed_quaternion_matrix(X) - Yu = _unembed_quaternion_matrix(Y) - # The trace need not be real; consider Xu = (i*I) and Yu = I. - # The result will be a quaternion algebra element, which doesn't - # have a "vector" method, but does have coefficient_tuple() method - # that returns the coefficients of 1, i, j, and k -- in that order. - return ((Xu*Yu).trace()).coefficient_tuple()[0] + sage: set_random_seed() + sage: n = ZZ.random_element(1,5) + sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ) + sage: all( M.is_symmetric() for M in B ) + True + """ + Q = QuaternionAlgebra(QQ,-1,-1) + I,J,K = Q.gens() + + # This is like the symmetric case, but we need to be careful: + # + # * We want conjugate-symmetry, not just symmetry. + # * The diagonal will (as a result) be real. + # + S = [] + for i in xrange(n): + for j in xrange(i+1): + Eij = matrix(Q, n, lambda k,l: k==i and l==j) + if i == j: + Sij = cls.real_embed(Eij) + S.append(Sij) + else: + # The second, third, and fourth ones have a minus + # because they're conjugated. + Sij_real = cls.real_embed(Eij + Eij.transpose()) + S.append(Sij_real) + Sij_I = cls.real_embed(I*Eij - I*Eij.transpose()) + S.append(Sij_I) + Sij_J = cls.real_embed(J*Eij - J*Eij.transpose()) + S.append(Sij_J) + Sij_K = cls.real_embed(K*Eij - K*Eij.transpose()) + S.append(Sij_K) + return S + + + def __init__(self, n, field=QQ, **kwargs): + basis = self._denormalized_basis(n,field) + super(QuaternionHermitianEJA,self).__init__(field, basis, n, **kwargs) -class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra): +class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA): """ The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)`` with the usual inner product and jordan product ``x*y = @@ -1681,22 +1750,12 @@ class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: JordanSpinEJA(2, prefix='B').gens() (B0, B1) - Our inner product satisfies the Jordan axiom:: - - sage: set_random_seed() - sage: J = JordanSpinEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() - sage: z = J.random_element() - sage: (x*y).inner_product(z) == y.inner_product(x*z) - True - """ def __init__(self, n, field=QQ, **kwargs): V = VectorSpace(field, n) - mult_table = [[V.zero() for j in range(n)] for i in range(n)] - for i in range(n): - for j in range(n): + mult_table = [[V.zero() for j in xrange(n)] for i in xrange(n)] + for i in xrange(n): + for j in xrange(n): x = V.gen(i) y = V.gen(j) x0 = x[0] @@ -1730,8 +1789,7 @@ class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra): sage: set_random_seed() sage: J = JordanSpinEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() + sage: x,y = J.random_elements(2) sage: X = x.natural_representation() sage: Y = y.natural_representation() sage: x.inner_product(y) == J.natural_inner_product(X,Y)