From: Michael Orlitzky Date: Sun, 22 Feb 2026 16:42:50 +0000 (-0500) Subject: mjo/clan: factor out mjo.clan.vinberg_clan X-Git-Url: https://gitweb.michael.orlitzky.com/?a=commitdiff_plain;h=8485f51f413b6b96fb7cfdbcf4be808f2a933ace;p=sage.d.git mjo/clan: factor out mjo.clan.vinberg_clan --- diff --git a/mjo/clan/all.py b/mjo/clan/all.py index 3467745..f98232a 100644 --- a/mjo/clan/all.py +++ b/mjo/clan/all.py @@ -4,6 +4,7 @@ All user-facing imports from mjo.clan. from mjo.clan.clan import ( Clans, ComplexHermitianClan, - RealSymmetricClan, - VinbergClan + RealSymmetricClan ) + +from mjo.clan.vinberg_clan import VinbergClan diff --git a/mjo/clan/clan.py b/mjo/clan/clan.py index 4838360..38dd169 100644 --- a/mjo/clan/clan.py +++ b/mjo/clan/clan.py @@ -636,426 +636,3 @@ class ComplexHermitianClan(MatrixClan): """ return f"Clan H^{self.rank()} over {self.base_ring()}" - - -class VinbergClan(NormalDecomposition): - r""" - The clan corresponding to the Vinberg cone (as defined by Ishi). - - The Vinberg cone lives in a space whose points are pairs of real - 2x2 symmetric matrices that agree in the first coordinate. The - cone itself is the subset where both elements of the pair are - positive-semidefinite. - - Ishi describes this space as x = (a,b) where - - a = [ x11 x21 ] - [ x21 x22 ], - - b = [ x11 x31 ] - [ x31 x33 ] - - As there is no obvious way to express this in Sage, we instead - simply write out the x = (x11, x21, x22, x31, x33) as a vector of - real numbers, and provide lifts to the matrix representation. The - clan product is in terms of the up-hat and down-hat that is - defined on `S^{2}. - - Following Gikdikin (p. 91), the basis for this clan is - - e[(0,0,1)] = [1,0], [1,0] - [0,0] [0,0] - - e[(1,0,1)] = [0,1], [0,0] - [1,0] [0,0] - - e[(1,1,1)] = [0,0], [0,0] - [0,1] [0,0] - - e[(2,0,1)] = [0,0], [0,1] - [0,0] [1,0] - - e[(2,1,1)] = nonexistent, component is trivial - - e[(2,2,1)] = [0,0], [0,0] - [0,0] [0,1] - - SETUP:: - - sage: from mjo.clan.clan import VinbergClan - - EXAMPLES: - - Check the unit element:: - - sage: C = VinbergClan() - sage: x = C.an_element() - sage: x*C.one() == x - True - sage: C.one()*x == x - True - - Verifying the axioms:: - - sage: C = VinbergClan() - sage: e = C.basis() - sage: all( e[i,i,1]*e[i,i,1] == e[i,i,1] for i in range(3) ) - True - sage: all( e[i,i,1]*e[j,i,1] == e[j,i,1]/2 - ....: for i in range(3) - ....: for j in range(i+1,3) - ....: if e.has_key((j,i,1))) - True - sage: all( e[i,i,1]*e[i,k,1] == e[i,k,1]/2 - ....: for i in range(2) - ....: for k in range(i)) - True - sage: all( (e[i,i,1]*e[j,k,1]).is_zero() - ....: for i in range(2) - ....: for j in range(2) - ....: for k in range(j+1) - ....: if not i in [j,k] ) - True - sage: all( (e[i,k,1]*e[i,i,1]).is_zero() - ....: for i in range(2) - ....: for k in range(i) ) - True - sage: all( (e[j,k,1]*e[i,i,1]).is_zero() - ....: for i in range(2) - ....: for j in range(2) - ....: for k in range(j) - ....: if not i in [j,k] ) - True - - The multiplication in this clan (verified by hand) is as - follows):: - - sage: C = VinbergClan() - sage: x = C.random_element() - sage: y = C.random_element() - sage: z = x*y - sage: x = x.to_vector() - sage: y = y.to_vector() - sage: z = z.to_vector() - sage: z[0] == x[0]*y[0] - True - sage: z[1] == y[1]*(x[0] + x[2])/QQ(2) + y[0]*x[1] - True - sage: z[2] == 2*x[1]*y[1] + x[2]*y[2] - True - sage: z[3] == y[3]*(x[0] + x[4])/QQ(2) + y[0]*x[3] - True - sage: z[4] == 2*x[3]*y[3] + x[4]*y[4] - True - - The Ishi inner product in this clan (verified by hand) is as - follows:: - - sage: C = VinbergClan() - sage: x = C.random_element() - sage: y = C.random_element() - sage: actual = x.inner_product(y) - sage: x = x.to_vector() - sage: y = y.to_vector() - sage: expected = ( x[0]*y[0] + x[2]*y[2] + x[4]*y[4] + - ....: 2*x[1]*y[1] + 2*x[3]*y[3] ) / QQ(2) - sage: actual == expected - True - - """ - def _unlift(self, pair): - A,B = pair - R5 = self._vector_space - return ( A[(0,0,1)]*R5((0,0,1)) + - A[(1,0,1)]*R5((1,0,1)) + - A[(1,1,1)]*R5((1,1,1)) + - B[(1,0,1)]*R5((2,0,1)) + - B[(1,1,1)]*R5((2,2,1)) ) - - def _lift(self, v): - M1 = ( self._S2((0,0,1))*v[(0,0,1)] + - self._S2((1,0,1))*v[(1,0,1)] + - self._S2((1,1,1))*v[(1,1,1)] ) - M2 = ( self._S2((0,0,1))*v[(0,0,1)] + - self._S2((1,0,1))*v[(2,0,1)] + - self._S2((1,1,1))*v[(2,2,1)] ) - return (M1,M2) - - def __init__(self, scalar_field=QQ, **kwargs): - from sage.matrix.matrix_space import MatrixSpace - from sage.modules.free_module import VectorSpace - - M2 = MatrixSpace(scalar_field, 2) - b = M2.basis() - - from sage.sets.family import Family - S2_basis = Family({ (i,j,1) : - a*(b[(i,j)] + b[(j,i)]) - for i in range(2) - for j in range(i+1) - if (a := 1 - (i == j)/scalar_field(2)) - }) - - # M2.submodule() destroys our basis keys, so use - # SubmoduleWithBasis directly. - self._S2 = SubmoduleWithBasis(S2_basis, - support_order=b.keys(), - ambient=M2) - - # We need an Ishi basis (i,j,k) for R^5 if we want to use - # NormalDecomposition. We imagine the (2,1,1) component - # being trivial to keep the "triangle" intact. We are - # example (f) on page 91 of Gindikin where we can cheat - # and see the normal decomposition. - R5 = VectorSpace(scalar_field, [ - (0,0,1), - (1,0,1), - (1,1,1), - (2,0,1), - (2,2,1) - ]) - - # The Clan __init__ does this, but if we do it now then we can - # use the self.lift() and self.unlift() methods to define the - # clan product rather than duplicating them locally. - self._vector_space = R5 - - def cp(x,y): - # up_hat and down_hat need MatrixSpace elements, not - # submodules with custom bases, so we need to lift - # everything twice. - X1,X2 = self._lift(x) - Y1,Y2 = self._lift(y) - X1 = X1.lift() - X2 = X2.lift() - Y1 = Y1.lift() - Y2 = Y2.lift() - Z1 = MatrixClan._down_hat(X1)*Y1 + Y1*MatrixClan._up_hat(X1) - Z2 = MatrixClan._down_hat(X2)*Y2 + Y2*MatrixClan._up_hat(X2) - return self._unlift((self._S2(Z1), self._S2(Z2))) - - def ip(x,y): - two = x.base_ring()(2) - p = cp(x,y) / two - return sum( p[idx] - for idx in p.monomial_coefficients() - if idx[0] == idx[1] ) - - super().__init__(R5, cp, ip, **kwargs) - - def __repr__(self) -> str: - r""" - The string representation of this clan. - - SETUP:: - - sage: from mjo.clan.clan import VinbergClan - - EXAMPLES:: - - sage: VinbergClan() - Vinberg clan over Rational Field - - """ - return f"Vinberg clan over {self.base_ring()}" - - def random_triangular_cone_automorphism(self): - r""" - Generate a random triangular automorphism of the Vinberg cone. - - Elliot Herrington in his thesis "Highly symmetric homogeneous - Kobayashi-hyperbolic manifolds" gives a formula for the - connected component of the identity in the group of triangular - automorphisms. This won't generate the whole group, but it's - a good start. - """ - from sage.matrix.matrix_space import MatrixSpace - R = self.base_ring() - MS = MatrixSpace(R, 5) - - # Herrington's notation for triangular automorphisms - a = R._random_nonzero_element().abs() - b = R.random_element() - c = R.random_element() - e = R._random_nonzero_element() - i = R._random_nonzero_element() - - T = MS([ [a**2, b**2, c**2, 2*a*b, 2*a*c], - [ 0, e**2, 0, 0, 0], - [ 0, 0, i**2, 0, 0], - [ 0, b*e, 0, a*e, 0], - [ 0, 0, c*i, 0, a*i] ]) - - from mjo.clan.clan_operator import ClanOperator - return ClanOperator(self, self, T) - - - def random_isotropy_cone_automorphism(self): - r""" - Generate a random automorphism of the Vinberg cone that - fixes the unit element. - - This is effectively a guess, based on the work done by Ishi - and Koufany for the **dual** Vinberg cone. - - SETUP:: - - sage: from mjo.clan.clan import VinbergClan - - TESTS: - - Evidence for the conjecture that these preserve the two trace - inner products:: - - sage: C = VinbergClan() - sage: A = C.random_isotropy_cone_automorphism() - sage: A(C.one()) == C.one() - True - sage: x = C.random_element() - sage: y = C.random_element() - sage: x.inner_product(y) == A(x).inner_product(A(y)) - True - sage: expected = x.inner_product_vinberg(y) - sage: actual = A(x).inner_product_vinberg(A(y)) - sage: actual == expected - True - - """ - from sage.matrix.matrix_space import MatrixSpace - MS = MatrixSpace(self.base_ring(), 5) - # Now the generators of the isotropy subgroup (inspired by - # Ishi/Kounfany, but basically just guessing and checking). - # - # x21 -> -x21 - g1 = MS([ [1, 0,0,0,0], - [0,-1,0,0,0], - [0, 0,1,0,0], - [0, 0,0,1,0], - [0, 0,0,0,1] ]) - # x31 -> -x31 - g2 = MS([ [1,0,0, 0,0], - [0,1,0, 0,0], - [0,0,1, 0,0], - [0,0,0,-1,0], - [0,0,0, 0,1] ]) - # x32 <-> x33, x21 <-> x31 - g3 = MS([ [1,0,0,0,0], - [0,0,0,1,0], - [0,0,0,0,1], - [0,1,0,0,0], - [0,0,1,0,0] ]) - - # Group is order eight? - gs = [MS.one(), g1, g2, g3, g1*g2, g1*g3, g3*g1, g3*g2] - - from random import choice - from mjo.clan.clan_operator import ClanOperator - return ClanOperator(self, self, choice(gs)) - - def random_cone_automorphism(self): - r""" - Generate a random automorphism of the Vinberg cone. - """ - T = self.random_triangular_cone_automorphism() - K = self.random_isotropy_cone_automorphism() - return T*K - - def from_matrices(self, A, B): - r""" - Construct an element of this clan from a pair of - symmetric matrices. - - SETUP:: - - sage: from mjo.clan.clan import VinbergClan - - EXAMPLES:: - - sage: C = VinbergClan() - sage: A = matrix(QQ, [ [2, 1], - ....: [1, 4] ]) - sage: B = matrix(QQ, [ [2, 2], - ....: [2,-1] ]) - sage: C.from_matrices(A,B).to_vector() - (2, 1, 4, 2, -1) - - """ - if not A.base_ring() == self.base_ring(): - raise ValueError(f"base ring of matrix A ({A.base_ring()})" - " does not match clan ({self.base_ring()})") - if not B.base_ring() == self.base_ring(): - raise ValueError(f"base ring of matrix B ({A.base_ring()})" - " does not match clan ({self.base_ring()})") - if not A == A.transpose(): - raise ValueError("matrix A is not symmetric") - if not B == B.transpose(): - raise ValueError("matrix B is not symmetric") - if not A.nrows() == 2: - raise ValueError(f"matrix A must be 2x2") - if not B.nrows() == 2: - raise ValueError(f"matrix B must be 2x2") - if not A[0,0] == B[0,0]: - raise ValueError(f"A and B must agree in the (0,0) position") - - return ( A[0,0]*self((0,0,1)) + - A[1,0]*self((1,0,1)) + - A[1,1]*self((1,1,1)) + - B[1,0]*self((2,0,1)) + - B[1,1]*self((2,2,1)) ) - - def from_list(self, l): - r""" - Construct an element of this clan from a list. - - This is a bit different from the other ``from_list`` methods - on matrix clans, because the underlying vector space for the - Vinberg clan is basically `R^5`. So this method just takes a - list of five numbers and returns a clan element. It is a - trivial wrapper around :meth:`from_vector`. - - SETUP:: - - sage: from mjo.clan.clan import VinbergClan - - EXAMPLES:: - - sage: C = VinbergClan() - sage: x = C.from_list([2,1,4,2,-1]) - sage: x.to_vector() - (2, 1, 4, 2, -1) - - """ - return self.sum_of_terms(zip(self.basis().keys(), l)) - - def from_lists(self, l1, l2): - r - """ - Construct an element of this clan from a pair of lists. - - This is a shortcut for :meth:`from_matrix`, as the clan knows - what the ambient matrix space was. - - SETUP:: - - sage: from mjo.clan.clan import VinbergClan - - EXAMPLES:: - - sage: C = VinbergClan() - sage: x = C.from_lists([[2,1],[1,4]], [[2,2],[2,-1]]) - sage: x.to_vector() - (2, 1, 4, 2, -1) - - This relies on the ambient vector space to convert a list to a - matrix, so we can use one long list (as opposed to a list of - lists) in each component:: - - sage: C = VinbergClan() - sage: x = C.from_lists([2,1,1,4],[2,2,2,-1]) - sage: x.to_vector() - (2, 1, 4, 2, -1) - - """ - A = self._S2.ambient()(l1) - B = self._S2.ambient()(l2) - return self.from_matrices(A,B) diff --git a/mjo/clan/clan_element.py b/mjo/clan/clan_element.py index 1cf73a3..1f35c44 100644 --- a/mjo/clan/clan_element.py +++ b/mjo/clan/clan_element.py @@ -81,7 +81,7 @@ class ClanElement(IndexedFreeModuleElement): SETUP:: - sage: from mjo.clan.clan import VinbergClan + sage: from mjo.clan.vinberg_clan import VinbergClan EXAMPLES:: @@ -106,7 +106,7 @@ class ClanElement(IndexedFreeModuleElement): SETUP:: - sage: from mjo.clan.clan import VinbergClan + sage: from mjo.clan.vinberg_clan import VinbergClan EXAMPLES:: @@ -351,7 +351,8 @@ class NormalDecompositionElement(ClanElement): SETUP:: - sage: from mjo.clan.clan import RealSymmetricClan, VinbergClan + sage: from mjo.clan.clan import RealSymmetricClan + sage: from mjo.clan.vinberg_clan import VinbergClan EXAMPLES: diff --git a/mjo/clan/clan_operator.py b/mjo/clan/clan_operator.py index a85c7e1..743aa5f 100644 --- a/mjo/clan/clan_operator.py +++ b/mjo/clan/clan_operator.py @@ -72,7 +72,7 @@ class ClanOperator(Map): SETUP:: sage: from mjo.clan.clan_operator import ClanOperator - sage: from mjo.clan.clan import VinbergClan + sage: from mjo.clan.vinberg_clan import VinbergClan EXAMPLES:: @@ -95,8 +95,8 @@ class ClanOperator(Map): sage: from mjo.clan.clan_operator import ClanOperator sage: from mjo.clan.clan import ( ComplexHermitianClan, - ....: RealSymmetricClan, - ....: VinbergClan ) + ....: RealSymmetricClan ) + sage: from mjo.clan.vinberg_clan import VinbergClan EXAMPLES:: diff --git a/mjo/clan/vinberg_clan.py b/mjo/clan/vinberg_clan.py new file mode 100644 index 0000000..9f6051b --- /dev/null +++ b/mjo/clan/vinberg_clan.py @@ -0,0 +1,427 @@ +from sage.rings.rational_field import QQ +from mjo.clan.clan import MatrixClan, NormalDecomposition + +class VinbergClan(NormalDecomposition): + r""" + The clan corresponding to the Vinberg cone (as defined by Ishi). + + The Vinberg cone lives in a space whose points are pairs of real + 2x2 symmetric matrices that agree in the first coordinate. The + cone itself is the subset where both elements of the pair are + positive-semidefinite. + + Ishi describes this space as x = (a,b) where + + a = [ x11 x21 ] + [ x21 x22 ], + + b = [ x11 x31 ] + [ x31 x33 ] + + As there is no obvious way to express this in Sage, we instead + simply write out the x = (x11, x21, x22, x31, x33) as a vector of + real numbers, and provide lifts to the matrix representation. The + clan product is in terms of the up-hat and down-hat that is + defined on `S^{2}. + + Following Gikdikin (p. 91), the basis for this clan is + + e[(0,0,1)] = [1,0], [1,0] + [0,0] [0,0] + + e[(1,0,1)] = [0,1], [0,0] + [1,0] [0,0] + + e[(1,1,1)] = [0,0], [0,0] + [0,1] [0,0] + + e[(2,0,1)] = [0,0], [0,1] + [0,0] [1,0] + + e[(2,1,1)] = nonexistent, component is trivial + + e[(2,2,1)] = [0,0], [0,0] + [0,0] [0,1] + + SETUP:: + + sage: from mjo.clan.vinberg_clan import VinbergClan + + EXAMPLES: + + Check the unit element:: + + sage: C = VinbergClan() + sage: x = C.an_element() + sage: x*C.one() == x + True + sage: C.one()*x == x + True + + Verifying the axioms:: + + sage: C = VinbergClan() + sage: e = C.basis() + sage: all( e[i,i,1]*e[i,i,1] == e[i,i,1] for i in range(3) ) + True + sage: all( e[i,i,1]*e[j,i,1] == e[j,i,1]/2 + ....: for i in range(3) + ....: for j in range(i+1,3) + ....: if e.has_key((j,i,1))) + True + sage: all( e[i,i,1]*e[i,k,1] == e[i,k,1]/2 + ....: for i in range(2) + ....: for k in range(i)) + True + sage: all( (e[i,i,1]*e[j,k,1]).is_zero() + ....: for i in range(2) + ....: for j in range(2) + ....: for k in range(j+1) + ....: if not i in [j,k] ) + True + sage: all( (e[i,k,1]*e[i,i,1]).is_zero() + ....: for i in range(2) + ....: for k in range(i) ) + True + sage: all( (e[j,k,1]*e[i,i,1]).is_zero() + ....: for i in range(2) + ....: for j in range(2) + ....: for k in range(j) + ....: if not i in [j,k] ) + True + + The multiplication in this clan (verified by hand) is as + follows):: + + sage: C = VinbergClan() + sage: x = C.random_element() + sage: y = C.random_element() + sage: z = x*y + sage: x = x.to_vector() + sage: y = y.to_vector() + sage: z = z.to_vector() + sage: z[0] == x[0]*y[0] + True + sage: z[1] == y[1]*(x[0] + x[2])/QQ(2) + y[0]*x[1] + True + sage: z[2] == 2*x[1]*y[1] + x[2]*y[2] + True + sage: z[3] == y[3]*(x[0] + x[4])/QQ(2) + y[0]*x[3] + True + sage: z[4] == 2*x[3]*y[3] + x[4]*y[4] + True + + The Ishi inner product in this clan (verified by hand) is as + follows:: + + sage: C = VinbergClan() + sage: x = C.random_element() + sage: y = C.random_element() + sage: actual = x.inner_product(y) + sage: x = x.to_vector() + sage: y = y.to_vector() + sage: expected = ( x[0]*y[0] + x[2]*y[2] + x[4]*y[4] + + ....: 2*x[1]*y[1] + 2*x[3]*y[3] ) / QQ(2) + sage: actual == expected + True + + """ + def _unlift(self, pair): + A,B = pair + R5 = self._vector_space + return ( A[(0,0,1)]*R5((0,0,1)) + + A[(1,0,1)]*R5((1,0,1)) + + A[(1,1,1)]*R5((1,1,1)) + + B[(1,0,1)]*R5((2,0,1)) + + B[(1,1,1)]*R5((2,2,1)) ) + + def _lift(self, v): + M1 = ( self._S2((0,0,1))*v[(0,0,1)] + + self._S2((1,0,1))*v[(1,0,1)] + + self._S2((1,1,1))*v[(1,1,1)] ) + M2 = ( self._S2((0,0,1))*v[(0,0,1)] + + self._S2((1,0,1))*v[(2,0,1)] + + self._S2((1,1,1))*v[(2,2,1)] ) + return (M1,M2) + + def __init__(self, scalar_field=QQ, **kwargs): + from sage.matrix.matrix_space import MatrixSpace + from sage.modules.free_module import VectorSpace + + M2 = MatrixSpace(scalar_field, 2) + b = M2.basis() + + from sage.sets.family import Family + S2_basis = Family({ (i,j,1) : + a*(b[(i,j)] + b[(j,i)]) + for i in range(2) + for j in range(i+1) + if (a := 1 - (i == j)/scalar_field(2)) + }) + + # M2.submodule() destroys our basis keys, so use + # SubmoduleWithBasis directly. + from sage.modules.with_basis.subquotient import ( + SubmoduleWithBasis + ) + self._S2 = SubmoduleWithBasis(S2_basis, + support_order=b.keys(), + ambient=M2) + + # We need an Ishi basis (i,j,k) for R^5 if we want to use + # NormalDecomposition. We imagine the (2,1,1) component + # being trivial to keep the "triangle" intact. We are + # example (f) on page 91 of Gindikin where we can cheat + # and see the normal decomposition. + R5 = VectorSpace(scalar_field, [ + (0,0,1), + (1,0,1), + (1,1,1), + (2,0,1), + (2,2,1) + ]) + + # The Clan __init__ does this, but if we do it now then we can + # use the self.lift() and self.unlift() methods to define the + # clan product rather than duplicating them locally. + self._vector_space = R5 + + def cp(x,y): + # up_hat and down_hat need MatrixSpace elements, not + # submodules with custom bases, so we need to lift + # everything twice. + X1,X2 = self._lift(x) + Y1,Y2 = self._lift(y) + X1 = X1.lift() + X2 = X2.lift() + Y1 = Y1.lift() + Y2 = Y2.lift() + Z1 = MatrixClan._down_hat(X1)*Y1 + Y1*MatrixClan._up_hat(X1) + Z2 = MatrixClan._down_hat(X2)*Y2 + Y2*MatrixClan._up_hat(X2) + return self._unlift((self._S2(Z1), self._S2(Z2))) + + def ip(x,y): + two = x.base_ring()(2) + p = cp(x,y) / two + return sum( p[idx] + for idx in p.monomial_coefficients() + if idx[0] == idx[1] ) + + super().__init__(R5, cp, ip, **kwargs) + + def __repr__(self) -> str: + r""" + The string representation of this clan. + + SETUP:: + + sage: from mjo.clan.vinberg_clan import VinbergClan + + EXAMPLES:: + + sage: VinbergClan() + Vinberg clan over Rational Field + + """ + return f"Vinberg clan over {self.base_ring()}" + + def random_triangular_cone_automorphism(self): + r""" + Generate a random triangular automorphism of the Vinberg cone. + + Elliot Herrington in his thesis "Highly symmetric homogeneous + Kobayashi-hyperbolic manifolds" gives a formula for the + connected component of the identity in the group of triangular + automorphisms. This won't generate the whole group, but it's + a good start. + """ + from sage.matrix.matrix_space import MatrixSpace + R = self.base_ring() + MS = MatrixSpace(R, 5) + + # Herrington's notation for triangular automorphisms + a = R._random_nonzero_element().abs() + b = R.random_element() + c = R.random_element() + e = R._random_nonzero_element() + i = R._random_nonzero_element() + + T = MS([ [a**2, b**2, c**2, 2*a*b, 2*a*c], + [ 0, e**2, 0, 0, 0], + [ 0, 0, i**2, 0, 0], + [ 0, b*e, 0, a*e, 0], + [ 0, 0, c*i, 0, a*i] ]) + + from mjo.clan.clan_operator import ClanOperator + return ClanOperator(self, self, T) + + + def random_isotropy_cone_automorphism(self): + r""" + Generate a random automorphism of the Vinberg cone that + fixes the unit element. + + This is effectively a guess, based on the work done by Ishi + and Koufany for the **dual** Vinberg cone. + + SETUP:: + + sage: from mjo.clan.vinberg_clan import VinbergClan + + TESTS: + + Evidence for the conjecture that these preserve the two trace + inner products:: + + sage: C = VinbergClan() + sage: A = C.random_isotropy_cone_automorphism() + sage: A(C.one()) == C.one() + True + sage: x = C.random_element() + sage: y = C.random_element() + sage: x.inner_product(y) == A(x).inner_product(A(y)) + True + sage: expected = x.inner_product_vinberg(y) + sage: actual = A(x).inner_product_vinberg(A(y)) + sage: actual == expected + True + + """ + from sage.matrix.matrix_space import MatrixSpace + MS = MatrixSpace(self.base_ring(), 5) + # Now the generators of the isotropy subgroup (inspired by + # Ishi/Kounfany, but basically just guessing and checking). + # + # x21 -> -x21 + g1 = MS([ [1, 0,0,0,0], + [0,-1,0,0,0], + [0, 0,1,0,0], + [0, 0,0,1,0], + [0, 0,0,0,1] ]) + # x31 -> -x31 + g2 = MS([ [1,0,0, 0,0], + [0,1,0, 0,0], + [0,0,1, 0,0], + [0,0,0,-1,0], + [0,0,0, 0,1] ]) + # x32 <-> x33, x21 <-> x31 + g3 = MS([ [1,0,0,0,0], + [0,0,0,1,0], + [0,0,0,0,1], + [0,1,0,0,0], + [0,0,1,0,0] ]) + + # Group is order eight? + gs = [MS.one(), g1, g2, g3, g1*g2, g1*g3, g3*g1, g3*g2] + + from random import choice + from mjo.clan.clan_operator import ClanOperator + return ClanOperator(self, self, choice(gs)) + + def random_cone_automorphism(self): + r""" + Generate a random automorphism of the Vinberg cone. + """ + T = self.random_triangular_cone_automorphism() + K = self.random_isotropy_cone_automorphism() + return T*K + + def from_matrices(self, A, B): + r""" + Construct an element of this clan from a pair of + symmetric matrices. + + SETUP:: + + sage: from mjo.clan.vinberg_clan import VinbergClan + + EXAMPLES:: + + sage: C = VinbergClan() + sage: A = matrix(QQ, [ [2, 1], + ....: [1, 4] ]) + sage: B = matrix(QQ, [ [2, 2], + ....: [2,-1] ]) + sage: C.from_matrices(A,B).to_vector() + (2, 1, 4, 2, -1) + + """ + if not A.base_ring() == self.base_ring(): + raise ValueError(f"base ring of matrix A ({A.base_ring()})" + " does not match clan ({self.base_ring()})") + if not B.base_ring() == self.base_ring(): + raise ValueError(f"base ring of matrix B ({A.base_ring()})" + " does not match clan ({self.base_ring()})") + if not A == A.transpose(): + raise ValueError("matrix A is not symmetric") + if not B == B.transpose(): + raise ValueError("matrix B is not symmetric") + if not A.nrows() == 2: + raise ValueError(f"matrix A must be 2x2") + if not B.nrows() == 2: + raise ValueError(f"matrix B must be 2x2") + if not A[0,0] == B[0,0]: + raise ValueError(f"A and B must agree in the (0,0) position") + + return ( A[0,0]*self((0,0,1)) + + A[1,0]*self((1,0,1)) + + A[1,1]*self((1,1,1)) + + B[1,0]*self((2,0,1)) + + B[1,1]*self((2,2,1)) ) + + def from_list(self, l): + r""" + Construct an element of this clan from a list. + + This is a bit different from the other ``from_list`` methods + on matrix clans, because the underlying vector space for the + Vinberg clan is basically `R^5`. So this method just takes a + list of five numbers and returns a clan element. It is a + trivial wrapper around :meth:`from_vector`. + + SETUP:: + + sage: from mjo.clan.vinberg_clan import VinbergClan + + EXAMPLES:: + + sage: C = VinbergClan() + sage: x = C.from_list([2,1,4,2,-1]) + sage: x.to_vector() + (2, 1, 4, 2, -1) + + """ + return self.sum_of_terms(zip(self.basis().keys(), l)) + + def from_lists(self, l1, l2): + r + """ + Construct an element of this clan from a pair of lists. + + This is a shortcut for :meth:`from_matrix`, as the clan knows + what the ambient matrix space was. + + SETUP:: + + sage: from mjo.clan.vinberg_clan import VinbergClan + + EXAMPLES:: + + sage: C = VinbergClan() + sage: x = C.from_lists([[2,1],[1,4]], [[2,2],[2,-1]]) + sage: x.to_vector() + (2, 1, 4, 2, -1) + + This relies on the ambient vector space to convert a list to a + matrix, so we can use one long list (as opposed to a list of + lists) in each component:: + + sage: C = VinbergClan() + sage: x = C.from_lists([2,1,1,4],[2,2,2,-1]) + sage: x.to_vector() + (2, 1, 4, 2, -1) + + """ + A = self._S2.ambient()(l1) + B = self._S2.ambient()(l2) + return self.from_matrices(A,B)