]> gitweb.michael.orlitzky.com - sage.d.git/commitdiff
mjo/clan: factor out mjo.clan.vinberg_clan
authorMichael Orlitzky <michael@orlitzky.com>
Sun, 22 Feb 2026 16:42:50 +0000 (11:42 -0500)
committerMichael Orlitzky <michael@orlitzky.com>
Sun, 22 Feb 2026 16:43:08 +0000 (11:43 -0500)
mjo/clan/all.py
mjo/clan/clan.py
mjo/clan/clan_element.py
mjo/clan/clan_operator.py
mjo/clan/vinberg_clan.py [new file with mode: 0644]

index 3467745f3fd53f273bb0212669f03afda628b95a..f98232a56063a5f4ad9e62ec68102dacf5fbd564 100644 (file)
@@ -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
index 48383606d1d5f7fdd23501424850cf3bf725e580..38dd1691c5f39fbc684ef18041208712d75ef2bf 100644 (file)
@@ -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)
index 1cf73a3083963b157f5a32643aa50e5843d92420..1f35c444de2950181d11d5cdef7f4d6488d58e70 100644 (file)
@@ -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:
 
index a85c7e135b5f3f29844c057cfc12df3a8da8fb53..743aa5f67e2696b99910ad3ae9d598b6dab65861 100644 (file)
@@ -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 (file)
index 0000000..9f6051b
--- /dev/null
@@ -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)