X-Git-Url: http://gitweb.michael.orlitzky.com/?a=blobdiff_plain;f=mjo%2Feja%2Feja_algebra.py;h=689a3db016437d1e6eda5c6372e52a3513896671;hb=3a476bd1ea5aef3ecd375e71d50342f1441fd35d;hp=a207250b4092f97e07d93a62c0ed7e23d9f9536d;hpb=7996413ef05ab2275c2cbb86494e30241904914b;p=sage.d.git diff --git a/mjo/eja/eja_algebra.py b/mjo/eja/eja_algebra.py index a207250..689a3db 100644 --- a/mjo/eja/eja_algebra.py +++ b/mjo/eja/eja_algebra.py @@ -5,33 +5,51 @@ are used in optimization, and have some additional nice methods beyond what can be supported in a general Jordan Algebra. """ +from itertools import repeat + from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra from sage.categories.magmatic_algebras import MagmaticAlgebras from sage.combinat.free_module import CombinatorialFreeModule from sage.matrix.constructor import matrix from sage.matrix.matrix_space import MatrixSpace from sage.misc.cachefunc import cached_method +from sage.misc.lazy_import import lazy_import from sage.misc.prandom import choice from sage.misc.table import table from sage.modules.free_module import FreeModule, VectorSpace -from sage.rings.integer_ring import ZZ -from sage.rings.number_field.number_field import NumberField, QuadraticField -from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing -from sage.rings.rational_field import QQ -from sage.rings.real_lazy import CLF, RLF -from sage.structure.element import is_Matrix - +from sage.rings.all import (ZZ, QQ, RR, RLF, CLF, + PolynomialRing, + QuadraticField) from mjo.eja.eja_element import FiniteDimensionalEuclideanJordanAlgebraElement +lazy_import('mjo.eja.eja_subalgebra', + 'FiniteDimensionalEuclideanJordanSubalgebra') from mjo.eja.eja_utils import _mat2vec class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): - # This is an ugly hack needed to prevent the category framework - # from implementing a coercion from our base ring (e.g. the - # rationals) into the algebra. First of all -- such a coercion is - # nonsense to begin with. But more importantly, it tries to do so - # in the category of rings, and since our algebras aren't - # associative they generally won't be rings. - _no_generic_basering_coercion = True + + def _coerce_map_from_base_ring(self): + """ + Disable the map from the base ring into the algebra. + + Performing a nonsense conversion like this automatically + is counterpedagogical. The fallback is to try the usual + element constructor, which should also fail. + + SETUP:: + + sage: from mjo.eja.eja_algebra import random_eja + + TESTS:: + + sage: set_random_seed() + sage: J = random_eja() + sage: J(1) + Traceback (most recent call last): + ... + ValueError: not a naturally-represented algebra element + + """ + return None def __init__(self, field, @@ -39,11 +57,12 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): rank, prefix='e', category=None, - natural_basis=None): + natural_basis=None, + check=True): """ SETUP:: - sage: from mjo.eja.eja_algebra import random_eja + sage: from mjo.eja.eja_algebra import (JordanSpinEJA, random_eja) EXAMPLES: @@ -51,18 +70,30 @@ 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 + TESTS: + + The ``field`` we're given must be real:: + + sage: JordanSpinEJA(2,QQbar) + Traceback (most recent call last): + ... + ValueError: field is not real + """ + if check: + if not field.is_subring(RR): + # Note: this does return true for the real algebraic + # field, and any quadratic field where we've specified + # a real embedding. + raise ValueError('field is not real') + 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() @@ -80,8 +111,10 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): # long run to have the multiplication table be in terms of # algebra elements. We do this after calling the superclass # constructor so that from_vector() knows what to do. - self._multiplication_table = [ map(lambda x: self.from_vector(x), ls) - for ls in mult_table ] + self._multiplication_table = [ + list(map(lambda x: self.from_vector(x), ls)) + for ls in mult_table + ] def _element_constructor_(self, elt): @@ -95,7 +128,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): SETUP:: sage: from mjo.eja.eja_algebra import (JordanSpinEJA, - ....: RealCartesianProductEJA, + ....: HadamardEJA, ....: RealSymmetricEJA) EXAMPLES: @@ -123,7 +156,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): vector representations) back and forth faithfully:: sage: set_random_seed() - sage: J = RealCartesianProductEJA.random_instance() + sage: J = HadamardEJA.random_instance() sage: x = J.random_element() sage: J(x.to_vector().column()) == x True @@ -133,15 +166,22 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): True """ + msg = "not a naturally-represented algebra element" if elt == 0: # The superclass implementation of random_element() # needs to be able to coerce "0" into the algebra. return self.zero() + elif elt in self.base_ring(): + # Ensure that no base ring -> algebra coercion is performed + # by this method. There's some stupidity in sage that would + # otherwise propagate to this method; for example, sage thinks + # that the integer 3 belongs to the space of 2-by-2 matrices. + raise ValueError(msg) natural_basis = self.natural_basis() basis_space = natural_basis[0].matrix_space() if elt not in basis_space: - raise ValueError("not a naturally-represented algebra element") + raise ValueError(msg) # Thanks for nothing! Matrix spaces aren't vector spaces in # Sage, so we have to figure out its natural-basis coordinates @@ -155,26 +195,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,22 +278,12 @@ 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(): + + if i == self.rank(): + return R.one() + if i > self.rank(): # Guaranteed by theory return R.zero() @@ -382,7 +392,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): SETUP:: - sage: from mjo.eja.eja_algebra import JordanSpinEJA + sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA EXAMPLES: @@ -396,12 +406,22 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): sage: p(*xvec) t^2 - 2*t + 1 + By definition, the characteristic polynomial is a monic + degree-zero polynomial in a rank-zero algebra. Note that + Cayley-Hamilton is indeed satisfied since the polynomial + ``1`` evaluates to the identity element of the algebra on + any argument:: + + sage: J = TrivialEJA() + sage: J.characteristic_polynomial() + 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) ] + # The list of coefficient polynomials a_0, a_1, a_2, ..., a_n. + a = [ self._charpoly_coeff(i) for i in range(r+1) ] # We go to a bit of trouble here to reorder the # indeterminates, so that it's easier to evaluate the @@ -413,17 +433,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): 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 - return sum( a[k]*(t**k) for k in range(len(a)) ) @@ -441,14 +450,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 @@ -466,15 +473,19 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): SETUP:: - sage: from mjo.eja.eja_algebra import ComplexHermitianEJA + sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA, + ....: TrivialEJA) EXAMPLES:: sage: J = ComplexHermitianEJA(3) sage: J.is_trivial() False - sage: A = J.zero().subalgebra_generated_by() - sage: A.is_trivial() + + :: + + sage: J = TrivialEJA() + sage: J.is_trivial() True """ @@ -595,12 +606,12 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): SETUP:: - sage: from mjo.eja.eja_algebra import (RealCartesianProductEJA, + sage: from mjo.eja.eja_algebra import (HadamardEJA, ....: random_eja) EXAMPLES:: - sage: J = RealCartesianProductEJA(5) + sage: J = HadamardEJA(5) sage: J.one() e0 + e1 + e2 + e3 + e4 @@ -649,35 +660,138 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule): return self.linear_combination(zip(self.gens(), coeffs)) - def random_element(self): - # Temporary workaround for https://trac.sagemath.org/ticket/28327 - if self.is_trivial(): - return self.zero() - else: - s = super(FiniteDimensionalEuclideanJordanAlgebra, self) - return s.random_element() + def peirce_decomposition(self, c): + """ + The Peirce decomposition of this algebra relative to the + idempotent ``c``. + In the future, this can be extended to a complete system of + orthogonal idempotents. + + INPUT: + + - ``c`` -- an idempotent of this algebra. + + OUTPUT: + + A triple (J0, J5, J1) containing two subalgebras and one subspace + of this algebra, + + - ``J0`` -- the algebra on the eigenspace of ``c.operator()`` + corresponding to the eigenvalue zero. + + - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()`` + corresponding to the eigenvalue one-half. + + - ``J1`` -- the algebra on the eigenspace of ``c.operator()`` + corresponding to the eigenvalue one. + + These are the only possible eigenspaces for that operator, and this + algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are + orthogonal, and are subalgebras of this algebra with the appropriate + restrictions. + + SETUP:: + + sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA + + EXAMPLES: + + The canonical example comes from the symmetric matrices, which + decompose into diagonal and off-diagonal parts:: + + sage: J = RealSymmetricEJA(3) + sage: C = matrix(QQ, [ [1,0,0], + ....: [0,1,0], + ....: [0,0,0] ]) + sage: c = J(C) + sage: J0,J5,J1 = J.peirce_decomposition(c) + sage: J0 + Euclidean Jordan algebra of dimension 1... + sage: J5 + Vector space of degree 6 and dimension 2... + sage: J1 + Euclidean Jordan algebra of dimension 3... + + TESTS: + + Every algebra decomposes trivially with respect to its identity + element:: + + sage: set_random_seed() + sage: J = random_eja() + sage: J0,J5,J1 = J.peirce_decomposition(J.one()) + sage: J0.dimension() == 0 and J5.dimension() == 0 + True + sage: J1.superalgebra() == J and J1.dimension() == J.dimension() + True + + The identity elements in the two subalgebras are the + projections onto their respective subspaces of the + superalgebra's identity element:: + + sage: set_random_seed() + sage: J = random_eja() + sage: x = J.random_element() + sage: if not J.is_trivial(): + ....: while x.is_nilpotent(): + ....: x = J.random_element() + sage: c = x.subalgebra_idempotent() + sage: J0,J5,J1 = J.peirce_decomposition(c) + sage: J1(c) == J1.one() + True + sage: J0(J.one() - c) == J0.one() + True - @classmethod - def random_instance(cls, field=QQ, **kwargs): """ - Return a random instance of this type of algebra. + if not c.is_idempotent(): + raise ValueError("element is not idempotent: %s" % c) + + # Default these to what they should be if they turn out to be + # trivial, because eigenspaces_left() won't return eigenvalues + # corresponding to trivial spaces (e.g. it returns only the + # eigenspace corresponding to lambda=1 if you take the + # decomposition relative to the identity element). + trivial = FiniteDimensionalEuclideanJordanSubalgebra(self, ()) + J0 = trivial # eigenvalue zero + J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half + J1 = trivial # eigenvalue one + + for (eigval, eigspace) in c.operator().matrix().left_eigenspaces(): + if eigval == ~(self.base_ring()(2)): + J5 = eigspace + else: + gens = tuple( self.from_vector(b) for b in eigspace.basis() ) + subalg = FiniteDimensionalEuclideanJordanSubalgebra(self, gens) + if eigval == 0: + J0 = subalg + elif eigval == 1: + J1 = subalg + else: + raise ValueError("unexpected eigenvalue: %s" % eigval) - In subclasses for algebras that we know how to construct, this - is a shortcut for constructing test cases and examples. + return (J0, J5, J1) + + + def random_elements(self, count): """ - 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 + Return ``count`` random elements as a tuple. - n = ZZ.random_element(1, cls._max_test_case_size()) - return cls(n, field, **kwargs) + SETUP:: + + 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 range(count) ) def rank(self): @@ -713,27 +827,25 @@ 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: Ensure that every EJA that we know how to construct has a - positive integer rank:: + positive integer rank, unless the algebra is trivial in + which case its rank will be zero:: sage: set_random_seed() - sage: r = random_eja().rank() - sage: r in ZZ and r > 0 + sage: J = random_eja() + sage: r = J.rank() + sage: r in ZZ + True + sage: r > 0 or (r == 0 and J.is_trivial()) True """ @@ -761,7 +873,61 @@ 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. + """ + if cls is TrivialEJA: + # The TrivialEJA class doesn't take an "n" argument because + # there's only one. + return cls(field) + + n = ZZ.random_element(cls._max_test_case_size()) + 1 + return cls(n, field, **kwargs) + + +class HadamardEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA): """ Return the Euclidean Jordan Algebra corresponding to the set `R^n` under the Hadamard product. @@ -772,13 +938,13 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra): SETUP:: - sage: from mjo.eja.eja_algebra import RealCartesianProductEJA + sage: from mjo.eja.eja_algebra import HadamardEJA EXAMPLES: This multiplication table can be verified by hand:: - sage: J = RealCartesianProductEJA(3) + sage: J = HadamardEJA(3) sage: e0,e1,e2 = J.gens() sage: e0*e0 e0 @@ -797,26 +963,16 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra): We can change the generator prefix:: - sage: RealCartesianProductEJA(3, prefix='r').gens() + sage: HadamardEJA(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) ] - fdeja = super(RealCartesianProductEJA, self) + fdeja = super(HadamardEJA, self) return fdeja.__init__(field, mult_table, rank=n, **kwargs) def inner_product(self, x, y): @@ -825,7 +981,7 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra): SETUP:: - sage: from mjo.eja.eja_algebra import RealCartesianProductEJA + sage: from mjo.eja.eja_algebra import HadamardEJA TESTS: @@ -833,9 +989,8 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra): over `R^n`:: sage: set_random_seed() - sage: J = RealCartesianProductEJA.random_instance() - sage: x = J.random_element() - sage: y = J.random_element() + sage: J = HadamardEJA.random_instance() + 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) @@ -845,32 +1000,10 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra): return x.to_vector().inner_product(y.to_vector()) -def random_eja(): +def random_eja(field=QQ, nontrivial=False): """ Return a "random" finite-dimensional Euclidean Jordan Algebra. - ALGORITHM: - - For now, we choose a random natural number ``n`` (greater than zero) - and then give you back one of the following: - - * The cartesian product of the rational numbers ``n`` times; this is - ``QQ^n`` with the Hadamard product. - - * The Jordan spin algebra on ``QQ^n``. - - * The ``n``-by-``n`` rational symmetric matrices with the symmetric - product. - - * The ``n``-by-``n`` complex-rational Hermitian matrices embedded - in the space of ``2n``-by-``2n`` real symmetric matrices. - - * The ``n``-by-``n`` quaternion-rational Hermitian matrices embedded - in the space of ``4n``-by-``4n`` real symmetric matrices. - - Later this might be extended to return Cartesian products of the - EJAs above. - SETUP:: sage: from mjo.eja.eja_algebra import random_eja @@ -881,12 +1014,11 @@ def random_eja(): Euclidean Jordan algebra of dimension... """ - classname = choice([RealCartesianProductEJA, - JordanSpinEJA, - RealSymmetricEJA, - ComplexHermitianEJA, - QuaternionHermitianEJA]) - return classname.random_instance() + eja_classes = KnownRankEJA.__subclasses__() + if nontrivial: + eja_classes.remove(TrivialEJA) + classname = choice(eja_classes) + return classname.random_instance(field=field) @@ -898,16 +1030,22 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra): def _max_test_case_size(): # Play it safe, since this will be squared and the underlying # field can have dimension 4 (quaternions) too. - return 3 + return 2 - @classmethod - def _denormalized_basis(cls, n, field): - raise NotImplementedError + 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 - def __init__(self, n, field=QQ, normalize_basis=True, **kwargs): - S = self._denormalized_basis(n, field) + # 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) - if n > 1 and normalize_basis: + 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. @@ -915,22 +1053,56 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra): 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 ] + field = field.extension(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 S ) - S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) ) + ~(self.natural_inner_product(s,s).sqrt()) for s in basis ) + basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers)) - Qs = self.multiplication_table_from_matrix_basis(S) + Qs = self.multiplication_table_from_matrix_basis(basis) fdeja = super(MatrixEuclideanJordanAlgebra, self) return fdeja.__init__(field, Qs, - rank=n, - natural_basis=S, + rank=rank, + natural_basis=basis, **kwargs) + @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 zip(self.natural_basis(), + self._basis_normalizers) ) + + # 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) + # 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 } + result = p.subs(substitutions) + + # The result of "subs" can be either a coefficient-ring + # element or a polynomial. Gotta handle both cases. + if result in QQ: + return self.base_ring()(result) + else: + return result.change_ring(self.base_ring()) + + @staticmethod def multiplication_table_from_matrix_basis(basis): """ @@ -991,6 +1163,7 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra): Xu = cls.real_unembed(X) Yu = cls.real_unembed(Y) tr = (Xu*Yu).trace() + if tr in RLF: # It's real already. return tr @@ -1006,7 +1179,25 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra): return tr.coefficient_tuple()[0] -class RealSymmetricEJA(MatrixEuclideanJordanAlgebra): +class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): + @staticmethod + def real_embed(M): + """ + The identity function, for embedding real matrices into real + matrices. + """ + return M + + @staticmethod + def real_unembed(M): + """ + The identity function, for unembedding real matrices from real + matrices. + """ + return M + + +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 @@ -1027,6 +1218,14 @@ class RealSymmetricEJA(MatrixEuclideanJordanAlgebra): sage: e2*e2 e2 + In theory, our "field" can be any subfield of the reals:: + + sage: RealSymmetricEJA(2, AA) + Euclidean Jordan algebra of dimension 3 over Algebraic Real Field + sage: RealSymmetricEJA(2, RR) + Euclidean Jordan algebra of dimension 3 over Real Field with + 53 bits of precision + TESTS: The dimension of this algebra is `(n^2 + n) / 2`:: @@ -1042,8 +1241,7 @@ class RealSymmetricEJA(MatrixEuclideanJordanAlgebra): 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() @@ -1058,16 +1256,6 @@ class RealSymmetricEJA(MatrixEuclideanJordanAlgebra): 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:: @@ -1109,45 +1297,25 @@ class RealSymmetricEJA(MatrixEuclideanJordanAlgebra): # 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): + for i in range(n): + for j in range(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) + return S @staticmethod def _max_test_case_size(): - return 5 # Dimension 10 + return 4 # Dimension 10 - @staticmethod - def real_embed(M): - """ - Embed the matrix ``M`` into a space of real matrices. - - 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. - - real_embed(M*N) = real_embed(M)*real_embed(N) - - """ - return M - - - @staticmethod - def real_unembed(M): - """ - The inverse of :meth:`real_embed`. - """ - return M + def __init__(self, n, field=QQ, **kwargs): + basis = self._denormalized_basis(n, field) + super(RealSymmetricEJA, self).__init__(field, basis, n, **kwargs) class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): @@ -1198,15 +1366,17 @@ class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): n = M.nrows() if M.ncols() != n: raise ValueError("the matrix 'M' must be square") - field = M.base_ring() + + # We don't need any adjoined elements... + field = M.base_ring().base_ring() + blocks = [] for z in M.list(): - a = z.vector()[0] # real part, I guess - b = z.vector()[1] # imag part, I guess + a = z.list()[0] # real part, I guess + b = z.list()[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) + return matrix.block(field, n, blocks) @staticmethod @@ -1247,17 +1417,19 @@ class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): 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 + # If "M" was normalized, its base ring might have roots + # adjoined and they can stick around after unembedding. + field = M.base_ring() R = PolynomialRing(field, 'z') z = R.gen() - F = NumberField(z**2 + 1,'i', embedding=CLF(-1).sqrt()) + F = field.extension(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): + for k in range(n/2): + for j in range(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') @@ -1269,7 +1441,38 @@ class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): return matrix(F, n/2, elements) -class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra): + @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, @@ -1280,6 +1483,16 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra): sage: from mjo.eja.eja_algebra import ComplexHermitianEJA + EXAMPLES: + + In theory, our "field" can be any subfield of the reals:: + + sage: ComplexHermitianEJA(2, AA) + Euclidean Jordan algebra of dimension 4 over Algebraic Real Field + sage: ComplexHermitianEJA(2, RR) + Euclidean Jordan algebra of dimension 4 over Real Field with + 53 bits of precision + TESTS: The dimension of this algebra is `n^2`:: @@ -1295,8 +1508,7 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra): 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() @@ -1311,16 +1523,6 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra): 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:: @@ -1341,6 +1543,7 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra): True """ + @classmethod def _denormalized_basis(cls, n, field): """ @@ -1368,7 +1571,7 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra): """ R = PolynomialRing(field, 'z') z = R.gen() - F = NumberField(z**2 + 1, 'I', embedding=CLF(-1).sqrt()) + F = field.extension(z**2 + 1, 'I') I = F.gen() # This is like the symmetric case, but we need to be careful: @@ -1377,8 +1580,8 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra): # * The diagonal will (as a result) be real. # S = [] - for i in xrange(n): - for j in xrange(i+1): + for i in range(n): + for j in range(i+1): Eij = matrix(F, n, lambda k,l: k==i and l==j) if i == j: Sij = cls.real_embed(Eij) @@ -1392,9 +1595,13 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra): # 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 ) + return ( s.change_ring(field) for s in S ) + 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 @@ -1499,7 +1706,7 @@ class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): 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") + raise ValueError("the matrix 'M' must be a quaternion embedding") # Use the base ring of the matrix to ensure that its entries can be # multiplied by elements of the quaternion algebra. @@ -1511,8 +1718,8 @@ class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): # 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): + for l in range(n/4): + for m in range(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(): @@ -1528,8 +1735,39 @@ class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra): 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. + + 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): +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 @@ -1540,6 +1778,16 @@ class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra): sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA + EXAMPLES: + + In theory, our "field" can be any subfield of the reals:: + + sage: QuaternionHermitianEJA(2, AA) + Euclidean Jordan algebra of dimension 6 over Algebraic Real Field + sage: QuaternionHermitianEJA(2, RR) + Euclidean Jordan algebra of dimension 6 over Real Field with + 53 bits of precision + TESTS: The dimension of this algebra is `2*n^2 - n`:: @@ -1555,8 +1803,7 @@ class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra): 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() @@ -1571,16 +1818,6 @@ class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra): 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:: @@ -1634,8 +1871,8 @@ class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra): # * The diagonal will (as a result) be real. # S = [] - for i in xrange(n): - for j in xrange(i+1): + for i in range(n): + for j in range(i+1): Eij = matrix(Q, n, lambda k,l: k==i and l==j) if i == j: Sij = cls.real_embed(Eij) @@ -1651,15 +1888,142 @@ class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra): S.append(Sij_J) Sij_K = cls.real_embed(K*Eij - K*Eij.transpose()) S.append(Sij_K) - return tuple(S) + # Since we embedded these, we can drop back to the "field" that we + # started with instead of the quaternion algebra "Q". + return ( s.change_ring(field) for s in S ) + + + def __init__(self, n, field=QQ, **kwargs): + basis = self._denormalized_basis(n,field) + super(QuaternionHermitianEJA,self).__init__(field, basis, n, **kwargs) + + +class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA): + r""" + The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)`` + with the half-trace inner product and jordan product ``x*y = + (x0*y0 + , x0*y_bar + y0*x_bar)`` where ``B`` is a + symmetric positive-definite "bilinear form" matrix. It has + dimension `n` over the reals, and reduces to the ``JordanSpinEJA`` + when ``B`` is the identity matrix of order ``n-1``. + + SETUP:: + + sage: from mjo.eja.eja_algebra import (BilinearFormEJA, + ....: JordanSpinEJA) + + EXAMPLES: + + When no bilinear form is specified, the identity matrix is used, + and the resulting algebra is the Jordan spin algebra:: + + sage: J0 = BilinearFormEJA(3) + sage: J1 = JordanSpinEJA(3) + sage: J0.multiplication_table() == J0.multiplication_table() + True + + TESTS: + + We can create a zero-dimensional algebra:: + + sage: J = BilinearFormEJA(0) + sage: J.basis() + Finite family {} + + We can check the multiplication condition given in the Jordan, von + Neumann, and Wigner paper (and also discussed on my "On the + symmetry..." paper). Note that this relies heavily on the standard + choice of basis, as does anything utilizing the bilinear form matrix:: + + sage: set_random_seed() + sage: n = ZZ.random_element(5) + sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular') + sage: B = M.transpose()*M + sage: J = BilinearFormEJA(n, B=B) + sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis() + sage: V = J.vector_space() + sage: sis = [ J.from_vector(V([0] + (M.inverse()*ei).list())) + ....: for ei in eis ] + sage: actual = [ sis[i]*sis[j] + ....: for i in range(n-1) + ....: for j in range(n-1) ] + sage: expected = [ J.one() if i == j else J.zero() + ....: for i in range(n-1) + ....: for j in range(n-1) ] + sage: actual == expected + True + """ + def __init__(self, n, field=QQ, B=None, **kwargs): + if B is None: + self._B = matrix.identity(field, max(0,n-1)) + else: + self._B = B + + 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): + x = V.gen(i) + y = V.gen(j) + x0 = x[0] + xbar = x[1:] + y0 = y[0] + ybar = y[1:] + z0 = x0*y0 + (self._B*xbar).inner_product(ybar) + zbar = y0*xbar + x0*ybar + z = V([z0] + zbar.list()) + mult_table[i][j] = z + + # The rank of this algebra is two, unless we're in a + # 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) + + def inner_product(self, x, y): + r""" + Half of the trace inner product. + + This is defined so that the special case of the Jordan spin + algebra gets the usual inner product. + + SETUP:: + sage: from mjo.eja.eja_algebra import BilinearFormEJA -class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra): + TESTS: + + Ensure that this is one-half of the trace inner-product:: + + sage: set_random_seed() + sage: n = ZZ.random_element(5) + sage: M = matrix.random(QQ, n-1, algorithm='unimodular') + sage: B = M.transpose()*M + sage: J = BilinearFormEJA(n, B=B) + sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis() + sage: V = J.vector_space() + sage: sis = [ J.from_vector(V([0] + (M.inverse()*ei).list())) + ....: for ei in eis ] + sage: actual = [ sis[i]*sis[j] + ....: for i in range(n-1) + ....: for j in range(n-1) ] + sage: expected = [ J.one() if i == j else J.zero() + ....: for i in range(n-1) + ....: for j in range(n-1) ] + + """ + xvec = x.to_vector() + xbar = xvec[1:] + yvec = y.to_vector() + ybar = yvec[1:] + return x[0]*y[0] + (self._B*xbar).inner_product(ybar) + +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 = - (, x0*y_bar + y0*x_bar)``. It has dimension `n` over + (, x0*y_bar + y0*x_bar)``. It has dimension `n` over the reals. SETUP:: @@ -1692,16 +2056,6 @@ 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) @@ -1741,8 +2095,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) @@ -1750,3 +2103,40 @@ class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra): """ return x.to_vector().inner_product(y.to_vector()) + + +class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA): + """ + The trivial Euclidean Jordan algebra consisting of only a zero element. + + SETUP:: + + sage: from mjo.eja.eja_algebra import TrivialEJA + + EXAMPLES:: + + sage: J = TrivialEJA() + sage: J.dimension() + 0 + sage: J.zero() + 0 + sage: J.one() + 0 + sage: 7*J.one()*12*J.one() + 0 + sage: J.one().inner_product(J.one()) + 0 + sage: J.one().norm() + 0 + sage: J.one().subalgebra_generated_by() + Euclidean Jordan algebra of dimension 0 over Rational Field + sage: J.rank() + 0 + + """ + def __init__(self, field=QQ, **kwargs): + mult_table = [] + 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)